@jonit-dev/night-watch-cli 1.8.12-beta.3 → 1.8.12-beta.4

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 (43) hide show
  1. package/dist/cli.js +1718 -380
  2. package/dist/commands/analytics.d.ts.map +1 -1
  3. package/dist/commands/analytics.js +60 -0
  4. package/dist/commands/analytics.js.map +1 -1
  5. package/dist/commands/audit.d.ts.map +1 -1
  6. package/dist/commands/audit.js +45 -0
  7. package/dist/commands/audit.js.map +1 -1
  8. package/dist/commands/init.d.ts.map +1 -1
  9. package/dist/commands/init.js +1 -0
  10. package/dist/commands/init.js.map +1 -1
  11. package/dist/commands/merge.d.ts.map +1 -1
  12. package/dist/commands/merge.js +30 -3
  13. package/dist/commands/merge.js.map +1 -1
  14. package/dist/commands/plan.d.ts.map +1 -1
  15. package/dist/commands/plan.js +45 -0
  16. package/dist/commands/plan.js.map +1 -1
  17. package/dist/commands/qa.d.ts.map +1 -1
  18. package/dist/commands/qa.js +24 -0
  19. package/dist/commands/qa.js.map +1 -1
  20. package/dist/commands/resolve.d.ts.map +1 -1
  21. package/dist/commands/resolve.js +26 -0
  22. package/dist/commands/resolve.js.map +1 -1
  23. package/dist/commands/review.d.ts +2 -0
  24. package/dist/commands/review.d.ts.map +1 -1
  25. package/dist/commands/review.js +46 -1
  26. package/dist/commands/review.js.map +1 -1
  27. package/dist/commands/run.d.ts +16 -1
  28. package/dist/commands/run.d.ts.map +1 -1
  29. package/dist/commands/run.js +80 -1
  30. package/dist/commands/run.js.map +1 -1
  31. package/dist/commands/shared/feedback.d.ts +24 -0
  32. package/dist/commands/shared/feedback.d.ts.map +1 -0
  33. package/dist/commands/shared/feedback.js +38 -0
  34. package/dist/commands/shared/feedback.js.map +1 -0
  35. package/dist/commands/slice.d.ts.map +1 -1
  36. package/dist/commands/slice.js +48 -1
  37. package/dist/commands/slice.js.map +1 -1
  38. package/dist/scripts/night-watch-cron.sh +5 -0
  39. package/dist/scripts/night-watch-pr-reviewer-cron.sh +4 -0
  40. package/dist/web/assets/index-DpvzoXEv.js +442 -0
  41. package/dist/web/assets/index-DyME41HV.css +1 -0
  42. package/dist/web/index.html +2 -2
  43. package/package.json +3 -1
package/dist/cli.js CHANGED
@@ -390,7 +390,7 @@ function resolveProviderBucketKey(provider, providerEnv) {
390
390
  return `claude-proxy:${baseUrl}`;
391
391
  }
392
392
  }
393
- var DEFAULT_DEFAULT_BRANCH, DEFAULT_PRD_DIR, DEFAULT_SUMMARY_WINDOW_HOURS, DEFAULT_MAX_RUNTIME, DEFAULT_REVIEWER_MAX_RUNTIME, DEFAULT_CRON_SCHEDULE, DEFAULT_REVIEWER_SCHEDULE, DEFAULT_CRON_SCHEDULE_OFFSET, DEFAULT_MAX_RETRIES, DEFAULT_REVIEWER_MAX_RETRIES, DEFAULT_REVIEWER_RETRY_DELAY, DEFAULT_REVIEWER_MAX_PRS_PER_RUN, DEFAULT_BRANCH_PREFIX, DEFAULT_BRANCH_PATTERNS, DEFAULT_MIN_REVIEW_SCORE, DEFAULT_MAX_LOG_SIZE, DEFAULT_PROVIDER, DEFAULT_EXECUTOR_ENABLED, DEFAULT_REVIEWER_ENABLED, DEFAULT_PROVIDER_ENV, DEFAULT_FALLBACK_ON_RATE_LIMIT, DEFAULT_CLAUDE_MODEL, DEFAULT_PRIMARY_FALLBACK_MODEL, DEFAULT_SECONDARY_FALLBACK_MODEL, VALID_CLAUDE_MODELS, CLAUDE_MODEL_IDS, DEFAULT_NOTIFICATIONS, DEFAULT_PRD_PRIORITY, DEFAULT_SLICER_SCHEDULE, DEFAULT_SLICER_MAX_RUNTIME, DEFAULT_ROADMAP_SCANNER, DEFAULT_TEMPLATES_DIR, DEFAULT_BOARD_PROVIDER, DEFAULT_LOCAL_BOARD_INFO, DEFAULT_AUTO_MERGE, DEFAULT_AUTO_MERGE_METHOD, VALID_MERGE_METHODS, DEFAULT_QA_ENABLED, DEFAULT_QA_SCHEDULE, DEFAULT_QA_MAX_RUNTIME, DEFAULT_QA_ARTIFACTS, DEFAULT_QA_SKIP_LABEL, DEFAULT_QA_AUTO_INSTALL_PLAYWRIGHT, DEFAULT_QA_VALIDATED_LABEL, DEFAULT_QA, QA_LOG_NAME, DEFAULT_AUDIT_ENABLED, DEFAULT_AUDIT_SCHEDULE, DEFAULT_AUDIT_MAX_RUNTIME, DEFAULT_AUDIT_TARGET_COLUMN, DEFAULT_AUDIT, DEFAULT_ANALYTICS_ENABLED, DEFAULT_ANALYTICS_SCHEDULE, DEFAULT_ANALYTICS_MAX_RUNTIME, DEFAULT_ANALYTICS_LOOKBACK_DAYS, DEFAULT_ANALYTICS_TARGET_COLUMN, DEFAULT_ANALYTICS_PROMPT, DEFAULT_ANALYTICS, DEFAULT_PR_RESOLVER_ENABLED, DEFAULT_PR_RESOLVER_SCHEDULE, DEFAULT_PR_RESOLVER_MAX_RUNTIME, DEFAULT_PR_RESOLVER_MAX_PRS_PER_RUN, DEFAULT_PR_RESOLVER_PER_PR_TIMEOUT, DEFAULT_PR_RESOLVER_AI_CONFLICT_RESOLUTION, DEFAULT_PR_RESOLVER_AI_REVIEW_RESOLUTION, DEFAULT_PR_RESOLVER_READY_LABEL, DEFAULT_PR_RESOLVER, DEFAULT_MERGER_ENABLED, DEFAULT_MERGER_SCHEDULE, DEFAULT_MERGER_MAX_RUNTIME, DEFAULT_MERGER_MERGE_METHOD, DEFAULT_MERGER_MIN_REVIEW_SCORE, DEFAULT_MERGER_REBASE_BEFORE_MERGE, DEFAULT_MERGER_MAX_PRS_PER_RUN, DEFAULT_MERGER, MERGER_LOG_NAME, AUDIT_LOG_NAME, PLANNER_LOG_NAME, ANALYTICS_LOG_NAME, PR_RESOLVER_LOG_NAME, VALID_PROVIDERS, VALID_JOB_TYPES, DEFAULT_JOB_PROVIDERS, DEFAULT_PROVIDER_SCHEDULE_OVERRIDES, DEFAULT_WEBHOOK_TRIGGER_SECRET_ENV, DEFAULT_WEBHOOK_TRIGGER_MAX_SKEW_SECONDS, DEFAULT_WEBHOOK_TRIGGERS, BUILT_IN_PRESETS, BUILT_IN_PRESET_IDS, PROVIDER_COMMANDS, CONFIG_FILE_NAME, LOCK_FILE_PREFIX, LOG_DIR, CLAIM_FILE_EXTENSION, EXECUTOR_LOG_NAME, REVIEWER_LOG_NAME, EXECUTOR_LOG_FILE, REVIEWER_LOG_FILE, LOG_FILE_NAMES, GLOBAL_CONFIG_DIR, REGISTRY_FILE_NAME, HISTORY_FILE_NAME, PRD_STATES_FILE_NAME, STATE_DB_FILE_NAME, GLOBAL_NOTIFICATIONS_FILE_NAME, MAX_HISTORY_RECORDS_PER_PRD, DEFAULT_QUEUE_ENABLED, DEFAULT_QUEUE_MODE, DEFAULT_QUEUE_MAX_CONCURRENCY, DEFAULT_QUEUE_MAX_WAIT_TIME, DEFAULT_QUEUE_PRIORITY, DEFAULT_QUEUE, DEFAULT_SCHEDULING_PRIORITY;
393
+ var DEFAULT_DEFAULT_BRANCH, DEFAULT_PRD_DIR, DEFAULT_SUMMARY_WINDOW_HOURS, DEFAULT_MAX_RUNTIME, DEFAULT_REVIEWER_MAX_RUNTIME, DEFAULT_CRON_SCHEDULE, DEFAULT_REVIEWER_SCHEDULE, DEFAULT_CRON_SCHEDULE_OFFSET, DEFAULT_MAX_RETRIES, DEFAULT_REVIEWER_MAX_RETRIES, DEFAULT_REVIEWER_RETRY_DELAY, DEFAULT_REVIEWER_MAX_PRS_PER_RUN, DEFAULT_FEEDBACK, DEFAULT_BRANCH_PREFIX, DEFAULT_BRANCH_PATTERNS, DEFAULT_MIN_REVIEW_SCORE, DEFAULT_MAX_LOG_SIZE, DEFAULT_PROVIDER, DEFAULT_EXECUTOR_ENABLED, DEFAULT_REVIEWER_ENABLED, DEFAULT_PROVIDER_ENV, DEFAULT_FALLBACK_ON_RATE_LIMIT, DEFAULT_CLAUDE_MODEL, DEFAULT_PRIMARY_FALLBACK_MODEL, DEFAULT_SECONDARY_FALLBACK_MODEL, VALID_CLAUDE_MODELS, CLAUDE_MODEL_IDS, DEFAULT_NOTIFICATIONS, DEFAULT_PRD_PRIORITY, DEFAULT_SLICER_SCHEDULE, DEFAULT_SLICER_MAX_RUNTIME, DEFAULT_ROADMAP_SCANNER, DEFAULT_TEMPLATES_DIR, DEFAULT_BOARD_PROVIDER, DEFAULT_LOCAL_BOARD_INFO, DEFAULT_AUTO_MERGE, DEFAULT_AUTO_MERGE_METHOD, VALID_MERGE_METHODS, DEFAULT_QA_ENABLED, DEFAULT_QA_SCHEDULE, DEFAULT_QA_MAX_RUNTIME, DEFAULT_QA_ARTIFACTS, DEFAULT_QA_SKIP_LABEL, DEFAULT_QA_AUTO_INSTALL_PLAYWRIGHT, DEFAULT_QA_VALIDATED_LABEL, DEFAULT_QA, QA_LOG_NAME, DEFAULT_AUDIT_ENABLED, DEFAULT_AUDIT_SCHEDULE, DEFAULT_AUDIT_MAX_RUNTIME, DEFAULT_AUDIT_TARGET_COLUMN, DEFAULT_AUDIT, DEFAULT_ANALYTICS_ENABLED, DEFAULT_ANALYTICS_SCHEDULE, DEFAULT_ANALYTICS_MAX_RUNTIME, DEFAULT_ANALYTICS_LOOKBACK_DAYS, DEFAULT_ANALYTICS_TARGET_COLUMN, DEFAULT_ANALYTICS_PROMPT, DEFAULT_ANALYTICS, DEFAULT_PR_RESOLVER_ENABLED, DEFAULT_PR_RESOLVER_SCHEDULE, DEFAULT_PR_RESOLVER_MAX_RUNTIME, DEFAULT_PR_RESOLVER_MAX_PRS_PER_RUN, DEFAULT_PR_RESOLVER_PER_PR_TIMEOUT, DEFAULT_PR_RESOLVER_AI_CONFLICT_RESOLUTION, DEFAULT_PR_RESOLVER_AI_REVIEW_RESOLUTION, DEFAULT_PR_RESOLVER_READY_LABEL, DEFAULT_PR_RESOLVER, DEFAULT_MERGER_ENABLED, DEFAULT_MERGER_SCHEDULE, DEFAULT_MERGER_MAX_RUNTIME, DEFAULT_MERGER_MERGE_METHOD, DEFAULT_MERGER_MIN_REVIEW_SCORE, DEFAULT_MERGER_REBASE_BEFORE_MERGE, DEFAULT_MERGER_MAX_PRS_PER_RUN, DEFAULT_MERGER, MERGER_LOG_NAME, AUDIT_LOG_NAME, PLANNER_LOG_NAME, ANALYTICS_LOG_NAME, PR_RESOLVER_LOG_NAME, VALID_PROVIDERS, VALID_JOB_TYPES, DEFAULT_JOB_PROVIDERS, DEFAULT_PROVIDER_SCHEDULE_OVERRIDES, DEFAULT_WEBHOOK_TRIGGER_SECRET_ENV, DEFAULT_WEBHOOK_TRIGGER_MAX_SKEW_SECONDS, DEFAULT_WEBHOOK_TRIGGERS, BUILT_IN_PRESETS, BUILT_IN_PRESET_IDS, PROVIDER_COMMANDS, CONFIG_FILE_NAME, LOCK_FILE_PREFIX, LOG_DIR, CLAIM_FILE_EXTENSION, EXECUTOR_LOG_NAME, REVIEWER_LOG_NAME, EXECUTOR_LOG_FILE, REVIEWER_LOG_FILE, LOG_FILE_NAMES, GLOBAL_CONFIG_DIR, REGISTRY_FILE_NAME, HISTORY_FILE_NAME, PRD_STATES_FILE_NAME, STATE_DB_FILE_NAME, GLOBAL_NOTIFICATIONS_FILE_NAME, MAX_HISTORY_RECORDS_PER_PRD, DEFAULT_QUEUE_ENABLED, DEFAULT_QUEUE_MODE, DEFAULT_QUEUE_MAX_CONCURRENCY, DEFAULT_QUEUE_MAX_WAIT_TIME, DEFAULT_QUEUE_PRIORITY, DEFAULT_QUEUE, DEFAULT_SCHEDULING_PRIORITY;
394
394
  var init_constants = __esm({
395
395
  "../core/dist/constants.js"() {
396
396
  "use strict";
@@ -407,6 +407,13 @@ var init_constants = __esm({
407
407
  DEFAULT_REVIEWER_MAX_RETRIES = 2;
408
408
  DEFAULT_REVIEWER_RETRY_DELAY = 30;
409
409
  DEFAULT_REVIEWER_MAX_PRS_PER_RUN = 0;
410
+ DEFAULT_FEEDBACK = {
411
+ enabled: true,
412
+ confidenceThreshold: 0.75,
413
+ augmentationTtlDays: 14,
414
+ maxActiveAugmentations: 3,
415
+ successStreakToExpire: 3
416
+ };
410
417
  DEFAULT_BRANCH_PREFIX = "night-watch";
411
418
  DEFAULT_BRANCH_PATTERNS = ["feat/", "night-watch/"];
412
419
  DEFAULT_MIN_REVIEW_SCORE = 80;
@@ -692,6 +699,16 @@ function normalizeConfig(rawConfig) {
692
699
  normalized.reviewerMaxRetries = readNumber(rawConfig.reviewerMaxRetries);
693
700
  normalized.reviewerRetryDelay = readNumber(rawConfig.reviewerRetryDelay);
694
701
  normalized.reviewerMaxPrsPerRun = readNumber(rawConfig.reviewerMaxPrsPerRun);
702
+ const rawFeedback = readObject2(rawConfig.feedback);
703
+ if (rawFeedback) {
704
+ normalized.feedback = {
705
+ enabled: readBoolean(rawFeedback.enabled) ?? true,
706
+ confidenceThreshold: readNumber(rawFeedback.confidenceThreshold) ?? 0.75,
707
+ augmentationTtlDays: readNumber(rawFeedback.augmentationTtlDays) ?? 14,
708
+ maxActiveAugmentations: readNumber(rawFeedback.maxActiveAugmentations) ?? 3,
709
+ successStreakToExpire: readNumber(rawFeedback.successStreakToExpire) ?? 3
710
+ };
711
+ }
695
712
  normalized.provider = validateProvider(String(rawConfig.provider ?? "")) ?? void 0;
696
713
  normalized.executorEnabled = readBoolean(rawConfig.executorEnabled);
697
714
  normalized.reviewerEnabled = readBoolean(rawConfig.reviewerEnabled);
@@ -1082,6 +1099,27 @@ function buildEnvOverrideConfig(fileConfig) {
1082
1099
  if (!isNaN(v) && v >= 0)
1083
1100
  env.reviewerMaxPrsPerRun = v;
1084
1101
  }
1102
+ if (process.env.NW_FEEDBACK_ENABLED !== void 0 || process.env.NW_FEEDBACK_CONFIDENCE_THRESHOLD !== void 0 || process.env.NW_FEEDBACK_AUGMENTATION_TTL_DAYS !== void 0 || process.env.NW_FEEDBACK_MAX_ACTIVE_AUGMENTATIONS !== void 0 || process.env.NW_FEEDBACK_SUCCESS_STREAK_TO_EXPIRE !== void 0) {
1103
+ const feedback = { ...fileConfig?.feedback ?? {} };
1104
+ const enabled = process.env.NW_FEEDBACK_ENABLED ? parseBoolean(process.env.NW_FEEDBACK_ENABLED) : null;
1105
+ if (enabled !== null)
1106
+ feedback.enabled = enabled;
1107
+ const confidenceThreshold = parseFloat(process.env.NW_FEEDBACK_CONFIDENCE_THRESHOLD ?? "");
1108
+ if (!Number.isNaN(confidenceThreshold))
1109
+ feedback.confidenceThreshold = confidenceThreshold;
1110
+ const augmentationTtlDays = parseInt(process.env.NW_FEEDBACK_AUGMENTATION_TTL_DAYS ?? "", 10);
1111
+ if (!Number.isNaN(augmentationTtlDays))
1112
+ feedback.augmentationTtlDays = augmentationTtlDays;
1113
+ const maxActiveAugmentations = parseInt(process.env.NW_FEEDBACK_MAX_ACTIVE_AUGMENTATIONS ?? "", 10);
1114
+ if (!Number.isNaN(maxActiveAugmentations)) {
1115
+ feedback.maxActiveAugmentations = maxActiveAugmentations;
1116
+ }
1117
+ const successStreakToExpire = parseInt(process.env.NW_FEEDBACK_SUCCESS_STREAK_TO_EXPIRE ?? "", 10);
1118
+ if (!Number.isNaN(successStreakToExpire)) {
1119
+ feedback.successStreakToExpire = successStreakToExpire;
1120
+ }
1121
+ env.feedback = feedback;
1122
+ }
1085
1123
  if (process.env.NW_PROVIDER) {
1086
1124
  const p = validateProvider(process.env.NW_PROVIDER);
1087
1125
  if (p !== null)
@@ -1297,6 +1335,7 @@ function getDefaultConfig() {
1297
1335
  qa: { ...DEFAULT_QA },
1298
1336
  audit: { ...DEFAULT_AUDIT },
1299
1337
  analytics: { ...DEFAULT_ANALYTICS },
1338
+ feedback: { ...DEFAULT_FEEDBACK },
1300
1339
  prResolver: { ...DEFAULT_PR_RESOLVER },
1301
1340
  merger: { ...DEFAULT_MERGER },
1302
1341
  jobProviders: { ...DEFAULT_JOB_PROVIDERS },
@@ -1495,7 +1534,7 @@ function mergeConfigLayer(base, layer) {
1495
1534
  })
1496
1535
  }
1497
1536
  };
1498
- } else if (_key === "providerEnv" || _key === "boardProvider" || _key === "qa" || _key === "audit" || _key === "analytics" || _key === "prResolver" || _key === "merger") {
1537
+ } else if (_key === "providerEnv" || _key === "boardProvider" || _key === "qa" || _key === "audit" || _key === "analytics" || _key === "feedback" || _key === "prResolver" || _key === "merger") {
1499
1538
  base[_key] = {
1500
1539
  ...base[_key],
1501
1540
  ...value
@@ -1529,6 +1568,13 @@ function mergeConfigs(base, fileConfig, envConfig) {
1529
1568
  merged.reviewerMaxRetries = sanitizeReviewerMaxRetries(merged.reviewerMaxRetries, DEFAULT_REVIEWER_MAX_RETRIES);
1530
1569
  merged.reviewerRetryDelay = sanitizeReviewerRetryDelay(merged.reviewerRetryDelay, DEFAULT_REVIEWER_RETRY_DELAY);
1531
1570
  merged.reviewerMaxPrsPerRun = sanitizeReviewerMaxPrsPerRun(merged.reviewerMaxPrsPerRun, DEFAULT_REVIEWER_MAX_PRS_PER_RUN);
1571
+ merged.feedback = {
1572
+ enabled: merged.feedback.enabled !== false,
1573
+ confidenceThreshold: Math.max(0, Math.min(1, merged.feedback.confidenceThreshold)),
1574
+ augmentationTtlDays: Math.max(1, Math.min(365, Math.floor(merged.feedback.augmentationTtlDays))),
1575
+ maxActiveAugmentations: Math.max(0, Math.min(10, Math.floor(merged.feedback.maxActiveAugmentations))),
1576
+ successStreakToExpire: Math.max(0, Math.min(20, Math.floor(merged.feedback.successStreakToExpire)))
1577
+ };
1532
1578
  if (merged.secondaryFallbackModel === void 0) {
1533
1579
  merged.secondaryFallbackModel = merged.primaryFallbackModel === void 0 ? DEFAULT_SECONDARY_FALLBACK_MODEL : merged.primaryFallbackModel;
1534
1580
  }
@@ -2053,339 +2099,337 @@ var init_roadmap_state_repository = __esm({
2053
2099
  }
2054
2100
  });
2055
2101
 
2056
- // ../core/src/board/types.ts
2057
- var BOARD_COLUMNS2;
2058
- var init_types3 = __esm({
2059
- "../core/src/board/types.ts"() {
2060
- "use strict";
2061
- BOARD_COLUMNS2 = ["Draft", "Ready", "In Progress", "Review", "Done"];
2102
+ // ../core/dist/storage/repositories/sqlite/session-outcome.repository.js
2103
+ import Database6 from "better-sqlite3";
2104
+ import { inject as inject6, injectable as injectable6 } from "tsyringe";
2105
+ function redactText(value) {
2106
+ return SECRET_TEXT_PATTERNS.reduce((current, [pattern, replacement]) => current.replace(pattern, replacement), value);
2107
+ }
2108
+ function redactOptionalText(value) {
2109
+ return value == null ? null : redactText(value);
2110
+ }
2111
+ function redactMetadataValue(value, key, seen) {
2112
+ if (key && SECRET_KEY_PATTERN.test(key)) {
2113
+ return SECRET_PLACEHOLDER;
2062
2114
  }
2063
- });
2064
-
2065
- // ../core/src/jobs/job-registry.ts
2066
- function getValidJobTypes2() {
2067
- return JOB_REGISTRY2.map((job) => job.id);
2115
+ if (typeof value === "string") {
2116
+ return redactText(value);
2117
+ }
2118
+ if (value === null || typeof value !== "object") {
2119
+ return value;
2120
+ }
2121
+ if (seen.has(value)) {
2122
+ return "[Circular]";
2123
+ }
2124
+ seen.add(value);
2125
+ if (Array.isArray(value)) {
2126
+ const redactedArray = value.map((item) => redactMetadataValue(item, void 0, seen));
2127
+ seen.delete(value);
2128
+ return redactedArray;
2129
+ }
2130
+ const redacted = {};
2131
+ for (const [entryKey, entryValue] of Object.entries(value)) {
2132
+ redacted[entryKey] = redactMetadataValue(entryValue, entryKey, seen);
2133
+ }
2134
+ seen.delete(value);
2135
+ return redacted;
2068
2136
  }
2069
- function getDefaultQueuePriority2() {
2070
- const result = {};
2071
- for (const job of JOB_REGISTRY2) {
2072
- result[job.id] = job.queuePriority;
2137
+ function redactMetadata(metadata) {
2138
+ const value = redactMetadataValue(metadata ?? {}, void 0, /* @__PURE__ */ new WeakSet());
2139
+ if (typeof value === "object" && value !== null && !Array.isArray(value)) {
2140
+ return value;
2073
2141
  }
2074
- return result;
2142
+ return {};
2075
2143
  }
2076
- function getLogFileNames2() {
2077
- const result = {};
2078
- for (const job of JOB_REGISTRY2) {
2079
- result[job.id] = job.logName;
2080
- if (job.cliCommand !== job.id) {
2081
- result[job.cliCommand] = job.logName;
2144
+ function parseMetadata(metadataJson) {
2145
+ try {
2146
+ const parsed = JSON.parse(metadataJson);
2147
+ if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
2148
+ return parsed;
2082
2149
  }
2150
+ } catch {
2151
+ return {};
2083
2152
  }
2084
- return result;
2153
+ return {};
2154
+ }
2155
+ function rowToOutcome(row) {
2156
+ return {
2157
+ id: row.id,
2158
+ projectPath: row.project_path,
2159
+ jobType: row.job_type,
2160
+ providerKey: row.provider_key,
2161
+ prdFile: row.prd_file,
2162
+ prNumber: row.pr_number,
2163
+ branchName: row.branch_name,
2164
+ startedAt: row.started_at,
2165
+ finishedAt: row.finished_at,
2166
+ durationSeconds: row.duration_seconds,
2167
+ outcome: row.outcome,
2168
+ exitCode: row.exit_code,
2169
+ attempt: row.attempt,
2170
+ retryCount: row.retry_count,
2171
+ reviewScore: row.review_score,
2172
+ ciStatus: row.ci_status,
2173
+ failureCategory: row.failure_category,
2174
+ failureSignature: row.failure_signature,
2175
+ metadata: parseMetadata(row.metadata_json)
2176
+ };
2177
+ }
2178
+ function rowToPattern(row) {
2179
+ return {
2180
+ id: row.id,
2181
+ projectPath: row.project_path,
2182
+ patternKey: row.pattern_key,
2183
+ jobType: row.job_type,
2184
+ category: row.category,
2185
+ title: row.title,
2186
+ description: row.description,
2187
+ sampleCount: row.sample_count,
2188
+ confidence: row.confidence,
2189
+ firstSeenAt: row.first_seen_at,
2190
+ lastSeenAt: row.last_seen_at,
2191
+ status: row.status,
2192
+ metadata: parseMetadata(row.metadata_json)
2193
+ };
2194
+ }
2195
+ function rowToAugmentation(row) {
2196
+ return {
2197
+ id: row.id,
2198
+ projectPath: row.project_path,
2199
+ patternId: row.pattern_id,
2200
+ jobType: row.job_type,
2201
+ promptText: row.prompt_text,
2202
+ status: row.status,
2203
+ createdAt: row.created_at,
2204
+ updatedAt: row.updated_at,
2205
+ expiresAt: row.expires_at,
2206
+ appliedCount: row.applied_count,
2207
+ successCount: row.success_count
2208
+ };
2209
+ }
2210
+ function buildOutcomeWhere(input) {
2211
+ const clauses = ["project_path = ?"];
2212
+ const params = [input.projectPath];
2213
+ if (input.jobType) {
2214
+ clauses.push("job_type = ?");
2215
+ params.push(input.jobType);
2216
+ }
2217
+ if ("outcome" in input && input.outcome) {
2218
+ clauses.push("outcome = ?");
2219
+ params.push(input.outcome);
2220
+ }
2221
+ if (input.fromFinishedAt != null) {
2222
+ clauses.push("finished_at >= ?");
2223
+ params.push(input.fromFinishedAt);
2224
+ }
2225
+ if (input.toFinishedAt != null) {
2226
+ clauses.push("finished_at <= ?");
2227
+ params.push(input.toFinishedAt);
2228
+ }
2229
+ return { params, where: clauses.join(" AND ") };
2085
2230
  }
2086
- var JOB_REGISTRY2, JOB_MAP2;
2087
- var init_job_registry2 = __esm({
2088
- "../core/src/jobs/job-registry.ts"() {
2231
+ var __decorate6, __metadata6, __param6, SECRET_PLACEHOLDER, SECRET_KEY_PATTERN, SECRET_TEXT_PATTERNS, SqliteSessionOutcomeRepository;
2232
+ var init_session_outcome_repository = __esm({
2233
+ "../core/dist/storage/repositories/sqlite/session-outcome.repository.js"() {
2089
2234
  "use strict";
2090
- init_types3();
2091
- JOB_REGISTRY2 = [
2092
- {
2093
- id: "executor",
2094
- name: "Executor",
2095
- description: "Creates implementation PRs from PRDs",
2096
- cliCommand: "run",
2097
- logName: "executor",
2098
- lockSuffix: ".lock",
2099
- queuePriority: 50,
2100
- envPrefix: "NW_EXECUTOR",
2101
- defaultConfig: {
2102
- enabled: true,
2103
- schedule: "5 */2 * * *",
2104
- maxRuntime: 7200
2105
- }
2106
- },
2107
- {
2108
- id: "reviewer",
2109
- name: "Reviewer",
2110
- description: "Reviews and improves PRs on night-watch branches",
2111
- cliCommand: "review",
2112
- logName: "reviewer",
2113
- lockSuffix: "-r.lock",
2114
- queuePriority: 40,
2115
- envPrefix: "NW_REVIEWER",
2116
- defaultConfig: {
2117
- enabled: true,
2118
- schedule: "25 */3 * * *",
2119
- maxRuntime: 3600
2120
- }
2121
- },
2122
- {
2123
- id: "pr-resolver",
2124
- name: "PR Conflict Solver",
2125
- description: "Resolves merge conflicts via AI rebase; optionally addresses review comments and labels PRs ready-to-merge",
2126
- cliCommand: "resolve",
2127
- logName: "pr-resolver",
2128
- lockSuffix: "-pr-resolver.lock",
2129
- queuePriority: 35,
2130
- envPrefix: "NW_PR_RESOLVER",
2131
- extraFields: [
2132
- { name: "branchPatterns", type: "string[]", defaultValue: [] },
2133
- { name: "maxPrsPerRun", type: "number", defaultValue: 0 },
2134
- { name: "perPrTimeout", type: "number", defaultValue: 600 },
2135
- { name: "aiConflictResolution", type: "boolean", defaultValue: true },
2136
- { name: "aiReviewResolution", type: "boolean", defaultValue: false },
2137
- { name: "readyLabel", type: "string", defaultValue: "ready-to-merge" }
2138
- ],
2139
- defaultConfig: {
2140
- enabled: true,
2141
- schedule: "15 6,14,22 * * *",
2142
- maxRuntime: 3600,
2143
- branchPatterns: [],
2144
- maxPrsPerRun: 0,
2145
- perPrTimeout: 600,
2146
- aiConflictResolution: true,
2147
- aiReviewResolution: false,
2148
- readyLabel: "ready-to-merge"
2149
- }
2150
- },
2151
- {
2152
- id: "slicer",
2153
- name: "Slicer",
2154
- description: "Generates PRDs from roadmap items",
2155
- cliCommand: "planner",
2156
- logName: "slicer",
2157
- lockSuffix: "-slicer.lock",
2158
- queuePriority: 30,
2159
- envPrefix: "NW_SLICER",
2160
- defaultConfig: {
2161
- enabled: true,
2162
- schedule: "35 */6 * * *",
2163
- maxRuntime: 600
2164
- }
2165
- },
2166
- {
2167
- id: "qa",
2168
- name: "QA",
2169
- description: "Runs end-to-end tests on PRs",
2170
- cliCommand: "qa",
2171
- logName: "night-watch-qa",
2172
- lockSuffix: "-qa.lock",
2173
- queuePriority: 20,
2174
- envPrefix: "NW_QA",
2175
- extraFields: [
2176
- { name: "branchPatterns", type: "string[]", defaultValue: [] },
2177
- {
2178
- name: "artifacts",
2179
- type: "enum",
2180
- enumValues: ["screenshot", "video", "both"],
2181
- defaultValue: "both"
2182
- },
2183
- { name: "skipLabel", type: "string", defaultValue: "skip-qa" },
2184
- { name: "autoInstallPlaywright", type: "boolean", defaultValue: true },
2185
- { name: "validatedLabel", type: "string", defaultValue: "e2e-validated" }
2186
- ],
2187
- defaultConfig: {
2188
- enabled: true,
2189
- schedule: "45 2,10,18 * * *",
2190
- maxRuntime: 3600,
2191
- branchPatterns: [],
2192
- artifacts: "both",
2193
- skipLabel: "skip-qa",
2194
- autoInstallPlaywright: true,
2195
- validatedLabel: "e2e-validated"
2196
- }
2197
- },
2198
- {
2199
- id: "audit",
2200
- name: "Auditor",
2201
- description: "Performs code audits and creates issues for findings",
2202
- cliCommand: "audit",
2203
- logName: "audit",
2204
- lockSuffix: "-audit.lock",
2205
- queuePriority: 10,
2206
- envPrefix: "NW_AUDIT",
2207
- extraFields: [
2208
- {
2209
- name: "targetColumn",
2210
- type: "enum",
2211
- enumValues: [...BOARD_COLUMNS2],
2212
- defaultValue: "Draft"
2213
- }
2214
- ],
2215
- defaultConfig: {
2216
- enabled: true,
2217
- schedule: "50 3 * * 1",
2218
- maxRuntime: 1800,
2219
- targetColumn: "Draft"
2235
+ __decorate6 = function(decorators, target, key, desc) {
2236
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
2237
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
2238
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
2239
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
2240
+ };
2241
+ __metadata6 = function(k, v) {
2242
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
2243
+ };
2244
+ __param6 = function(paramIndex, decorator) {
2245
+ return function(target, key) {
2246
+ decorator(target, key, paramIndex);
2247
+ };
2248
+ };
2249
+ SECRET_PLACEHOLDER = "[REDACTED_SECRET]";
2250
+ SECRET_KEY_PATTERN = /(?:api[_-]?key|authorization|client[_-]?secret|cookie|password|private[_-]?key|secret|token)/i;
2251
+ SECRET_TEXT_PATTERNS = [
2252
+ [
2253
+ /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g,
2254
+ SECRET_PLACEHOLDER
2255
+ ],
2256
+ [/\bsk-ant-[\w-]{20,}\b/g, SECRET_PLACEHOLDER],
2257
+ [/\bsk-[\w-]{20,}\b/g, SECRET_PLACEHOLDER],
2258
+ [/\bgh[opsru]_\w{30,}\b/g, SECRET_PLACEHOLDER],
2259
+ [/\bxox[baprs]-[\w-]{20,}\b/g, SECRET_PLACEHOLDER],
2260
+ [/\b(?:AKIA|ASIA)[A-Z0-9]{16}\b/g, SECRET_PLACEHOLDER],
2261
+ [/\b(Bearer|Basic)\s+[\w.~+/=-]{12,}/gi, `$1 ${SECRET_PLACEHOLDER}`]
2262
+ ];
2263
+ SqliteSessionOutcomeRepository = class SqliteSessionOutcomeRepository2 {
2264
+ db;
2265
+ constructor(db) {
2266
+ this.db = db;
2267
+ }
2268
+ insertOutcome(input) {
2269
+ const metadataJson = JSON.stringify(redactMetadata(input.metadata));
2270
+ const result = this.db.prepare(`INSERT INTO session_outcomes
2271
+ (project_path, job_type, provider_key, prd_file, pr_number, branch_name,
2272
+ started_at, finished_at, duration_seconds, outcome, exit_code, attempt,
2273
+ retry_count, review_score, ci_status, failure_category, failure_signature,
2274
+ metadata_json)
2275
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(input.projectPath, input.jobType, input.providerKey, input.prdFile ?? null, input.prNumber ?? null, redactOptionalText(input.branchName), input.startedAt, input.finishedAt, input.durationSeconds ?? null, input.outcome, input.exitCode ?? null, input.attempt ?? 1, input.retryCount ?? 0, input.reviewScore ?? null, redactOptionalText(input.ciStatus), redactOptionalText(input.failureCategory), redactOptionalText(input.failureSignature), metadataJson);
2276
+ return this.getOutcomeById(Number(result.lastInsertRowid));
2277
+ }
2278
+ queryOutcomes(input) {
2279
+ const { params, where } = buildOutcomeWhere(input);
2280
+ const limit = Math.min(Math.max(input.limit ?? 100, 1), 500);
2281
+ const rows = this.db.prepare(`SELECT *
2282
+ FROM session_outcomes
2283
+ WHERE ${where}
2284
+ ORDER BY finished_at DESC, id DESC
2285
+ LIMIT ?`).all(...params, limit);
2286
+ return rows.map(rowToOutcome);
2287
+ }
2288
+ querySummary(input) {
2289
+ const { params, where } = buildOutcomeWhere(input);
2290
+ const outcomeRows = this.db.prepare(`SELECT outcome as key, COUNT(*) as count
2291
+ FROM session_outcomes
2292
+ WHERE ${where}
2293
+ GROUP BY outcome`).all(...params);
2294
+ const categoryRows = this.db.prepare(`SELECT failure_category as key, COUNT(*) as count
2295
+ FROM session_outcomes
2296
+ WHERE ${where} AND failure_category IS NOT NULL
2297
+ GROUP BY failure_category`).all(...params);
2298
+ const averageRow = this.db.prepare(`SELECT AVG(duration_seconds) as average_duration
2299
+ FROM session_outcomes
2300
+ WHERE ${where} AND duration_seconds IS NOT NULL`).get(...params);
2301
+ const byOutcome = Object.fromEntries(outcomeRows.map((row) => [row.key ?? "unknown", row.count]));
2302
+ const byFailureCategory = Object.fromEntries(categoryRows.map((row) => [row.key ?? "unknown", row.count]));
2303
+ return {
2304
+ totalCount: outcomeRows.reduce((total, row) => total + row.count, 0),
2305
+ successCount: byOutcome.success ?? 0,
2306
+ failureCount: byOutcome.failure ?? 0,
2307
+ timeoutCount: byOutcome.timeout ?? 0,
2308
+ rateLimitedCount: byOutcome.rate_limited ?? 0,
2309
+ skippedCount: byOutcome.skipped ?? 0,
2310
+ averageDurationSeconds: averageRow?.average_duration ?? null,
2311
+ byOutcome,
2312
+ byFailureCategory
2313
+ };
2314
+ }
2315
+ upsertPattern(input) {
2316
+ const now = Date.now();
2317
+ const existing = this.getPattern(input.projectPath, input.patternKey, input.jobType);
2318
+ const firstSeenAt = existing?.firstSeenAt ?? input.firstSeenAt ?? now;
2319
+ const lastSeenAt = input.lastSeenAt ?? now;
2320
+ const sampleCount = input.sampleCount ?? (existing ? existing.sampleCount + 1 : 1);
2321
+ const confidence = input.confidence ?? existing?.confidence ?? 0;
2322
+ const status = input.status ?? existing?.status ?? "observing";
2323
+ const metadataJson = JSON.stringify(redactMetadata(input.metadata ?? existing?.metadata));
2324
+ this.db.prepare(`INSERT INTO feedback_patterns
2325
+ (project_path, pattern_key, job_type, category, title, description, sample_count,
2326
+ confidence, first_seen_at, last_seen_at, status, metadata_json)
2327
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
2328
+ ON CONFLICT(project_path, pattern_key, job_type)
2329
+ DO UPDATE SET category = excluded.category,
2330
+ title = excluded.title,
2331
+ description = excluded.description,
2332
+ sample_count = excluded.sample_count,
2333
+ confidence = excluded.confidence,
2334
+ last_seen_at = excluded.last_seen_at,
2335
+ status = excluded.status,
2336
+ metadata_json = excluded.metadata_json`).run(input.projectPath, input.patternKey, input.jobType, redactText(input.category), redactText(input.title), redactText(input.description), sampleCount, confidence, firstSeenAt, lastSeenAt, status, metadataJson);
2337
+ return this.getPattern(input.projectPath, input.patternKey, input.jobType);
2338
+ }
2339
+ listPatterns(input) {
2340
+ const clauses = ["project_path = ?"];
2341
+ const params = [input.projectPath];
2342
+ if (input.jobType) {
2343
+ clauses.push("job_type = ?");
2344
+ params.push(input.jobType);
2220
2345
  }
2221
- },
2222
- {
2223
- id: "analytics",
2224
- name: "Analytics",
2225
- description: "Analyzes product analytics and creates issues for trends",
2226
- cliCommand: "analytics",
2227
- logName: "analytics",
2228
- lockSuffix: "-analytics.lock",
2229
- queuePriority: 10,
2230
- envPrefix: "NW_ANALYTICS",
2231
- extraFields: [
2232
- { name: "lookbackDays", type: "number", defaultValue: 7 },
2233
- {
2234
- name: "targetColumn",
2235
- type: "enum",
2236
- enumValues: [...BOARD_COLUMNS2],
2237
- defaultValue: "Draft"
2238
- },
2239
- { name: "analysisPrompt", type: "string", defaultValue: "" }
2240
- ],
2241
- defaultConfig: {
2242
- enabled: false,
2243
- schedule: "0 6 * * 1",
2244
- maxRuntime: 900,
2245
- lookbackDays: 7,
2246
- targetColumn: "Draft",
2247
- analysisPrompt: ""
2346
+ if (input.status) {
2347
+ clauses.push("status = ?");
2348
+ params.push(input.status);
2248
2349
  }
2249
- },
2250
- {
2251
- id: "merger",
2252
- name: "Merge Orchestrator",
2253
- description: "Repo-wide PR merge coordinator \u2014 scans, rebases, and merges in FIFO order",
2254
- cliCommand: "merge",
2255
- logName: "merger",
2256
- lockSuffix: "-merger.lock",
2257
- queuePriority: 45,
2258
- envPrefix: "NW_MERGER",
2259
- extraFields: [
2260
- {
2261
- name: "mergeMethod",
2262
- type: "enum",
2263
- enumValues: ["squash", "merge", "rebase"],
2264
- defaultValue: "squash"
2265
- },
2266
- { name: "minReviewScore", type: "number", defaultValue: 80 },
2267
- { name: "branchPatterns", type: "string[]", defaultValue: [] },
2268
- { name: "rebaseBeforeMerge", type: "boolean", defaultValue: true },
2269
- { name: "maxPrsPerRun", type: "number", defaultValue: 0 }
2270
- ],
2271
- defaultConfig: {
2272
- enabled: false,
2273
- schedule: "55 */4 * * *",
2274
- maxRuntime: 1800,
2275
- mergeMethod: "squash",
2276
- minReviewScore: 80,
2277
- branchPatterns: [],
2278
- rebaseBeforeMerge: true,
2279
- maxPrsPerRun: 0
2350
+ const limit = Math.min(Math.max(input.limit ?? 25, 1), 100);
2351
+ const rows = this.db.prepare(`SELECT *
2352
+ FROM feedback_patterns
2353
+ WHERE ${clauses.join(" AND ")}
2354
+ ORDER BY sample_count DESC, confidence DESC, last_seen_at DESC, id DESC
2355
+ LIMIT ?`).all(...params, limit);
2356
+ return rows.map(rowToPattern);
2357
+ }
2358
+ createAugmentation(input) {
2359
+ const now = Date.now();
2360
+ const createdAt = input.createdAt ?? now;
2361
+ const updatedAt = input.updatedAt ?? createdAt;
2362
+ const result = this.db.prepare(`INSERT INTO prompt_augmentations
2363
+ (project_path, pattern_id, job_type, prompt_text, status, created_at, updated_at, expires_at)
2364
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`).run(input.projectPath, input.patternId ?? null, input.jobType, redactText(input.promptText), input.status ?? "active", createdAt, updatedAt, input.expiresAt ?? null);
2365
+ return this.getAugmentationById(Number(result.lastInsertRowid));
2366
+ }
2367
+ listAugmentations(input) {
2368
+ const clauses = ["project_path = ?"];
2369
+ const params = [input.projectPath];
2370
+ if (input.jobType) {
2371
+ clauses.push("job_type = ?");
2372
+ params.push(input.jobType);
2280
2373
  }
2281
- }
2282
- ];
2283
- JOB_MAP2 = new Map(JOB_REGISTRY2.map((job) => [job.id, job]));
2284
- }
2285
- });
2286
-
2287
- // ../core/src/constants.ts
2288
- var DEFAULT_LOCAL_BOARD_INFO2, VALID_JOB_TYPES2, DEFAULT_WEBHOOK_TRIGGER_SECRET_ENV2, DEFAULT_WEBHOOK_TRIGGER_MAX_SKEW_SECONDS2, DEFAULT_WEBHOOK_TRIGGERS2, BUILT_IN_PRESETS2, BUILT_IN_PRESET_IDS2, PROVIDER_COMMANDS2, LOG_FILE_NAMES2, GLOBAL_CONFIG_DIR2, STATE_DB_FILE_NAME2, DEFAULT_QUEUE_ENABLED2, DEFAULT_QUEUE_MODE2, DEFAULT_QUEUE_MAX_CONCURRENCY2, DEFAULT_QUEUE_MAX_WAIT_TIME2, DEFAULT_QUEUE_PRIORITY2, DEFAULT_QUEUE2;
2289
- var init_constants2 = __esm({
2290
- "../core/src/constants.ts"() {
2291
- "use strict";
2292
- init_job_registry2();
2293
- DEFAULT_LOCAL_BOARD_INFO2 = { id: "local", number: 0, title: "Local Kanban", url: "" };
2294
- VALID_JOB_TYPES2 = getValidJobTypes2();
2295
- DEFAULT_WEBHOOK_TRIGGER_SECRET_ENV2 = "NIGHT_WATCH_WEBHOOK_SECRET";
2296
- DEFAULT_WEBHOOK_TRIGGER_MAX_SKEW_SECONDS2 = 300;
2297
- DEFAULT_WEBHOOK_TRIGGERS2 = {
2298
- enabled: false,
2299
- secretEnv: DEFAULT_WEBHOOK_TRIGGER_SECRET_ENV2,
2300
- allowedJobIds: getValidJobTypes2(),
2301
- requireTimestamp: false,
2302
- maxSkewSeconds: DEFAULT_WEBHOOK_TRIGGER_MAX_SKEW_SECONDS2,
2303
- github: {
2304
- enabled: false,
2305
- events: [],
2306
- rules: []
2307
- }
2308
- };
2309
- BUILT_IN_PRESETS2 = {
2310
- claude: {
2311
- name: "Claude",
2312
- command: "claude",
2313
- promptFlag: "-p",
2314
- autoApproveFlag: "--dangerously-skip-permissions"
2315
- },
2316
- "claude-sonnet-4-6": {
2317
- name: "Claude Sonnet 4.6",
2318
- command: "claude",
2319
- promptFlag: "-p",
2320
- autoApproveFlag: "--dangerously-skip-permissions",
2321
- modelFlag: "--model",
2322
- model: "claude-sonnet-4-6"
2323
- },
2324
- "claude-opus-4-6": {
2325
- name: "Claude Opus 4.6",
2326
- command: "claude",
2327
- promptFlag: "-p",
2328
- autoApproveFlag: "--dangerously-skip-permissions",
2329
- modelFlag: "--model",
2330
- model: "claude-opus-4-6"
2331
- },
2332
- codex: {
2333
- name: "Codex",
2334
- command: "codex",
2335
- subcommand: "exec",
2336
- autoApproveFlag: "--yolo",
2337
- workdirFlag: "-C"
2338
- },
2339
- "glm-47": {
2340
- name: "GLM-4.7",
2341
- command: "claude",
2342
- promptFlag: "-p",
2343
- autoApproveFlag: "--dangerously-skip-permissions",
2344
- modelFlag: "--model",
2345
- model: "glm-4.7",
2346
- envVars: {
2347
- ANTHROPIC_BASE_URL: "https://api.z.ai/api/anthropic",
2348
- API_TIMEOUT_MS: "3000000",
2349
- ANTHROPIC_DEFAULT_OPUS_MODEL: "glm-4.7",
2350
- ANTHROPIC_DEFAULT_SONNET_MODEL: "glm-4.7"
2374
+ if (input.status) {
2375
+ clauses.push("status = ?");
2376
+ params.push(input.status);
2351
2377
  }
2352
- },
2353
- "glm-5": {
2354
- name: "GLM-5",
2355
- command: "claude",
2356
- promptFlag: "-p",
2357
- autoApproveFlag: "--dangerously-skip-permissions",
2358
- modelFlag: "--model",
2359
- model: "glm-5",
2360
- envVars: {
2361
- ANTHROPIC_BASE_URL: "https://api.z.ai/api/anthropic",
2362
- API_TIMEOUT_MS: "3000000",
2363
- ANTHROPIC_DEFAULT_OPUS_MODEL: "glm-5",
2364
- ANTHROPIC_DEFAULT_SONNET_MODEL: "glm-5"
2378
+ if (!input.includeExpired) {
2379
+ clauses.push("(expires_at IS NULL OR expires_at > ?)");
2380
+ params.push(input.now ?? Date.now());
2365
2381
  }
2382
+ const limit = Math.min(Math.max(input.limit ?? 100, 1), 250);
2383
+ const rows = this.db.prepare(`SELECT *
2384
+ FROM prompt_augmentations
2385
+ WHERE ${clauses.join(" AND ")}
2386
+ ORDER BY created_at ASC, id ASC
2387
+ LIMIT ?`).all(...params, limit);
2388
+ return rows.map(rowToAugmentation);
2389
+ }
2390
+ listActiveAugmentations(projectPath, jobType, now = Date.now()) {
2391
+ const rows = this.db.prepare(`SELECT *
2392
+ FROM prompt_augmentations
2393
+ WHERE project_path = ?
2394
+ AND job_type = ?
2395
+ AND status = 'active'
2396
+ AND (expires_at IS NULL OR expires_at > ?)
2397
+ ORDER BY created_at ASC, id ASC`).all(projectPath, jobType, now);
2398
+ return rows.map(rowToAugmentation);
2399
+ }
2400
+ updateAugmentationStatus(id, status, projectPath) {
2401
+ const result = projectPath === void 0 ? this.db.prepare("UPDATE prompt_augmentations SET status = ?, updated_at = ? WHERE id = ?").run(status, Date.now(), id) : this.db.prepare(`UPDATE prompt_augmentations
2402
+ SET status = ?, updated_at = ?
2403
+ WHERE id = ? AND project_path = ?`).run(status, Date.now(), id, projectPath);
2404
+ return result.changes > 0 ? this.getAugmentationById(id) : null;
2405
+ }
2406
+ incrementAugmentationCounts(id, success2 = false) {
2407
+ this.db.prepare(`UPDATE prompt_augmentations
2408
+ SET applied_count = applied_count + 1,
2409
+ success_count = success_count + ?,
2410
+ updated_at = ?
2411
+ WHERE id = ?`).run(success2 ? 1 : 0, Date.now(), id);
2412
+ }
2413
+ getOutcomeById(id) {
2414
+ const row = this.db.prepare("SELECT * FROM session_outcomes WHERE id = ?").get(id);
2415
+ return row ? rowToOutcome(row) : null;
2416
+ }
2417
+ getPattern(projectPath, patternKey, jobType) {
2418
+ const row = this.db.prepare(`SELECT *
2419
+ FROM feedback_patterns
2420
+ WHERE project_path = ? AND pattern_key = ? AND job_type = ?`).get(projectPath, patternKey, jobType);
2421
+ return row ? rowToPattern(row) : null;
2422
+ }
2423
+ getAugmentationById(id) {
2424
+ const row = this.db.prepare("SELECT * FROM prompt_augmentations WHERE id = ?").get(id);
2425
+ return row ? rowToAugmentation(row) : null;
2366
2426
  }
2367
2427
  };
2368
- BUILT_IN_PRESET_IDS2 = Object.keys(BUILT_IN_PRESETS2);
2369
- PROVIDER_COMMANDS2 = {
2370
- claude: BUILT_IN_PRESETS2.claude.command,
2371
- codex: BUILT_IN_PRESETS2.codex.command
2372
- };
2373
- LOG_FILE_NAMES2 = getLogFileNames2();
2374
- GLOBAL_CONFIG_DIR2 = ".night-watch";
2375
- STATE_DB_FILE_NAME2 = "state.db";
2376
- DEFAULT_QUEUE_ENABLED2 = true;
2377
- DEFAULT_QUEUE_MODE2 = "auto";
2378
- DEFAULT_QUEUE_MAX_CONCURRENCY2 = 3;
2379
- DEFAULT_QUEUE_MAX_WAIT_TIME2 = 7200;
2380
- DEFAULT_QUEUE_PRIORITY2 = getDefaultQueuePriority2();
2381
- DEFAULT_QUEUE2 = {
2382
- enabled: DEFAULT_QUEUE_ENABLED2,
2383
- mode: DEFAULT_QUEUE_MODE2,
2384
- maxConcurrency: DEFAULT_QUEUE_MAX_CONCURRENCY2,
2385
- maxWaitTime: DEFAULT_QUEUE_MAX_WAIT_TIME2,
2386
- priority: { ...DEFAULT_QUEUE_PRIORITY2 },
2387
- providerBuckets: {}
2388
- };
2428
+ SqliteSessionOutcomeRepository = __decorate6([
2429
+ injectable6(),
2430
+ __param6(0, inject6("Database")),
2431
+ __metadata6("design:paramtypes", [Object])
2432
+ ], SqliteSessionOutcomeRepository);
2389
2433
  }
2390
2434
  });
2391
2435
 
@@ -2393,10 +2437,10 @@ var init_constants2 = __esm({
2393
2437
  import * as fs2 from "fs";
2394
2438
  import * as os from "os";
2395
2439
  import * as path2 from "path";
2396
- import Database6 from "better-sqlite3";
2440
+ import Database7 from "better-sqlite3";
2397
2441
  function getDbPath() {
2398
- const base = process.env.NIGHT_WATCH_HOME || path2.join(os.homedir(), GLOBAL_CONFIG_DIR2);
2399
- return path2.join(base, STATE_DB_FILE_NAME2);
2442
+ const base = process.env.NIGHT_WATCH_HOME || path2.join(os.homedir(), GLOBAL_CONFIG_DIR);
2443
+ return path2.join(base, STATE_DB_FILE_NAME);
2400
2444
  }
2401
2445
  function getDb() {
2402
2446
  if (_db) {
@@ -2404,7 +2448,7 @@ function getDb() {
2404
2448
  }
2405
2449
  const dbPath = getDbPath();
2406
2450
  fs2.mkdirSync(path2.dirname(dbPath), { recursive: true });
2407
- const db = new Database6(dbPath);
2451
+ const db = new Database7(dbPath);
2408
2452
  db.pragma("journal_mode = WAL");
2409
2453
  db.pragma("busy_timeout = 5000");
2410
2454
  _db = db;
@@ -2418,8 +2462,8 @@ function closeDb() {
2418
2462
  }
2419
2463
  function createDbForDir(projectDir) {
2420
2464
  fs2.mkdirSync(projectDir, { recursive: true });
2421
- const dbPath = path2.join(projectDir, STATE_DB_FILE_NAME2);
2422
- const db = new Database6(dbPath);
2465
+ const dbPath = path2.join(projectDir, STATE_DB_FILE_NAME);
2466
+ const db = new Database7(dbPath);
2423
2467
  db.pragma("journal_mode = WAL");
2424
2468
  db.pragma("busy_timeout = 5000");
2425
2469
  return db;
@@ -2428,7 +2472,7 @@ var _db;
2428
2472
  var init_client = __esm({
2429
2473
  "../core/dist/storage/sqlite/client.js"() {
2430
2474
  "use strict";
2431
- init_constants2();
2475
+ init_constants();
2432
2476
  _db = null;
2433
2477
  }
2434
2478
  });
@@ -2545,6 +2589,65 @@ function runMigrations(db) {
2545
2589
  );
2546
2590
  CREATE INDEX IF NOT EXISTS idx_job_runs_lookup
2547
2591
  ON job_runs(project_path, started_at DESC, job_type, provider_key);
2592
+
2593
+ CREATE TABLE IF NOT EXISTS session_outcomes (
2594
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
2595
+ project_path TEXT NOT NULL,
2596
+ job_type TEXT NOT NULL,
2597
+ provider_key TEXT NOT NULL,
2598
+ prd_file TEXT,
2599
+ pr_number INTEGER,
2600
+ branch_name TEXT,
2601
+ started_at INTEGER NOT NULL,
2602
+ finished_at INTEGER NOT NULL,
2603
+ duration_seconds INTEGER,
2604
+ outcome TEXT NOT NULL,
2605
+ exit_code INTEGER,
2606
+ attempt INTEGER NOT NULL DEFAULT 1,
2607
+ retry_count INTEGER NOT NULL DEFAULT 0,
2608
+ review_score INTEGER,
2609
+ ci_status TEXT,
2610
+ failure_category TEXT,
2611
+ failure_signature TEXT,
2612
+ metadata_json TEXT NOT NULL DEFAULT '{}'
2613
+ );
2614
+ CREATE INDEX IF NOT EXISTS idx_session_outcomes_lookup
2615
+ ON session_outcomes(project_path, finished_at DESC, job_type, outcome);
2616
+
2617
+ CREATE TABLE IF NOT EXISTS feedback_patterns (
2618
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
2619
+ project_path TEXT NOT NULL,
2620
+ pattern_key TEXT NOT NULL,
2621
+ job_type TEXT NOT NULL,
2622
+ category TEXT NOT NULL,
2623
+ title TEXT NOT NULL,
2624
+ description TEXT NOT NULL,
2625
+ sample_count INTEGER NOT NULL DEFAULT 0,
2626
+ confidence REAL NOT NULL DEFAULT 0,
2627
+ first_seen_at INTEGER NOT NULL,
2628
+ last_seen_at INTEGER NOT NULL,
2629
+ status TEXT NOT NULL DEFAULT 'observing',
2630
+ metadata_json TEXT NOT NULL DEFAULT '{}',
2631
+ UNIQUE(project_path, pattern_key, job_type)
2632
+ );
2633
+ CREATE INDEX IF NOT EXISTS idx_feedback_patterns_lookup
2634
+ ON feedback_patterns(project_path, job_type, status, confidence DESC);
2635
+
2636
+ CREATE TABLE IF NOT EXISTS prompt_augmentations (
2637
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
2638
+ project_path TEXT NOT NULL,
2639
+ pattern_id INTEGER REFERENCES feedback_patterns(id),
2640
+ job_type TEXT NOT NULL,
2641
+ prompt_text TEXT NOT NULL,
2642
+ status TEXT NOT NULL DEFAULT 'active',
2643
+ created_at INTEGER NOT NULL,
2644
+ updated_at INTEGER NOT NULL,
2645
+ expires_at INTEGER,
2646
+ applied_count INTEGER NOT NULL DEFAULT 0,
2647
+ success_count INTEGER NOT NULL DEFAULT 0
2648
+ );
2649
+ CREATE INDEX IF NOT EXISTS idx_prompt_augmentations_active
2650
+ ON prompt_augmentations(project_path, job_type, status, expires_at);
2548
2651
  `);
2549
2652
  db.exec(`DROP TABLE IF EXISTS slack_discussions`);
2550
2653
  try {
@@ -2603,6 +2706,7 @@ function initContainer(projectDir) {
2603
2706
  container.registerSingleton(SqlitePrdStateRepository);
2604
2707
  container.registerSingleton(SqliteProjectRegistryRepository);
2605
2708
  container.registerSingleton(SqliteRoadmapStateRepository);
2709
+ container.registerSingleton(SqliteSessionOutcomeRepository);
2606
2710
  }
2607
2711
  function isContainerInitialized() {
2608
2712
  return container.isRegistered(DATABASE_TOKEN);
@@ -2616,6 +2720,7 @@ var init_container = __esm({
2616
2720
  init_prd_state_repository();
2617
2721
  init_project_registry_repository();
2618
2722
  init_roadmap_state_repository();
2723
+ init_session_outcome_repository();
2619
2724
  init_client();
2620
2725
  init_migrations();
2621
2726
  DATABASE_TOKEN = "Database";
@@ -3390,21 +3495,21 @@ var LocalKanbanProvider;
3390
3495
  var init_local_kanban = __esm({
3391
3496
  "../core/dist/board/providers/local-kanban.js"() {
3392
3497
  "use strict";
3393
- init_types3();
3394
- init_constants2();
3498
+ init_types2();
3499
+ init_constants();
3395
3500
  LocalKanbanProvider = class {
3396
3501
  repo;
3397
3502
  constructor(repo) {
3398
3503
  this.repo = repo;
3399
3504
  }
3400
3505
  async setupBoard(title) {
3401
- return { ...DEFAULT_LOCAL_BOARD_INFO2, title };
3506
+ return { ...DEFAULT_LOCAL_BOARD_INFO, title };
3402
3507
  }
3403
3508
  async getBoard() {
3404
- return DEFAULT_LOCAL_BOARD_INFO2;
3509
+ return DEFAULT_LOCAL_BOARD_INFO;
3405
3510
  }
3406
3511
  async getColumns() {
3407
- return BOARD_COLUMNS2.map((name, i) => ({ id: String(i), name }));
3512
+ return BOARD_COLUMNS.map((name, i) => ({ id: String(i), name }));
3408
3513
  }
3409
3514
  async createIssue(input) {
3410
3515
  const row = this.repo.create({
@@ -3810,7 +3915,8 @@ function getRepositories() {
3810
3915
  projectRegistry: container.resolve(SqliteProjectRegistryRepository),
3811
3916
  executionHistory: container.resolve(SqliteExecutionHistoryRepository),
3812
3917
  prdState: container.resolve(SqlitePrdStateRepository),
3813
- roadmapState: container.resolve(SqliteRoadmapStateRepository)
3918
+ roadmapState: container.resolve(SqliteRoadmapStateRepository),
3919
+ sessionOutcomes: container.resolve(SqliteSessionOutcomeRepository)
3814
3920
  };
3815
3921
  }
3816
3922
  const db = getDb();
@@ -3822,7 +3928,8 @@ function getRepositories() {
3822
3928
  projectRegistry: new SqliteProjectRegistryRepository(db),
3823
3929
  executionHistory: new SqliteExecutionHistoryRepository(db),
3824
3930
  prdState: new SqlitePrdStateRepository(db),
3825
- roadmapState: new SqliteRoadmapStateRepository(db)
3931
+ roadmapState: new SqliteRoadmapStateRepository(db),
3932
+ sessionOutcomes: new SqliteSessionOutcomeRepository(db)
3826
3933
  };
3827
3934
  }
3828
3935
  function resetRepositories() {
@@ -3839,6 +3946,8 @@ var init_repositories = __esm({
3839
3946
  init_execution_history_repository();
3840
3947
  init_prd_state_repository();
3841
3948
  init_roadmap_state_repository();
3949
+ init_session_outcome_repository();
3950
+ init_session_outcome_repository();
3842
3951
  _initialized = false;
3843
3952
  }
3844
3953
  });
@@ -7476,14 +7585,14 @@ var init_worktree_manager = __esm({
7476
7585
  import * as fs21 from "fs";
7477
7586
  import * as os7 from "os";
7478
7587
  import * as path20 from "path";
7479
- import Database7 from "better-sqlite3";
7588
+ import Database8 from "better-sqlite3";
7480
7589
  function getStateDbPath() {
7481
7590
  const base = process.env.NIGHT_WATCH_HOME || path20.join(os7.homedir(), GLOBAL_CONFIG_DIR);
7482
7591
  return path20.join(base, STATE_DB_FILE_NAME);
7483
7592
  }
7484
7593
  function openDb() {
7485
7594
  const dbPath = getStateDbPath();
7486
- const db = new Database7(dbPath);
7595
+ const db = new Database8(dbPath);
7487
7596
  db.pragma("journal_mode = WAL");
7488
7597
  db.pragma("busy_timeout = 5000");
7489
7598
  if (!_migrationsApplied) {
@@ -8508,32 +8617,671 @@ async function syncAuditFindingsToBoard(config, projectDir) {
8508
8617
  summary: `found ${findings.length} actionable audit finding(s), but failed to create board issue(s)`
8509
8618
  };
8510
8619
  }
8511
- return {
8512
- status: "partial",
8513
- findingsCount: findings.length,
8514
- issuesCreated: created,
8515
- issuesFailed: failed,
8516
- targetColumn,
8517
- summary: `created ${created} of ${findings.length} audit issue(s) in ${targetColumn} (${failed} failed)`
8518
- };
8620
+ return {
8621
+ status: "partial",
8622
+ findingsCount: findings.length,
8623
+ issuesCreated: created,
8624
+ issuesFailed: failed,
8625
+ targetColumn,
8626
+ summary: `created ${created} of ${findings.length} audit issue(s) in ${targetColumn} (${failed} failed)`
8627
+ };
8628
+ }
8629
+ var logger5;
8630
+ var init_board_sync = __esm({
8631
+ "../core/dist/audit/board-sync.js"() {
8632
+ "use strict";
8633
+ init_factory();
8634
+ init_logger();
8635
+ init_report();
8636
+ logger5 = createLogger("audit-sync");
8637
+ }
8638
+ });
8639
+
8640
+ // ../core/dist/audit/index.js
8641
+ var init_audit = __esm({
8642
+ "../core/dist/audit/index.js"() {
8643
+ "use strict";
8644
+ init_report();
8645
+ init_board_sync();
8646
+ }
8647
+ });
8648
+
8649
+ // ../core/dist/feedback/outcome-parser.js
8650
+ function stripAnsi2(value) {
8651
+ return value.replace(ANSI_PATTERN, "");
8652
+ }
8653
+ function redactOutcomeText(value) {
8654
+ return SECRET_TEXT_PATTERNS2.reduce((current, [pattern, replacement]) => current.replace(pattern, replacement), value);
8655
+ }
8656
+ function trimLine(value, maxLength) {
8657
+ return value.length <= maxLength ? value : `${value.slice(0, maxLength - 3)}...`;
8658
+ }
8659
+ function normalizeLine(value, projectPath) {
8660
+ let normalized = stripAnsi2(redactOutcomeText(value)).trim();
8661
+ if (projectPath) {
8662
+ normalized = normalized.replaceAll(projectPath, "<project>");
8663
+ }
8664
+ return trimLine(normalized.replace(/:\d+:\d+/g, ":<line>:<col>").replace(/:\d+\b/g, ":<line>").replace(/\b0x[0-9a-f]+\b/gi, "<hex>").replace(/\b\d{4,}\b/g, "<num>").replace(/\s+/g, " ").toLowerCase(), MAX_ERROR_LINE_LENGTH);
8665
+ }
8666
+ function getOutputLines(stdout, stderr) {
8667
+ return `${stdout ?? ""}
8668
+ ${stderr ?? ""}`.split(/\r?\n/).map((line) => stripAnsi2(redactOutcomeText(line)).trim()).filter((line) => line.length > 0 && !line.startsWith("NIGHT_WATCH_RESULT:"));
8669
+ }
8670
+ function extractFilePath(line, projectPath) {
8671
+ const normalizedLine = line.replaceAll("\\", "/");
8672
+ const normalizedProjectPath = projectPath.replaceAll("\\", "/");
8673
+ if (normalizedProjectPath) {
8674
+ const projectPrefix = `${normalizedProjectPath}/`;
8675
+ const projectIndex = normalizedLine.indexOf(projectPrefix);
8676
+ if (projectIndex >= 0) {
8677
+ const relativeLine = normalizedLine.slice(projectIndex + projectPrefix.length);
8678
+ const relativePath = extractFilePathToken(relativeLine.split(TOKEN_SPLIT_PATTERN));
8679
+ if (relativePath) {
8680
+ return relativePath;
8681
+ }
8682
+ }
8683
+ }
8684
+ return extractFilePathToken(normalizedLine.split(TOKEN_SPLIT_PATTERN));
8685
+ }
8686
+ function extractFilePathToken(tokens) {
8687
+ for (const token of tokens) {
8688
+ const withoutLocation = cleanFilePathToken(token);
8689
+ if (FILE_PATH_PATTERN.test(withoutLocation)) {
8690
+ return withoutLocation;
8691
+ }
8692
+ }
8693
+ return null;
8694
+ }
8695
+ function cleanFilePathToken(token) {
8696
+ let candidate = token;
8697
+ const locationIndex = findLocationIndex(candidate);
8698
+ if (locationIndex >= 0) {
8699
+ candidate = candidate.slice(0, locationIndex);
8700
+ }
8701
+ while (candidate.startsWith("(") || candidate.startsWith("[") || candidate.startsWith("{")) {
8702
+ candidate = candidate.slice(1);
8703
+ }
8704
+ while (candidate.endsWith(")") || candidate.endsWith(",") || candidate.endsWith(";")) {
8705
+ candidate = candidate.slice(0, -1);
8706
+ }
8707
+ return candidate.startsWith("./") ? candidate.slice(2) : candidate;
8708
+ }
8709
+ function findLocationIndex(value) {
8710
+ for (let index = 0; index < value.length - 1; index += 1) {
8711
+ if (value[index] === ":" && value[index + 1] >= "0" && value[index + 1] <= "9") {
8712
+ return index;
8713
+ }
8714
+ }
8715
+ return -1;
8716
+ }
8717
+ function filePathToArea(filePath) {
8718
+ if (!filePath) {
8719
+ return null;
8720
+ }
8721
+ const segments = filePath.split("/").filter(Boolean);
8722
+ if (segments.length <= 1) {
8723
+ return ".";
8724
+ }
8725
+ return segments.slice(0, Math.min(segments.length - 1, 3)).join("/");
8726
+ }
8727
+ function findFirstMatchingLine(lines, category) {
8728
+ const rule = CLASSIFIER_RULES.find((entry) => entry.category === category);
8729
+ if (rule) {
8730
+ const matched = lines.find((line) => rule.pattern.test(line));
8731
+ if (matched) {
8732
+ return matched;
8733
+ }
8734
+ }
8735
+ return lines.find((line) => /\b(error|failed|failure|fatal|exception|conflict|timeout)\b/i.test(line)) ?? lines[0] ?? null;
8736
+ }
8737
+ function classifyCategory(lines, scriptResult, minReviewScore, exitCode) {
8738
+ const status = scriptResult?.status ?? "";
8739
+ const data = scriptResult?.data ?? {};
8740
+ const combined = [...lines, status, data.reason ?? "", data.detail ?? ""].join("\n");
8741
+ if (exitCode === 124 || status === "timeout") {
8742
+ return "timeout";
8743
+ }
8744
+ if (status === "rate_limited" || data.rate_limit_fallback === "1") {
8745
+ return "rate-limit";
8746
+ }
8747
+ const reviewScore = parseOptionalNumber(data.final_score ?? data.review_score);
8748
+ if (reviewScore != null && minReviewScore != null && Number.isFinite(minReviewScore) && reviewScore < minReviewScore) {
8749
+ return "review-score";
8750
+ }
8751
+ for (const rule of CLASSIFIER_RULES) {
8752
+ if (rule.pattern.test(combined)) {
8753
+ return rule.category;
8754
+ }
8755
+ }
8756
+ return "unknown";
8757
+ }
8758
+ function classifyFailure(input) {
8759
+ const lines = getOutputLines(input.stdout, input.stderr);
8760
+ const category = classifyCategory(lines, input.scriptResult, input.minReviewScore, input.exitCode);
8761
+ const firstErrorLine = findFirstMatchingLine(lines, category);
8762
+ const filePath = (firstErrorLine ? extractFilePath(firstErrorLine, input.projectPath) : null) ?? lines.map((line) => extractFilePath(line, input.projectPath)).find((value) => value != null) ?? null;
8763
+ const fileArea = filePathToArea(filePath);
8764
+ const normalizedLine = firstErrorLine ? normalizeLine(firstErrorLine, input.projectPath) : "no-error-line";
8765
+ const failureSignature = trimLine(`${category}|${fileArea ?? "unknown-area"}|${normalizedLine}`, MAX_SIGNATURE_LENGTH);
8766
+ return {
8767
+ category,
8768
+ failureSignature,
8769
+ fileArea,
8770
+ firstErrorLine: firstErrorLine ? trimLine(firstErrorLine, MAX_ERROR_LINE_LENGTH) : null
8771
+ };
8772
+ }
8773
+ function parseOptionalNumber(value) {
8774
+ if (!value) {
8775
+ return null;
8776
+ }
8777
+ const normalized = value.trim().replace(/^#/, "");
8778
+ const parsed = parseInt(normalized, 10);
8779
+ return Number.isNaN(parsed) ? null : parsed;
8780
+ }
8781
+ function parseFirstPrNumber(scriptResult) {
8782
+ const data = scriptResult?.data ?? {};
8783
+ const direct = parseOptionalNumber(data.pr_number) ?? parseOptionalNumber(data.prNumber) ?? parseOptionalNumber(data.pr) ?? parseOptionalNumber(data.failed_pr);
8784
+ if (direct != null) {
8785
+ return direct;
8786
+ }
8787
+ const urlMatch = data.pr_url?.match(/\/pull\/(\d+)/);
8788
+ if (urlMatch?.[1]) {
8789
+ return parseOptionalNumber(urlMatch[1]);
8790
+ }
8791
+ const prsRaw = data.prs ?? data.auto_merged;
8792
+ if (!prsRaw) {
8793
+ return null;
8794
+ }
8795
+ const firstToken = prsRaw.split(",").find((token) => parseOptionalNumber(token) != null);
8796
+ return parseOptionalNumber(firstToken);
8797
+ }
8798
+ function parseAttemptCount(scriptResult, lines) {
8799
+ const fromData = parseOptionalNumber(scriptResult?.data.attempt) ?? parseOptionalNumber(scriptResult?.data.attempts);
8800
+ if (fromData != null && fromData > 0) {
8801
+ return fromData;
8802
+ }
8803
+ let maxAttempt = 1;
8804
+ for (const line of lines) {
8805
+ const match = /\bATTEMPT:\s*(\d+)\//i.exec(line) ?? /\bStarting attempt\s+(\d+)\//i.exec(line);
8806
+ if (match?.[1]) {
8807
+ maxAttempt = Math.max(maxAttempt, parseInt(match[1], 10));
8808
+ }
8809
+ }
8810
+ return maxAttempt;
8811
+ }
8812
+ function parseRetryCount(scriptResult, attempt) {
8813
+ const retryCount = parseOptionalNumber(scriptResult?.data.retry_count);
8814
+ if (retryCount != null && retryCount >= 0) {
8815
+ return retryCount;
8816
+ }
8817
+ return Math.max(0, attempt - 1);
8818
+ }
8819
+ function markerIndicatesFailure(scriptResult) {
8820
+ const data = scriptResult?.data ?? {};
8821
+ const positiveFailureCount = (parseOptionalNumber(data.failed) ?? 0) > 0 || (parseOptionalNumber(data.prs_failed) ?? 0) > 0 || (parseOptionalNumber(data.failed_count) ?? 0) > 0;
8822
+ if (positiveFailureCount) {
8823
+ return true;
8824
+ }
8825
+ return [data.failed_pr, data.auto_merge_failed, data.failed_automation].filter((value) => typeof value === "string").some((value) => value.trim().length > 0 && value.trim().toLowerCase() !== "none");
8826
+ }
8827
+ function determineOutcome(exitCode, scriptResult, category) {
8828
+ const status = scriptResult?.status ?? "";
8829
+ if (status === "queued" || status.startsWith("skip_")) {
8830
+ return "skipped";
8831
+ }
8832
+ if (exitCode === 124 || status === "timeout" || category === "timeout") {
8833
+ return "timeout";
8834
+ }
8835
+ if (status === "rate_limited" || exitCode !== 0 && category === "rate-limit") {
8836
+ return "rate_limited";
8837
+ }
8838
+ if (status.startsWith("failure") || category === "review-score" || markerIndicatesFailure(scriptResult)) {
8839
+ return "failure";
8840
+ }
8841
+ return exitCode === 0 ? "success" : "failure";
8842
+ }
8843
+ function redactMetadata2(metadata) {
8844
+ return JSON.parse(redactOutcomeText(JSON.stringify(metadata)));
8845
+ }
8846
+ function buildSessionOutcomeInput(input) {
8847
+ const lines = getOutputLines(input.stdout, input.stderr);
8848
+ const classification = classifyFailure(input);
8849
+ const outcome = determineOutcome(input.exitCode, input.scriptResult, classification.category);
8850
+ const attempt = parseAttemptCount(input.scriptResult, lines);
8851
+ const retryCount = parseRetryCount(input.scriptResult, attempt);
8852
+ const reviewScore = parseOptionalNumber(input.scriptResult?.data.final_score ?? input.scriptResult?.data.review_score);
8853
+ const failureCategory = outcome === "failure" || outcome === "timeout" || outcome === "rate_limited" ? classification.category : null;
8854
+ return {
8855
+ projectPath: input.projectPath,
8856
+ jobType: input.jobType,
8857
+ providerKey: input.providerKey || "unknown",
8858
+ prdFile: input.scriptResult?.data.prd ?? input.scriptResult?.data.prd_file ?? null,
8859
+ prNumber: parseFirstPrNumber(input.scriptResult),
8860
+ branchName: input.scriptResult?.data.branch ?? null,
8861
+ startedAt: input.startedAt,
8862
+ finishedAt: input.finishedAt,
8863
+ durationSeconds: Math.max(0, Math.round((input.finishedAt - input.startedAt) / 1e3)),
8864
+ outcome,
8865
+ exitCode: input.exitCode,
8866
+ attempt,
8867
+ retryCount,
8868
+ reviewScore,
8869
+ ciStatus: failureCategory === "ci" ? "fail" : input.scriptResult?.data.ci_status ?? null,
8870
+ failureCategory,
8871
+ failureSignature: failureCategory ? classification.failureSignature : null,
8872
+ metadata: redactMetadata2({
8873
+ ...input.metadata ?? {},
8874
+ scriptStatus: input.scriptResult?.status ?? null,
8875
+ scriptData: input.scriptResult?.data ?? {},
8876
+ minReviewScore: input.minReviewScore ?? null,
8877
+ firstErrorLine: classification.firstErrorLine,
8878
+ fileArea: classification.fileArea
8879
+ })
8880
+ };
8881
+ }
8882
+ var FAILURE_CATEGORIES, SECRET_PLACEHOLDER2, MAX_SIGNATURE_LENGTH, MAX_ERROR_LINE_LENGTH, ANSI_PATTERN, FILE_PATH_PATTERN, TOKEN_SPLIT_PATTERN, SECRET_TEXT_PATTERNS2, CLASSIFIER_RULES;
8883
+ var init_outcome_parser = __esm({
8884
+ "../core/dist/feedback/outcome-parser.js"() {
8885
+ "use strict";
8886
+ FAILURE_CATEGORIES = [
8887
+ "typescript",
8888
+ "eslint",
8889
+ "test",
8890
+ "ci",
8891
+ "review-score",
8892
+ "rate-limit",
8893
+ "timeout",
8894
+ "conflict",
8895
+ "unknown"
8896
+ ];
8897
+ SECRET_PLACEHOLDER2 = "[REDACTED_SECRET]";
8898
+ MAX_SIGNATURE_LENGTH = 240;
8899
+ MAX_ERROR_LINE_LENGTH = 300;
8900
+ ANSI_PATTERN = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, "g");
8901
+ FILE_PATH_PATTERN = /\.(?:[cm]?[jt]sx?|json|md|css|scss|ya?ml)$/i;
8902
+ TOKEN_SPLIT_PATTERN = /[\s('"`]+/;
8903
+ SECRET_TEXT_PATTERNS2 = [
8904
+ [
8905
+ /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g,
8906
+ SECRET_PLACEHOLDER2
8907
+ ],
8908
+ [/\bsk-ant-[\w-]{20,}\b/g, SECRET_PLACEHOLDER2],
8909
+ [/\bsk-[\w-]{20,}\b/g, SECRET_PLACEHOLDER2],
8910
+ [/\bgh[opsru]_\w{30,}\b/g, SECRET_PLACEHOLDER2],
8911
+ [/\bxox[baprs]-[\w-]{20,}\b/g, SECRET_PLACEHOLDER2],
8912
+ [/\b(?:AKIA|ASIA)[A-Z0-9]{16}\b/g, SECRET_PLACEHOLDER2],
8913
+ [/\b(Bearer|Basic)\s+[\w.~+/=-]{12,}/gi, `$1 ${SECRET_PLACEHOLDER2}`],
8914
+ [/\b(token|api[_-]?key|password|secret)=["']?[\w.~+/=-]{12,}/gi, `$1=${SECRET_PLACEHOLDER2}`]
8915
+ ];
8916
+ CLASSIFIER_RULES = [
8917
+ {
8918
+ category: "timeout",
8919
+ pattern: /\b(timed?\s*out|timeout|etimedout|operation was aborted|exit code 124|signal sigterm)\b/i
8920
+ },
8921
+ {
8922
+ category: "rate-limit",
8923
+ pattern: /\b(429|rate[- ]?limit(?:ed)?|too many requests|quota exceeded|resource_exhausted|overloaded)\b/i
8924
+ },
8925
+ {
8926
+ category: "conflict",
8927
+ pattern: /\b(merge conflict|conflict \(|conflict:|unmerged files|needs merge|automatic merge failed|both modified:)\b/i
8928
+ },
8929
+ {
8930
+ category: "typescript",
8931
+ pattern: /\b(TS\d{4}|typescript error|tsc\b.*(?:failed|error)|error TS\d{4})\b/i
8932
+ },
8933
+ {
8934
+ category: "eslint",
8935
+ pattern: /\b(eslint|@typescript-eslint|no-unused-vars|no-explicit-any|react-hooks\/rules-of-hooks)\b/i
8936
+ },
8937
+ {
8938
+ category: "test",
8939
+ pattern: /\b(vitest|jest|playwright|cypress|mocha|assertionerror)\b/i
8940
+ },
8941
+ {
8942
+ category: "test",
8943
+ pattern: /\b(test files?|tests?)\b.*\bfailed\b/i
8944
+ },
8945
+ {
8946
+ category: "test",
8947
+ pattern: /\b(expect\(|locator\(|FAIL\s+\S+\.(?:test|spec)\.)/i
8948
+ },
8949
+ {
8950
+ category: "review-score",
8951
+ pattern: /\b(review score|final_score|score)\b.*\b(below|minimum|min|required|threshold|failed|miss)\b/i
8952
+ },
8953
+ {
8954
+ category: "ci",
8955
+ pattern: /\b(ci|github actions|workflow|status check|required check|check run|failing checks?)\b.*\b(fail|error|cancel|timed out|action_required)\b/i
8956
+ }
8957
+ ];
8958
+ }
8959
+ });
8960
+
8961
+ // ../core/dist/feedback/pattern-analyzer.js
8962
+ function isFailureOutcome(outcome) {
8963
+ return outcome.outcome === "failure" || outcome.outcome === "timeout" || outcome.outcome === "rate_limited";
8964
+ }
8965
+ function truncateText(value, maxLength = MAX_PATTERN_TEXT_LENGTH) {
8966
+ const normalized = value.replace(/\s+/g, " ").trim();
8967
+ if (normalized.length <= maxLength) {
8968
+ return normalized;
8969
+ }
8970
+ return `${normalized.slice(0, maxLength - 3).trimEnd()}...`;
8971
+ }
8972
+ function getStringMetadata(metadata, key) {
8973
+ const value = metadata[key];
8974
+ return typeof value === "string" && value.trim().length > 0 ? value : null;
8975
+ }
8976
+ function getFileArea(outcome) {
8977
+ return getStringMetadata(outcome.metadata, "fileArea") ?? "unknown area";
8978
+ }
8979
+ function countRecentStreaks(repository, outcome) {
8980
+ const recentOutcomes = repository.queryOutcomes({
8981
+ projectPath: outcome.projectPath,
8982
+ jobType: outcome.jobType,
8983
+ limit: 25
8984
+ });
8985
+ let failureStreak = 0;
8986
+ let signatureStreak = 0;
8987
+ for (const recentOutcome of recentOutcomes) {
8988
+ if (!isFailureOutcome(recentOutcome)) {
8989
+ break;
8990
+ }
8991
+ failureStreak += 1;
8992
+ if (recentOutcome.failureSignature === outcome.failureSignature) {
8993
+ signatureStreak += 1;
8994
+ } else {
8995
+ break;
8996
+ }
8997
+ }
8998
+ return { failureStreak, signatureStreak };
8999
+ }
9000
+ function countSuccessStreak(repository, projectPath, jobType) {
9001
+ const recentOutcomes = repository.queryOutcomes({ projectPath, jobType, limit: 25 });
9002
+ let successStreak = 0;
9003
+ for (const outcome of recentOutcomes) {
9004
+ if (outcome.outcome !== "success") {
9005
+ break;
9006
+ }
9007
+ successStreak += 1;
9008
+ }
9009
+ return successStreak;
9010
+ }
9011
+ function calculateRecencyScore(now, lastSeenAt) {
9012
+ const ageMs = Math.max(0, now - lastSeenAt);
9013
+ if (ageMs <= RECENT_WINDOW_MS) {
9014
+ return 1;
9015
+ }
9016
+ if (ageMs <= STALE_WINDOW_MS) {
9017
+ return 0.5;
9018
+ }
9019
+ return 0.15;
9020
+ }
9021
+ function calculateConfidence(sampleCount, lastSeenAt, failureStreak, signatureStreak, now) {
9022
+ const sampleScore = Math.min(1, sampleCount / 2);
9023
+ const streakScore = Math.min(1, Math.max(failureStreak, signatureStreak) / 2);
9024
+ const recencyScore = calculateRecencyScore(now, lastSeenAt);
9025
+ const confidence = sampleScore * 0.45 + streakScore * 0.35 + recencyScore * 0.2;
9026
+ return Math.round(Math.min(1, confidence) * 100) / 100;
9027
+ }
9028
+ function buildPatternTitle(outcome) {
9029
+ const category = outcome.failureCategory ?? "unknown";
9030
+ return truncateText(`Repeated ${category} failure in ${getFileArea(outcome)}`, 120);
9031
+ }
9032
+ function buildPatternDescription(outcome, sampleCount) {
9033
+ return truncateText(`Failure signature has appeared ${sampleCount} times for ${outcome.jobType} sessions.`);
9034
+ }
9035
+ function buildAugmentationPrompt(pattern, outcome) {
9036
+ const category = outcome.failureCategory ?? pattern.category;
9037
+ const fileArea = getFileArea(outcome);
9038
+ const signature = truncateText(outcome.failureSignature ?? pattern.patternKey, MAX_SIGNATURE_PROMPT_LENGTH);
9039
+ const confidencePercent = Math.round(pattern.confidence * 100);
9040
+ const actionByCategory = {
9041
+ ci: "Check failing CI details before broad edits and prioritize the repeated failure area.",
9042
+ conflict: "Check merge conflicts before editing and resolve the repeated conflict area first.",
9043
+ eslint: "Run lint early and fix the repeated ESLint issue before final verification.",
9044
+ "rate-limit": "Avoid unnecessary provider calls and continue with local evidence when rate limits appear.",
9045
+ "review-score": "Read prior low-score review feedback before declaring the PR ready and address repeated concerns.",
9046
+ test: "Run the targeted test area early and fix the repeated failure before final verification.",
9047
+ timeout: "Keep the work scoped and verify incrementally because prior sessions timed out.",
9048
+ typescript: "Run typecheck early and fix the repeated TypeScript issue before final verification.",
9049
+ unknown: "Investigate the repeated failure signature before making broad changes."
9050
+ };
9051
+ return truncateText(`${actionByCategory[category] ?? actionByCategory.unknown} Area: ${fileArea}. Provenance: pattern #${pattern.id}, samples=${pattern.sampleCount}, confidence=${confidencePercent}%, signature="${signature}".`, 320);
9052
+ }
9053
+ function expireStaleAugmentations(repository, projectPath, jobType, now) {
9054
+ const expired = [];
9055
+ const activeAugmentations = repository.listAugmentations({
9056
+ includeExpired: true,
9057
+ jobType,
9058
+ projectPath,
9059
+ status: "active"
9060
+ });
9061
+ for (const augmentation of activeAugmentations) {
9062
+ if (augmentation.expiresAt != null && augmentation.expiresAt <= now) {
9063
+ repository.updateAugmentationStatus(augmentation.id, "expired", projectPath);
9064
+ expired.push(augmentation.id);
9065
+ }
9066
+ }
9067
+ return expired;
9068
+ }
9069
+ function expireAugmentationsAfterSuccessStreak(repository, projectPath, jobType, successStreakToExpire, now) {
9070
+ if (successStreakToExpire <= 0) {
9071
+ return [];
9072
+ }
9073
+ const successStreak = countSuccessStreak(repository, projectPath, jobType);
9074
+ if (successStreak < successStreakToExpire) {
9075
+ return [];
9076
+ }
9077
+ const expired = [];
9078
+ const activeAugmentations = repository.listActiveAugmentations(projectPath, jobType, now);
9079
+ for (const augmentation of activeAugmentations) {
9080
+ repository.updateAugmentationStatus(augmentation.id, "expired", projectPath);
9081
+ expired.push(augmentation.id);
9082
+ }
9083
+ return expired;
9084
+ }
9085
+ function enforceAugmentationCap(repository, projectPath, jobType, maxActiveAugmentations, now) {
9086
+ if (maxActiveAugmentations < 1) {
9087
+ return repository.listActiveAugmentations(projectPath, jobType, now).map((augmentation) => {
9088
+ repository.updateAugmentationStatus(augmentation.id, "expired", projectPath);
9089
+ return augmentation.id;
9090
+ });
9091
+ }
9092
+ const activeAugmentations = repository.listActiveAugmentations(projectPath, jobType, now);
9093
+ if (activeAugmentations.length <= maxActiveAugmentations) {
9094
+ return [];
9095
+ }
9096
+ const activePatterns = repository.listPatterns({
9097
+ jobType,
9098
+ projectPath,
9099
+ status: "active",
9100
+ limit: 100
9101
+ });
9102
+ const confidenceByPatternId = new Map(activePatterns.map((pattern) => [pattern.id, pattern.confidence]));
9103
+ const keepIds = new Set(activeAugmentations.slice().sort((left, right) => {
9104
+ const leftConfidence = left.patternId == null ? 0 : confidenceByPatternId.get(left.patternId) ?? 0;
9105
+ const rightConfidence = right.patternId == null ? 0 : confidenceByPatternId.get(right.patternId) ?? 0;
9106
+ if (leftConfidence !== rightConfidence) {
9107
+ return rightConfidence - leftConfidence;
9108
+ }
9109
+ if (left.createdAt !== right.createdAt) {
9110
+ return right.createdAt - left.createdAt;
9111
+ }
9112
+ return right.id - left.id;
9113
+ }).slice(0, maxActiveAugmentations).map((augmentation) => augmentation.id));
9114
+ const expired = [];
9115
+ for (const augmentation of activeAugmentations) {
9116
+ if (!keepIds.has(augmentation.id)) {
9117
+ repository.updateAugmentationStatus(augmentation.id, "expired", projectPath);
9118
+ expired.push(augmentation.id);
9119
+ }
9120
+ }
9121
+ return expired;
9122
+ }
9123
+ function findActiveAugmentationForPattern(repository, projectPath, jobType, patternId, now) {
9124
+ return repository.listActiveAugmentations(projectPath, jobType, now).find((augmentation) => augmentation.patternId === patternId) ?? null;
9125
+ }
9126
+ function analyzeFeedbackOutcome(repository, outcome, options = {}) {
9127
+ const now = options.now ?? outcome.finishedAt ?? Date.now();
9128
+ const expiredAugmentationIds = expireStaleAugmentations(repository, outcome.projectPath, outcome.jobType, now);
9129
+ if (!isFailureOutcome(outcome) || !outcome.failureSignature || !outcome.failureCategory) {
9130
+ expiredAugmentationIds.push(...expireAugmentationsAfterSuccessStreak(repository, outcome.projectPath, outcome.jobType, options.successStreakToExpire ?? DEFAULT_SUCCESS_STREAK_TO_EXPIRE, now));
9131
+ return { augmentation: null, expiredAugmentationIds, pattern: null };
9132
+ }
9133
+ const existingPattern = repository.listPatterns({
9134
+ jobType: outcome.jobType,
9135
+ projectPath: outcome.projectPath,
9136
+ limit: 100
9137
+ }).find((pattern2) => pattern2.patternKey === outcome.failureSignature) ?? null;
9138
+ const sampleCount = (existingPattern?.sampleCount ?? 0) + 1;
9139
+ const streakStats = countRecentStreaks(repository, outcome);
9140
+ const confidence = calculateConfidence(sampleCount, outcome.finishedAt, streakStats.failureStreak, streakStats.signatureStreak, now);
9141
+ const status = confidence >= (options.confidenceThreshold ?? DEFAULT_CONFIDENCE_THRESHOLD) ? "active" : existingPattern?.status ?? "observing";
9142
+ const pattern = repository.upsertPattern({
9143
+ category: outcome.failureCategory,
9144
+ confidence,
9145
+ description: buildPatternDescription(outcome, sampleCount),
9146
+ jobType: outcome.jobType,
9147
+ lastSeenAt: outcome.finishedAt,
9148
+ metadata: {
9149
+ confidenceInputs: {
9150
+ failureStreak: streakStats.failureStreak,
9151
+ recencyScore: calculateRecencyScore(now, outcome.finishedAt),
9152
+ sampleCount,
9153
+ signatureStreak: streakStats.signatureStreak
9154
+ },
9155
+ failureSignature: outcome.failureSignature,
9156
+ fileArea: getFileArea(outcome),
9157
+ firstErrorLine: getStringMetadata(outcome.metadata, "firstErrorLine"),
9158
+ lastOutcomeId: outcome.id
9159
+ },
9160
+ patternKey: outcome.failureSignature,
9161
+ projectPath: outcome.projectPath,
9162
+ sampleCount,
9163
+ status,
9164
+ title: buildPatternTitle(outcome)
9165
+ });
9166
+ let augmentation = null;
9167
+ if (pattern.status === "active") {
9168
+ augmentation = findActiveAugmentationForPattern(repository, outcome.projectPath, outcome.jobType, pattern.id, now);
9169
+ if (!augmentation) {
9170
+ augmentation = repository.createAugmentation({
9171
+ expiresAt: now + (options.augmentationTtlMs ?? DEFAULT_AUGMENTATION_TTL_MS),
9172
+ jobType: outcome.jobType,
9173
+ patternId: pattern.id,
9174
+ projectPath: outcome.projectPath,
9175
+ promptText: buildAugmentationPrompt(pattern, outcome),
9176
+ status: "active"
9177
+ });
9178
+ }
9179
+ }
9180
+ expiredAugmentationIds.push(...enforceAugmentationCap(repository, outcome.projectPath, outcome.jobType, options.maxActiveAugmentations ?? DEFAULT_MAX_ACTIVE_AUGMENTATIONS, now));
9181
+ return { augmentation, expiredAugmentationIds, pattern };
8519
9182
  }
8520
- var logger5;
8521
- var init_board_sync = __esm({
8522
- "../core/dist/audit/board-sync.js"() {
9183
+ var DEFAULT_CONFIDENCE_THRESHOLD, DEFAULT_AUGMENTATION_TTL_MS, DEFAULT_MAX_ACTIVE_AUGMENTATIONS, DEFAULT_SUCCESS_STREAK_TO_EXPIRE, RECENT_WINDOW_MS, STALE_WINDOW_MS, MAX_PATTERN_TEXT_LENGTH, MAX_SIGNATURE_PROMPT_LENGTH;
9184
+ var init_pattern_analyzer = __esm({
9185
+ "../core/dist/feedback/pattern-analyzer.js"() {
8523
9186
  "use strict";
8524
- init_factory();
8525
- init_logger();
8526
- init_report();
8527
- logger5 = createLogger("audit-sync");
9187
+ DEFAULT_CONFIDENCE_THRESHOLD = 0.75;
9188
+ DEFAULT_AUGMENTATION_TTL_MS = 14 * 24 * 60 * 60 * 1e3;
9189
+ DEFAULT_MAX_ACTIVE_AUGMENTATIONS = 3;
9190
+ DEFAULT_SUCCESS_STREAK_TO_EXPIRE = 3;
9191
+ RECENT_WINDOW_MS = 7 * 24 * 60 * 60 * 1e3;
9192
+ STALE_WINDOW_MS = 14 * 24 * 60 * 60 * 1e3;
9193
+ MAX_PATTERN_TEXT_LENGTH = 180;
9194
+ MAX_SIGNATURE_PROMPT_LENGTH = 90;
8528
9195
  }
8529
9196
  });
8530
9197
 
8531
- // ../core/dist/audit/index.js
8532
- var init_audit = __esm({
8533
- "../core/dist/audit/index.js"() {
9198
+ // ../core/dist/feedback/prompt-augmenter.js
9199
+ function isActiveAt(augmentation, now) {
9200
+ return augmentation.status === "active" && (augmentation.expiresAt == null || augmentation.expiresAt > now);
9201
+ }
9202
+ function normalizeMaxActive(value) {
9203
+ if (value == null || !Number.isFinite(value)) {
9204
+ return DEFAULT_MAX_ACTIVE_AUGMENTATIONS2;
9205
+ }
9206
+ return Math.max(0, Math.floor(value));
9207
+ }
9208
+ function truncateSnippet(value) {
9209
+ const normalized = value.replace(/\s+/g, " ").trim();
9210
+ if (normalized.length <= MAX_SNIPPET_LENGTH) {
9211
+ return normalized;
9212
+ }
9213
+ return `${normalized.slice(0, MAX_SNIPPET_LENGTH - 3).trimEnd()}...`;
9214
+ }
9215
+ function isFeedbackPromptEnabled() {
9216
+ const raw = process.env.NW_FEEDBACK_ENABLED ?? process.env.NW_FEEDBACK_PROMPT_ENABLED;
9217
+ if (!raw) {
9218
+ return true;
9219
+ }
9220
+ return !DISABLED_VALUES.has(raw.trim().toLowerCase());
9221
+ }
9222
+ function selectPromptAugmentations(augmentations, options = {}) {
9223
+ if (options.feedbackEnabled === false) {
9224
+ return [];
9225
+ }
9226
+ const now = options.now ?? Date.now();
9227
+ const maxActive = normalizeMaxActive(options.maxActiveAugmentations);
9228
+ if (maxActive === 0) {
9229
+ return [];
9230
+ }
9231
+ return augmentations.filter((augmentation) => isActiveAt(augmentation, now)).sort((left, right) => {
9232
+ if (left.createdAt !== right.createdAt) {
9233
+ return left.createdAt - right.createdAt;
9234
+ }
9235
+ return left.id - right.id;
9236
+ }).slice(0, maxActive);
9237
+ }
9238
+ function renderProjectFeedbackBlock(augmentations, options = {}) {
9239
+ const selected = selectPromptAugmentations(augmentations, options);
9240
+ if (selected.length === 0) {
9241
+ return "";
9242
+ }
9243
+ const lines = [
9244
+ "## Project Feedback",
9245
+ "The following short notes come from repeated recent Night Watch failures. Treat them as targeted guardrails, not as replacements for the main task instructions.",
9246
+ "",
9247
+ ...selected.map((augmentation) => `- ${truncateSnippet(augmentation.promptText)}`)
9248
+ ];
9249
+ return lines.join("\n");
9250
+ }
9251
+ function buildProjectFeedbackPromptBlock(repository, projectPath, jobType, options = {}) {
9252
+ const enabled = options.feedbackEnabled ?? isFeedbackPromptEnabled();
9253
+ if (!enabled) {
9254
+ return { augmentationIds: [], promptBlock: "" };
9255
+ }
9256
+ const now = options.now ?? Date.now();
9257
+ const activeAugmentations = repository.listActiveAugmentations(projectPath, jobType, now);
9258
+ const selected = selectPromptAugmentations(activeAugmentations, {
9259
+ ...options,
9260
+ feedbackEnabled: enabled,
9261
+ now
9262
+ });
9263
+ const promptBlock = renderProjectFeedbackBlock(selected, {
9264
+ ...options,
9265
+ feedbackEnabled: enabled,
9266
+ now
9267
+ });
9268
+ if (options.markApplied === true && promptBlock.length > 0) {
9269
+ for (const augmentation of selected) {
9270
+ repository.incrementAugmentationCounts(augmentation.id);
9271
+ }
9272
+ }
9273
+ return {
9274
+ augmentationIds: selected.map((augmentation) => augmentation.id),
9275
+ promptBlock
9276
+ };
9277
+ }
9278
+ var DEFAULT_MAX_ACTIVE_AUGMENTATIONS2, MAX_SNIPPET_LENGTH, DISABLED_VALUES;
9279
+ var init_prompt_augmenter = __esm({
9280
+ "../core/dist/feedback/prompt-augmenter.js"() {
8534
9281
  "use strict";
8535
- init_report();
8536
- init_board_sync();
9282
+ DEFAULT_MAX_ACTIVE_AUGMENTATIONS2 = 3;
9283
+ MAX_SNIPPET_LENGTH = 260;
9284
+ DISABLED_VALUES = /* @__PURE__ */ new Set(["0", "false", "no", "off", "disabled"]);
8537
9285
  }
8538
9286
  });
8539
9287
 
@@ -8756,6 +9504,7 @@ __export(dist_exports, {
8756
9504
  DEFAULT_DEFAULT_BRANCH: () => DEFAULT_DEFAULT_BRANCH,
8757
9505
  DEFAULT_EXECUTOR_ENABLED: () => DEFAULT_EXECUTOR_ENABLED,
8758
9506
  DEFAULT_FALLBACK_ON_RATE_LIMIT: () => DEFAULT_FALLBACK_ON_RATE_LIMIT,
9507
+ DEFAULT_FEEDBACK: () => DEFAULT_FEEDBACK,
8759
9508
  DEFAULT_JOB_PROVIDERS: () => DEFAULT_JOB_PROVIDERS,
8760
9509
  DEFAULT_LOCAL_BOARD_INFO: () => DEFAULT_LOCAL_BOARD_INFO,
8761
9510
  DEFAULT_MAX_LOG_SIZE: () => DEFAULT_MAX_LOG_SIZE,
@@ -8821,6 +9570,7 @@ __export(dist_exports, {
8821
9570
  EXECUTOR_PARTIAL_LABEL: () => EXECUTOR_PARTIAL_LABEL,
8822
9571
  EXECUTOR_READY_REVIEW_LABEL: () => EXECUTOR_READY_REVIEW_LABEL,
8823
9572
  EXECUTOR_RESUMABLE_LABEL: () => EXECUTOR_RESUMABLE_LABEL,
9573
+ FAILURE_CATEGORIES: () => FAILURE_CATEGORIES,
8824
9574
  GLOBAL_CONFIG_DIR: () => GLOBAL_CONFIG_DIR,
8825
9575
  GLOBAL_NOTIFICATIONS_FILE_NAME: () => GLOBAL_NOTIFICATIONS_FILE_NAME,
8826
9576
  HISTORY_FILE_NAME: () => HISTORY_FILE_NAME,
@@ -8849,6 +9599,7 @@ __export(dist_exports, {
8849
9599
  ROADMAP_SECTION_MAPPINGS: () => ROADMAP_SECTION_MAPPINGS,
8850
9600
  STATE_DB_FILE_NAME: () => STATE_DB_FILE_NAME,
8851
9601
  SqliteKanbanIssueRepository: () => SqliteKanbanIssueRepository,
9602
+ SqliteSessionOutcomeRepository: () => SqliteSessionOutcomeRepository,
8852
9603
  VALID_CLAUDE_MODELS: () => VALID_CLAUDE_MODELS,
8853
9604
  VALID_JOB_TYPES: () => VALID_JOB_TYPES,
8854
9605
  VALID_MERGE_METHODS: () => VALID_MERGE_METHODS,
@@ -8857,9 +9608,12 @@ __export(dist_exports, {
8857
9608
  addDelayToIsoString: () => addDelayToIsoString,
8858
9609
  addEntry: () => addEntry,
8859
9610
  analyticsLockPath: () => analyticsLockPath,
9611
+ analyzeFeedbackOutcome: () => analyzeFeedbackOutcome,
8860
9612
  auditLockPath: () => auditLockPath,
8861
9613
  buildDescription: () => buildDescription,
8862
9614
  buildJobEnvOverrides: () => buildJobEnvOverrides,
9615
+ buildProjectFeedbackPromptBlock: () => buildProjectFeedbackPromptBlock,
9616
+ buildSessionOutcomeInput: () => buildSessionOutcomeInput,
8863
9617
  calculateStringSimilarity: () => calculateStringSimilarity,
8864
9618
  camelToUpperSnake: () => camelToUpperSnake,
8865
9619
  canStartJob: () => canStartJob,
@@ -8875,6 +9629,7 @@ __export(dist_exports, {
8875
9629
  checkProviderCli: () => checkProviderCli,
8876
9630
  checkRateLimited: () => checkRateLimited,
8877
9631
  claimJobSlot: () => claimJobSlot,
9632
+ classifyFailure: () => classifyFailure,
8878
9633
  cleanupExpiredJobs: () => cleanupExpiredJobs,
8879
9634
  cleanupWorktrees: () => cleanupWorktrees,
8880
9635
  clearPrdState: () => clearPrdState,
@@ -8981,6 +9736,7 @@ __export(dist_exports, {
8981
9736
  info: () => info,
8982
9737
  initContainer: () => initContainer,
8983
9738
  isContainerInitialized: () => isContainerInitialized,
9739
+ isFeedbackPromptEnabled: () => isFeedbackPromptEnabled,
8984
9740
  isInCooldown: () => isInCooldown,
8985
9741
  isItemProcessed: () => isItemProcessed,
8986
9742
  isJobTypeEnabled: () => isJobTypeEnabled,
@@ -9024,6 +9780,7 @@ __export(dist_exports, {
9024
9780
  readPrdStates: () => readPrdStates,
9025
9781
  recordExecution: () => recordExecution,
9026
9782
  recordJobRun: () => recordJobRun,
9783
+ redactOutcomeText: () => redactOutcomeText,
9027
9784
  registerProject: () => registerProject,
9028
9785
  releaseLock: () => releaseLock,
9029
9786
  removeEntries: () => removeEntries,
@@ -9031,6 +9788,7 @@ __export(dist_exports, {
9031
9788
  removeJob: () => removeJob,
9032
9789
  removeProject: () => removeProject,
9033
9790
  renderPrdTemplate: () => renderPrdTemplate,
9791
+ renderProjectFeedbackBlock: () => renderProjectFeedbackBlock,
9034
9792
  renderSlicerPrompt: () => renderSlicerPrompt,
9035
9793
  resetRepositories: () => resetRepositories,
9036
9794
  resolveJobProvider: () => resolveJobProvider,
@@ -9048,6 +9806,7 @@ __export(dist_exports, {
9048
9806
  saveRegistry: () => saveRegistry,
9049
9807
  saveRoadmapState: () => saveRoadmapState,
9050
9808
  scanRoadmap: () => scanRoadmap,
9809
+ selectPromptAugmentations: () => selectPromptAugmentations,
9051
9810
  sendNotifications: () => sendNotifications,
9052
9811
  sendWebhook: () => sendWebhook,
9053
9812
  setConfigValue: () => setConfigValue,
@@ -9117,6 +9876,9 @@ var init_dist = __esm({
9117
9876
  init_summary();
9118
9877
  init_analytics();
9119
9878
  init_audit();
9879
+ init_outcome_parser();
9880
+ init_pattern_analyzer();
9881
+ init_prompt_augmenter();
9120
9882
  init_prd_template();
9121
9883
  init_slicer_prompt();
9122
9884
  init_jobs();
@@ -9394,6 +10156,7 @@ function buildInitConfig(params) {
9394
10156
  },
9395
10157
  audit: { ...defaults.audit },
9396
10158
  analytics: { ...defaults.analytics },
10159
+ feedback: { ...defaults.feedback },
9397
10160
  merger: { ...defaults.merger },
9398
10161
  prResolver: { ...defaults.prResolver },
9399
10162
  jobProviders: { ...defaults.jobProviders },
@@ -9958,6 +10721,47 @@ function getTelegramStatusWebhooks(config) {
9958
10721
  ).map((wh) => ({ botToken: wh.botToken, chatId: wh.chatId }));
9959
10722
  }
9960
10723
 
10724
+ // src/commands/shared/feedback.ts
10725
+ init_dist();
10726
+ function getFeedbackAnalysisOptions(config) {
10727
+ const feedback = config.feedback ?? {
10728
+ augmentationTtlDays: 14,
10729
+ confidenceThreshold: 0.75,
10730
+ maxActiveAugmentations: 3,
10731
+ successStreakToExpire: 3
10732
+ };
10733
+ return {
10734
+ augmentationTtlMs: feedback.augmentationTtlDays * 24 * 60 * 60 * 1e3,
10735
+ confidenceThreshold: feedback.confidenceThreshold,
10736
+ maxActiveAugmentations: feedback.maxActiveAugmentations,
10737
+ successStreakToExpire: feedback.successStreakToExpire
10738
+ };
10739
+ }
10740
+ function isFeedbackEnabled(config) {
10741
+ return config.feedback?.enabled !== false && isFeedbackPromptEnabled();
10742
+ }
10743
+ function recordJobOutcome(input) {
10744
+ const repository = getRepositories().sessionOutcomes;
10745
+ const storedOutcome = repository.insertOutcome(
10746
+ buildSessionOutcomeInput({
10747
+ exitCode: input.exitCode,
10748
+ finishedAt: input.finishedAt,
10749
+ jobType: input.jobType,
10750
+ metadata: input.metadata,
10751
+ minReviewScore: input.minReviewScore,
10752
+ projectPath: input.projectDir,
10753
+ providerKey: input.providerKey ?? resolveJobProvider(input.config, input.jobType),
10754
+ scriptResult: input.scriptResult,
10755
+ startedAt: input.startedAt,
10756
+ stderr: input.stderr,
10757
+ stdout: input.stdout
10758
+ })
10759
+ );
10760
+ if (isFeedbackEnabled(input.config)) {
10761
+ analyzeFeedbackOutcome(repository, storedOutcome, getFeedbackAnalysisOptions(input.config));
10762
+ }
10763
+ }
10764
+
9961
10765
  // src/commands/run.ts
9962
10766
  import * as fs24 from "fs";
9963
10767
  import * as path23 from "path";
@@ -10072,16 +10876,34 @@ async function runCrossProjectFallback(currentProjectDir, options) {
10072
10876
  let candidateConfig = loadConfig(candidate.path);
10073
10877
  candidateConfig = applyCliOverrides(candidateConfig, options);
10074
10878
  const envVars = buildEnvVars(candidateConfig, options);
10879
+ applyProjectFeedbackPromptEnv(envVars, candidate.path, "executor");
10075
10880
  envVars.NW_CROSS_PROJECT_FALLBACK_ACTIVE = "1";
10076
10881
  try {
10882
+ const startedAt = Date.now();
10077
10883
  const { exitCode, stdout, stderr } = await executeScriptWithOutput(
10078
10884
  scriptPath,
10079
10885
  [candidate.path],
10080
10886
  envVars,
10081
10887
  { cwd: candidate.path }
10082
10888
  );
10889
+ const finishedAt = Date.now();
10083
10890
  const scriptResult = parseScriptResult(`${stdout}
10084
10891
  ${stderr}`);
10892
+ try {
10893
+ recordRunSessionOutcome({
10894
+ projectDir: candidate.path,
10895
+ config: candidateConfig,
10896
+ envVars,
10897
+ startedAt,
10898
+ finishedAt,
10899
+ exitCode,
10900
+ stdout,
10901
+ stderr,
10902
+ scriptResult,
10903
+ metadata: { crossProjectFallback: true }
10904
+ });
10905
+ } catch {
10906
+ }
10085
10907
  if (!options.dryRun) {
10086
10908
  await sendRunCompletionNotifications(
10087
10909
  candidateConfig,
@@ -10118,6 +10940,48 @@ function getRateLimitFallbackTelegramWebhooks(config) {
10118
10940
  function isRateLimitFallbackTriggered(resultData) {
10119
10941
  return resultData?.rate_limit_fallback === "1";
10120
10942
  }
10943
+ function recordRunSessionOutcome(input) {
10944
+ const outcome = buildSessionOutcomeInput({
10945
+ projectPath: input.projectDir,
10946
+ jobType: "executor",
10947
+ providerKey: input.envVars.NW_PROVIDER_KEY ?? resolveJobProvider(input.config, "executor"),
10948
+ startedAt: input.startedAt,
10949
+ finishedAt: input.finishedAt,
10950
+ exitCode: input.exitCode,
10951
+ stdout: input.stdout,
10952
+ stderr: input.stderr,
10953
+ scriptResult: input.scriptResult,
10954
+ metadata: {
10955
+ providerCommand: input.envVars.NW_PROVIDER_CMD,
10956
+ providerLabel: input.envVars.NW_PROVIDER_LABEL,
10957
+ ...input.metadata ?? {}
10958
+ }
10959
+ });
10960
+ const repository = getRepositories().sessionOutcomes;
10961
+ const storedOutcome = repository.insertOutcome(outcome);
10962
+ if (isFeedbackEnabled(input.config)) {
10963
+ analyzeFeedbackOutcome(repository, storedOutcome, getFeedbackAnalysisOptions(input.config));
10964
+ }
10965
+ }
10966
+ function applyProjectFeedbackPromptEnv(envVars, projectDir, jobType, markApplied = true) {
10967
+ delete envVars.NW_PROJECT_FEEDBACK_PROMPT;
10968
+ const config = loadConfig(projectDir);
10969
+ if (!isFeedbackPromptEnabled() || config.feedback?.enabled === false) {
10970
+ return;
10971
+ }
10972
+ try {
10973
+ const { promptBlock } = buildProjectFeedbackPromptBlock(
10974
+ getRepositories().sessionOutcomes,
10975
+ projectDir,
10976
+ jobType,
10977
+ { markApplied, maxActiveAugmentations: config.feedback?.maxActiveAugmentations }
10978
+ );
10979
+ if (promptBlock.length > 0) {
10980
+ envVars.NW_PROJECT_FEEDBACK_PROMPT = promptBlock;
10981
+ }
10982
+ } catch {
10983
+ }
10984
+ }
10121
10985
  function buildEnvVars(config, options) {
10122
10986
  const env = buildBaseEnvVars(config, "executor", options.dryRun);
10123
10987
  env.NW_MAX_RUNTIME = String(config.maxRuntime);
@@ -10256,6 +11120,7 @@ function runCommand(program2) {
10256
11120
  process.exit(0);
10257
11121
  }
10258
11122
  const envVars = buildEnvVars(config, options);
11123
+ applyProjectFeedbackPromptEnv(envVars, projectDir, "executor", !options.dryRun);
10259
11124
  const scriptPath = getScriptPath("night-watch-cron.sh");
10260
11125
  if (options.dryRun) {
10261
11126
  header("Dry Run: PRD Executor");
@@ -10348,6 +11213,7 @@ function runCommand(program2) {
10348
11213
  const spinner = createSpinner("Running PRD executor...");
10349
11214
  spinner.start();
10350
11215
  try {
11216
+ const startedAt = Date.now();
10351
11217
  await maybeApplyCronSchedulingDelay(config, "executor", projectDir);
10352
11218
  const { exitCode, stdout, stderr } = await executeScriptWithOutput(
10353
11219
  scriptPath,
@@ -10355,6 +11221,7 @@ function runCommand(program2) {
10355
11221
  envVars,
10356
11222
  { cwd: projectDir }
10357
11223
  );
11224
+ const finishedAt = Date.now();
10358
11225
  const scriptResult = parseScriptResult(`${stdout}
10359
11226
  ${stderr}`);
10360
11227
  if (exitCode === 0) {
@@ -10371,6 +11238,20 @@ ${stderr}`);
10371
11238
  spinner.fail(`PRD executor exited with code ${exitCode}`);
10372
11239
  }
10373
11240
  if (!options.dryRun) {
11241
+ try {
11242
+ recordRunSessionOutcome({
11243
+ projectDir,
11244
+ config,
11245
+ envVars,
11246
+ startedAt,
11247
+ finishedAt,
11248
+ exitCode,
11249
+ stdout,
11250
+ stderr,
11251
+ scriptResult
11252
+ });
11253
+ } catch {
11254
+ }
10374
11255
  await sendRunCompletionNotifications(config, projectDir, options, exitCode, scriptResult);
10375
11256
  }
10376
11257
  if (shouldAttemptCrossProjectFallback(options, scriptResult?.status)) {
@@ -10440,6 +11321,25 @@ function buildReviewNotificationTargets(reviewedPrNumbers, noChangesPrNumbers, l
10440
11321
  noChangesNeeded: noChangesSet.has(prNumber)
10441
11322
  }));
10442
11323
  }
11324
+ function applyProjectFeedbackPromptEnv2(envVars, projectDir, jobType, markApplied = true) {
11325
+ delete envVars.NW_PROJECT_FEEDBACK_PROMPT;
11326
+ const config = loadConfig(projectDir);
11327
+ if (!isFeedbackPromptEnabled() || config.feedback?.enabled === false) {
11328
+ return;
11329
+ }
11330
+ try {
11331
+ const { promptBlock } = buildProjectFeedbackPromptBlock(
11332
+ getRepositories().sessionOutcomes,
11333
+ projectDir,
11334
+ jobType,
11335
+ { markApplied, maxActiveAugmentations: config.feedback?.maxActiveAugmentations }
11336
+ );
11337
+ if (promptBlock.length > 0) {
11338
+ envVars.NW_PROJECT_FEEDBACK_PROMPT = promptBlock;
11339
+ }
11340
+ } catch {
11341
+ }
11342
+ }
10443
11343
  function parseRetryAttempts(raw) {
10444
11344
  if (!raw) {
10445
11345
  return 1;
@@ -10550,6 +11450,7 @@ function reviewCommand(program2) {
10550
11450
  process.exit(0);
10551
11451
  }
10552
11452
  const envVars = buildEnvVars2(config, options);
11453
+ applyProjectFeedbackPromptEnv2(envVars, projectDir, "reviewer", !options.dryRun);
10553
11454
  const scriptPath = getScriptPath("night-watch-pr-reviewer-cron.sh");
10554
11455
  if (options.dryRun) {
10555
11456
  header("Dry Run: PR Reviewer");
@@ -10614,12 +11515,14 @@ function reviewCommand(program2) {
10614
11515
  const spinner = createSpinner("Running PR reviewer...");
10615
11516
  spinner.start();
10616
11517
  try {
11518
+ const startedAt = Date.now();
10617
11519
  await maybeApplyCronSchedulingDelay(config, "reviewer", projectDir);
10618
11520
  const { exitCode, stdout, stderr } = await executeScriptWithOutput(
10619
11521
  scriptPath,
10620
11522
  [projectDir],
10621
11523
  envVars
10622
11524
  );
11525
+ const finishedAt = Date.now();
10623
11526
  const scriptResult = parseScriptResult(`${stdout}
10624
11527
  ${stderr}`);
10625
11528
  if (exitCode === 0) {
@@ -10634,6 +11537,31 @@ ${stderr}`);
10634
11537
  spinner.fail(`PR reviewer exited with code ${exitCode}`);
10635
11538
  }
10636
11539
  if (!options.dryRun) {
11540
+ try {
11541
+ const repository = getRepositories().sessionOutcomes;
11542
+ const storedOutcome = repository.insertOutcome(
11543
+ buildSessionOutcomeInput({
11544
+ exitCode,
11545
+ finishedAt,
11546
+ jobType: "reviewer",
11547
+ metadata: {
11548
+ providerCommand: envVars.NW_PROVIDER_CMD,
11549
+ providerLabel: envVars.NW_PROVIDER_LABEL
11550
+ },
11551
+ minReviewScore: config.minReviewScore,
11552
+ projectPath: projectDir,
11553
+ providerKey: envVars.NW_PROVIDER_KEY ?? resolveJobProvider(config, "reviewer"),
11554
+ scriptResult,
11555
+ startedAt,
11556
+ stderr,
11557
+ stdout
11558
+ })
11559
+ );
11560
+ if (isFeedbackEnabled(config)) {
11561
+ analyzeFeedbackOutcome(repository, storedOutcome, getFeedbackAnalysisOptions(config));
11562
+ }
11563
+ } catch {
11564
+ }
10637
11565
  const shouldNotifyCompletion = shouldSendReviewCompletionNotification(
10638
11566
  exitCode,
10639
11567
  scriptResult?.status
@@ -10847,12 +11775,14 @@ function qaCommand(program2) {
10847
11775
  const spinner = createSpinner("Running QA process...");
10848
11776
  spinner.start();
10849
11777
  try {
11778
+ const startedAt = Date.now();
10850
11779
  await maybeApplyCronSchedulingDelay(config, "qa", projectDir);
10851
11780
  const { exitCode, stdout, stderr } = await executeScriptWithOutput(
10852
11781
  scriptPath,
10853
11782
  [projectDir],
10854
11783
  envVars
10855
11784
  );
11785
+ const finishedAt = Date.now();
10856
11786
  const scriptResult = parseScriptResult(`${stdout}
10857
11787
  ${stderr}`);
10858
11788
  if (exitCode === 0) {
@@ -10869,6 +11799,25 @@ ${stderr}`);
10869
11799
  spinner.fail(`QA process exited with code ${exitCode}`);
10870
11800
  }
10871
11801
  if (!options.dryRun) {
11802
+ try {
11803
+ recordJobOutcome({
11804
+ config,
11805
+ exitCode,
11806
+ finishedAt,
11807
+ jobType: "qa",
11808
+ metadata: {
11809
+ providerCommand: envVars.NW_PROVIDER_CMD,
11810
+ providerLabel: envVars.NW_PROVIDER_LABEL
11811
+ },
11812
+ projectDir,
11813
+ providerKey: envVars.NW_PROVIDER_KEY ?? resolveJobProvider(config, "qa"),
11814
+ scriptResult,
11815
+ startedAt,
11816
+ stderr,
11817
+ stdout
11818
+ });
11819
+ } catch {
11820
+ }
10872
11821
  const skipNotification = !shouldSendQaNotification(scriptResult?.status);
10873
11822
  if (skipNotification) {
10874
11823
  info("Skipping QA notification (no actionable QA result)");
@@ -10971,6 +11920,7 @@ function auditCommand(program2) {
10971
11920
  }
10972
11921
  const spinner = createSpinner("Running code audit...");
10973
11922
  spinner.start();
11923
+ const startedAt = Date.now();
10974
11924
  try {
10975
11925
  await maybeApplyCronSchedulingDelay(config, "audit", projectDir);
10976
11926
  const { exitCode, stdout, stderr } = await executeScriptWithOutput(
@@ -10978,8 +11928,30 @@ function auditCommand(program2) {
10978
11928
  [projectDir],
10979
11929
  envVars
10980
11930
  );
11931
+ const finishedAt = Date.now();
10981
11932
  const scriptResult = parseScriptResult(`${stdout}
10982
11933
  ${stderr}`);
11934
+ if (!options.dryRun) {
11935
+ try {
11936
+ recordJobOutcome({
11937
+ config,
11938
+ exitCode,
11939
+ finishedAt,
11940
+ jobType: "audit",
11941
+ metadata: {
11942
+ providerCommand: envVars.NW_PROVIDER_CMD,
11943
+ providerLabel: envVars.NW_PROVIDER_LABEL
11944
+ },
11945
+ projectDir,
11946
+ providerKey: envVars.NW_PROVIDER_KEY ?? resolveJobProvider(config, "audit"),
11947
+ scriptResult,
11948
+ startedAt,
11949
+ stderr,
11950
+ stdout
11951
+ });
11952
+ } catch {
11953
+ }
11954
+ }
10983
11955
  if (exitCode === 0) {
10984
11956
  if (scriptResult?.status === "queued") {
10985
11957
  spinner.succeed("Code audit queued \u2014 another job is currently running");
@@ -11016,6 +11988,23 @@ ${stderr}`);
11016
11988
  process.exit(exitCode || 1);
11017
11989
  }
11018
11990
  } catch (err) {
11991
+ try {
11992
+ recordJobOutcome({
11993
+ config,
11994
+ exitCode: 1,
11995
+ finishedAt: Date.now(),
11996
+ jobType: "audit",
11997
+ metadata: {
11998
+ providerCommand: envVars.NW_PROVIDER_CMD,
11999
+ providerLabel: envVars.NW_PROVIDER_LABEL
12000
+ },
12001
+ projectDir,
12002
+ providerKey: envVars.NW_PROVIDER_KEY ?? resolveJobProvider(config, "audit"),
12003
+ startedAt,
12004
+ stderr: err instanceof Error ? err.message : String(err)
12005
+ });
12006
+ } catch {
12007
+ }
11019
12008
  spinner.fail(`Code audit failed: ${err instanceof Error ? err.message : String(err)}`);
11020
12009
  process.exit(1);
11021
12010
  }
@@ -11047,6 +12036,25 @@ function analyticsCommand(program2) {
11047
12036
  const apiKey = config.providerEnv?.AMPLITUDE_API_KEY;
11048
12037
  const secretKey = config.providerEnv?.AMPLITUDE_SECRET_KEY;
11049
12038
  if (!apiKey || !secretKey) {
12039
+ const now = Date.now();
12040
+ if (!options.dryRun) {
12041
+ try {
12042
+ recordJobOutcome({
12043
+ config,
12044
+ exitCode: 1,
12045
+ finishedAt: now,
12046
+ jobType: "analytics",
12047
+ metadata: {
12048
+ missingAmplitudeCredentials: true
12049
+ },
12050
+ projectDir,
12051
+ providerKey: resolveJobProvider(config, "analytics"),
12052
+ startedAt: now,
12053
+ stderr: "AMPLITUDE_API_KEY and AMPLITUDE_SECRET_KEY must be set in providerEnv to run analytics."
12054
+ });
12055
+ } catch {
12056
+ }
12057
+ }
11050
12058
  info(
11051
12059
  "AMPLITUDE_API_KEY and AMPLITUDE_SECRET_KEY must be set in providerEnv to run analytics."
11052
12060
  );
@@ -11068,11 +12076,45 @@ function analyticsCommand(program2) {
11068
12076
  }
11069
12077
  const spinner = createSpinner("Running analytics job...");
11070
12078
  spinner.start();
12079
+ const startedAt = Date.now();
11071
12080
  try {
11072
12081
  await maybeApplyCronSchedulingDelay(config, "analytics", projectDir);
11073
12082
  const result = await runAnalytics(config, projectDir);
12083
+ try {
12084
+ recordJobOutcome({
12085
+ config,
12086
+ exitCode: 0,
12087
+ finishedAt: Date.now(),
12088
+ jobType: "analytics",
12089
+ metadata: {
12090
+ lookbackDays: config.analytics.lookbackDays,
12091
+ summary: result.summary
12092
+ },
12093
+ projectDir,
12094
+ providerKey: resolveJobProvider(config, "analytics"),
12095
+ startedAt,
12096
+ stdout: result.summary
12097
+ });
12098
+ } catch {
12099
+ }
11074
12100
  spinner.succeed(`Analytics complete \u2014 ${result.summary}`);
11075
12101
  } catch (err) {
12102
+ try {
12103
+ recordJobOutcome({
12104
+ config,
12105
+ exitCode: 1,
12106
+ finishedAt: Date.now(),
12107
+ jobType: "analytics",
12108
+ metadata: {
12109
+ lookbackDays: config.analytics.lookbackDays
12110
+ },
12111
+ projectDir,
12112
+ providerKey: resolveJobProvider(config, "analytics"),
12113
+ startedAt,
12114
+ stderr: err instanceof Error ? err.message : String(err)
12115
+ });
12116
+ } catch {
12117
+ }
11076
12118
  spinner.fail(`Analytics failed: ${err instanceof Error ? err.message : String(err)}`);
11077
12119
  process.exit(1);
11078
12120
  }
@@ -15787,11 +16829,217 @@ function createProjectDoctorRoutes() {
15787
16829
  return router;
15788
16830
  }
15789
16831
 
16832
+ // ../server/dist/routes/feedback.routes.js
16833
+ init_dist();
16834
+ import { Router as Router5 } from "express";
16835
+ var DAY_MS = 24 * 60 * 60 * 1e3;
16836
+ var WINDOW_DAYS = [7, 30];
16837
+ var VALID_AUGMENTATION_STATUSES = [
16838
+ "active",
16839
+ "paused",
16840
+ "expired",
16841
+ "archived"
16842
+ ];
16843
+ function emptyBreakdown() {
16844
+ return {
16845
+ totalCount: 0,
16846
+ successCount: 0,
16847
+ failureCount: 0,
16848
+ timeoutCount: 0,
16849
+ rateLimitedCount: 0,
16850
+ skippedCount: 0,
16851
+ successRate: null
16852
+ };
16853
+ }
16854
+ function applyOutcome(summary, outcome) {
16855
+ summary.totalCount += 1;
16856
+ if (outcome === "success")
16857
+ summary.successCount += 1;
16858
+ if (outcome === "failure")
16859
+ summary.failureCount += 1;
16860
+ if (outcome === "timeout")
16861
+ summary.timeoutCount += 1;
16862
+ if (outcome === "rate_limited")
16863
+ summary.rateLimitedCount += 1;
16864
+ if (outcome === "skipped")
16865
+ summary.skippedCount += 1;
16866
+ }
16867
+ function finalizeBreakdown(summary) {
16868
+ return {
16869
+ ...summary,
16870
+ successRate: summary.totalCount > 0 ? summary.successCount / summary.totalCount : null
16871
+ };
16872
+ }
16873
+ function summarizeOutcomesBy(outcomes, getKey) {
16874
+ const grouped = {};
16875
+ for (const outcome of outcomes) {
16876
+ const key = getKey(outcome);
16877
+ grouped[key] ??= emptyBreakdown();
16878
+ applyOutcome(grouped[key], outcome.outcome);
16879
+ }
16880
+ return Object.fromEntries(Object.entries(grouped).map(([key, summary]) => [key, finalizeBreakdown(summary)]));
16881
+ }
16882
+ function buildWindowSummary(projectPath, days) {
16883
+ const repo = getRepositories().sessionOutcomes;
16884
+ const toFinishedAt = Date.now();
16885
+ const fromFinishedAt = toFinishedAt - days * DAY_MS;
16886
+ const base = repo.querySummary({ projectPath, fromFinishedAt, toFinishedAt });
16887
+ const outcomes = repo.queryOutcomes({ projectPath, fromFinishedAt, toFinishedAt, limit: 500 });
16888
+ const byJobType = Object.fromEntries(getValidJobTypes().map((jobType) => {
16889
+ const summary = repo.querySummary({ projectPath, jobType, fromFinishedAt, toFinishedAt });
16890
+ return [
16891
+ jobType,
16892
+ finalizeBreakdown({
16893
+ totalCount: summary.totalCount,
16894
+ successCount: summary.successCount,
16895
+ failureCount: summary.failureCount,
16896
+ timeoutCount: summary.timeoutCount,
16897
+ rateLimitedCount: summary.rateLimitedCount,
16898
+ skippedCount: summary.skippedCount,
16899
+ successRate: null
16900
+ })
16901
+ ];
16902
+ }).filter(([, summary]) => summary.totalCount > 0));
16903
+ return {
16904
+ ...base,
16905
+ days,
16906
+ fromFinishedAt,
16907
+ toFinishedAt,
16908
+ successRate: base.totalCount > 0 ? base.successCount / base.totalCount : null,
16909
+ byJobType,
16910
+ byProvider: summarizeOutcomesBy(outcomes, (outcome) => outcome.providerKey)
16911
+ };
16912
+ }
16913
+ function getActiveAugmentations(projectPath) {
16914
+ return getRepositories().sessionOutcomes.listAugmentations({
16915
+ projectPath,
16916
+ status: "active",
16917
+ includeExpired: false,
16918
+ limit: 250
16919
+ });
16920
+ }
16921
+ function buildFailurePatterns(projectPath) {
16922
+ const outcomes = getRepositories().sessionOutcomes.queryOutcomes({
16923
+ projectPath,
16924
+ outcome: "failure",
16925
+ limit: 500
16926
+ });
16927
+ const patterns = /* @__PURE__ */ new Map();
16928
+ for (const outcome of outcomes) {
16929
+ const key = [
16930
+ outcome.jobType,
16931
+ outcome.providerKey,
16932
+ outcome.failureCategory ?? "uncategorized",
16933
+ outcome.failureSignature ?? "unknown"
16934
+ ].join(":");
16935
+ const current = patterns.get(key);
16936
+ if (current) {
16937
+ current.sampleCount += 1;
16938
+ current.lastSeenAt = Math.max(current.lastSeenAt, outcome.finishedAt);
16939
+ continue;
16940
+ }
16941
+ patterns.set(key, {
16942
+ key,
16943
+ jobType: outcome.jobType,
16944
+ providerKey: outcome.providerKey,
16945
+ category: outcome.failureCategory,
16946
+ signature: outcome.failureSignature,
16947
+ sampleCount: 1,
16948
+ lastSeenAt: outcome.finishedAt
16949
+ });
16950
+ }
16951
+ return [...patterns.values()].sort((a, b) => b.sampleCount - a.sampleCount || b.lastSeenAt - a.lastSeenAt).slice(0, 10);
16952
+ }
16953
+ function resolveAugmentationStatus(body) {
16954
+ if (body.action === "enable")
16955
+ return "active";
16956
+ if (body.action === "disable")
16957
+ return "paused";
16958
+ if (body.action === "expire")
16959
+ return "expired";
16960
+ if (body.enabled === true)
16961
+ return "active";
16962
+ if (body.enabled === false)
16963
+ return "paused";
16964
+ if (body.status && VALID_AUGMENTATION_STATUSES.includes(body.status))
16965
+ return body.status;
16966
+ return null;
16967
+ }
16968
+ function createFeedbackRouteHandlers(ctx) {
16969
+ const router = Router5({ mergeParams: true });
16970
+ const p = ctx.pathPrefix;
16971
+ router.get(`/${p}summary`, (req, res) => {
16972
+ try {
16973
+ const projectPath = ctx.getProjectDir(req);
16974
+ const response = {
16975
+ projectPath,
16976
+ windows: {
16977
+ last7Days: buildWindowSummary(projectPath, WINDOW_DAYS[0]),
16978
+ last30Days: buildWindowSummary(projectPath, WINDOW_DAYS[1])
16979
+ },
16980
+ activeAugmentations: getActiveAugmentations(projectPath)
16981
+ };
16982
+ res.json(response);
16983
+ } catch (error2) {
16984
+ res.status(500).json({ error: error2 instanceof Error ? error2.message : String(error2) });
16985
+ }
16986
+ });
16987
+ router.get(`/${p}patterns`, (req, res) => {
16988
+ try {
16989
+ const projectPath = ctx.getProjectDir(req);
16990
+ const response = {
16991
+ projectPath,
16992
+ patterns: getRepositories().sessionOutcomes.listPatterns({ projectPath, limit: 25 }),
16993
+ topFailurePatterns: buildFailurePatterns(projectPath)
16994
+ };
16995
+ res.json(response);
16996
+ } catch (error2) {
16997
+ res.status(500).json({ error: error2 instanceof Error ? error2.message : String(error2) });
16998
+ }
16999
+ });
17000
+ router.patch(`/${p}augmentations/:id`, (req, res) => {
17001
+ try {
17002
+ const id = parseInt(req.params.id, 10);
17003
+ if (!Number.isInteger(id) || id <= 0) {
17004
+ res.status(400).json({ error: "Invalid augmentation id" });
17005
+ return;
17006
+ }
17007
+ const status = resolveAugmentationStatus(req.body);
17008
+ if (!status) {
17009
+ res.status(400).json({ error: "Expected action, enabled, or status update" });
17010
+ return;
17011
+ }
17012
+ const projectPath = ctx.getProjectDir(req);
17013
+ const augmentation = getRepositories().sessionOutcomes.updateAugmentationStatus(id, status, projectPath);
17014
+ if (!augmentation) {
17015
+ res.status(404).json({ error: "Augmentation not found" });
17016
+ return;
17017
+ }
17018
+ res.json({ augmentation });
17019
+ } catch (error2) {
17020
+ res.status(500).json({ error: error2 instanceof Error ? error2.message : String(error2) });
17021
+ }
17022
+ });
17023
+ return router;
17024
+ }
17025
+ function createFeedbackRoutes(deps) {
17026
+ return createFeedbackRouteHandlers({
17027
+ getProjectDir: () => deps.projectDir,
17028
+ pathPrefix: ""
17029
+ });
17030
+ }
17031
+ function createProjectFeedbackRoutes() {
17032
+ return createFeedbackRouteHandlers({
17033
+ getProjectDir: (req) => req.projectDir,
17034
+ pathPrefix: "feedback/"
17035
+ });
17036
+ }
17037
+
15790
17038
  // ../server/dist/routes/job.routes.js
15791
17039
  init_dist();
15792
17040
  import { spawn as spawn7 } from "child_process";
15793
17041
  import { createHmac, randomUUID, timingSafeEqual } from "crypto";
15794
- import { Router as Router5 } from "express";
17042
+ import { Router as Router6 } from "express";
15795
17043
  var SUPPORTED_GITHUB_EVENTS = [
15796
17044
  "workflow_run",
15797
17045
  "check_suite",
@@ -15984,7 +17232,7 @@ function getLockPathForJob2(projectDir, jobId) {
15984
17232
  }
15985
17233
  }
15986
17234
  function createJobRouteHandlers(ctx) {
15987
- const router = Router5({ mergeParams: true });
17235
+ const router = Router6({ mergeParams: true });
15988
17236
  const p = ctx.pathPrefix;
15989
17237
  router.post(`/${p}:id/run`, (req, res) => {
15990
17238
  try {
@@ -16108,10 +17356,10 @@ function createProjectJobRoutes() {
16108
17356
  // ../server/dist/routes/log.routes.js
16109
17357
  init_dist();
16110
17358
  import * as path35 from "path";
16111
- import { Router as Router6 } from "express";
17359
+ import { Router as Router7 } from "express";
16112
17360
  function createLogRoutes(deps) {
16113
17361
  const { projectDir } = deps;
16114
- const router = Router6();
17362
+ const router = Router7();
16115
17363
  router.get("/:name", (req, res) => {
16116
17364
  try {
16117
17365
  const { name } = req.params;
@@ -16136,7 +17384,7 @@ function createLogRoutes(deps) {
16136
17384
  return router;
16137
17385
  }
16138
17386
  function createProjectLogRoutes() {
16139
- const router = Router6({ mergeParams: true });
17387
+ const router = Router7({ mergeParams: true });
16140
17388
  router.get("/logs/:name", (req, res) => {
16141
17389
  try {
16142
17390
  const projectDir = req.projectDir;
@@ -16163,9 +17411,9 @@ function createProjectLogRoutes() {
16163
17411
  }
16164
17412
 
16165
17413
  // ../server/dist/routes/prd.routes.js
16166
- import { Router as Router7 } from "express";
17414
+ import { Router as Router8 } from "express";
16167
17415
  function createPrdRoutes(_deps) {
16168
- const router = Router7();
17416
+ const router = Router8();
16169
17417
  router.get("/", (_req, res) => {
16170
17418
  res.status(410).json({ error: "PRDs endpoint deprecated - use GitHub Board instead" });
16171
17419
  });
@@ -16175,7 +17423,7 @@ function createPrdRoutes(_deps) {
16175
17423
  return router;
16176
17424
  }
16177
17425
  function createProjectPrdRoutes() {
16178
- const router = Router7({ mergeParams: true });
17426
+ const router = Router8({ mergeParams: true });
16179
17427
  router.get("/prds", (_req, res) => {
16180
17428
  res.status(410).json({ error: "PRDs endpoint deprecated - use GitHub Board instead" });
16181
17429
  });
@@ -16188,9 +17436,9 @@ function createProjectPrdRoutes() {
16188
17436
  // ../server/dist/routes/roadmap.routes.js
16189
17437
  init_dist();
16190
17438
  import * as path36 from "path";
16191
- import { Router as Router8 } from "express";
17439
+ import { Router as Router9 } from "express";
16192
17440
  function createRoadmapRouteHandlers(ctx) {
16193
- const router = Router8({ mergeParams: true });
17441
+ const router = Router9({ mergeParams: true });
16194
17442
  const p = ctx.pathPrefix;
16195
17443
  router.get(`/${p}`, (req, res) => {
16196
17444
  try {
@@ -16271,11 +17519,11 @@ function createProjectRoadmapRoutes() {
16271
17519
 
16272
17520
  // ../server/dist/routes/status.routes.js
16273
17521
  init_dist();
16274
- import { Router as Router9 } from "express";
17522
+ import { Router as Router10 } from "express";
16275
17523
  import { CronExpressionParser as CronExpressionParser2 } from "cron-parser";
16276
17524
  function createStatusRoutes(deps) {
16277
17525
  const { projectDir, getConfig, sseClients } = deps;
16278
- const router = Router9();
17526
+ const router = Router10();
16279
17527
  router.get("/events", (req, res) => {
16280
17528
  res.setHeader("Content-Type", "text/event-stream");
16281
17529
  res.setHeader("Cache-Control", "no-cache");
@@ -16404,7 +17652,7 @@ function buildScheduleInfoResponse(projectDir, config, entries, installed) {
16404
17652
  }
16405
17653
  function createScheduleInfoRoutes(deps) {
16406
17654
  const { projectDir, getConfig } = deps;
16407
- const router = Router9();
17655
+ const router = Router10();
16408
17656
  router.get("/", async (_req, res) => {
16409
17657
  try {
16410
17658
  const config = getConfig();
@@ -16418,7 +17666,7 @@ function createScheduleInfoRoutes(deps) {
16418
17666
  }
16419
17667
  function createProjectSseRoutes(deps) {
16420
17668
  const { projectSseClients, projectSseWatchers } = deps;
16421
- const router = Router9({ mergeParams: true });
17669
+ const router = Router10({ mergeParams: true });
16422
17670
  router.get("/status/events", (req, res) => {
16423
17671
  const projectDir = req.projectDir;
16424
17672
  if (!projectSseClients.has(projectDir)) {
@@ -16475,9 +17723,9 @@ data: ${JSON.stringify(snapshot)}
16475
17723
 
16476
17724
  // ../server/dist/routes/queue.routes.js
16477
17725
  init_dist();
16478
- import { Router as Router10 } from "express";
17726
+ import { Router as Router11 } from "express";
16479
17727
  function createGlobalQueueRoutes() {
16480
- const router = Router10();
17728
+ const router = Router11();
16481
17729
  router.get("/status", async (_req, res) => {
16482
17730
  try {
16483
17731
  const status = getQueueStatus();
@@ -16512,7 +17760,7 @@ function createGlobalQueueRoutes() {
16512
17760
  }
16513
17761
  function createQueueRoutes(deps) {
16514
17762
  const { getConfig } = deps;
16515
- const router = Router10();
17763
+ const router = Router11();
16516
17764
  router.get("/status", async (_req, res) => {
16517
17765
  try {
16518
17766
  const config = getConfig();
@@ -16622,6 +17870,7 @@ function createApp(projectDir) {
16622
17870
  app.use("/api/logs", createLogRoutes({ projectDir }));
16623
17871
  app.use("/api/doctor", createDoctorRoutes({ projectDir, getConfig: () => config }));
16624
17872
  app.use("/api/queue", createQueueRoutes({ getConfig: () => config }));
17873
+ app.use("/api/feedback", createFeedbackRoutes({ projectDir }));
16625
17874
  app.use("/api/global-notifications", createGlobalNotificationsRoutes());
16626
17875
  app.get("/api/prs", async (_req, res) => {
16627
17876
  try {
@@ -16669,6 +17918,7 @@ function createProjectRouter() {
16669
17918
  router.use(createProjectActionRoutes({ projectSseClients }));
16670
17919
  router.use(createProjectJobRoutes());
16671
17920
  router.use(createProjectRoadmapRoutes());
17921
+ router.use(createProjectFeedbackRoutes());
16672
17922
  router.get("/prs", async (req, res) => {
16673
17923
  try {
16674
17924
  res.json(await collectPrInfo(req.projectDir, req.projectConfig.branchPatterns));
@@ -17698,6 +18948,7 @@ function sliceCommand(program2) {
17698
18948
  }
17699
18949
  const spinner = createSpinner("Running Planner...");
17700
18950
  spinner.start();
18951
+ const startedAt = Date.now();
17701
18952
  try {
17702
18953
  await maybeApplyCronSchedulingDelay(config, "slicer", projectDir);
17703
18954
  const result = await sliceNextItem(projectDir, config);
@@ -17727,6 +18978,28 @@ function sliceCommand(program2) {
17727
18978
  }
17728
18979
  const nothingPending = result.error === "No pending items to process";
17729
18980
  const exitCode = result.sliced || nothingPending ? 0 : 1;
18981
+ if (!options.dryRun) {
18982
+ try {
18983
+ recordJobOutcome({
18984
+ config,
18985
+ exitCode,
18986
+ finishedAt: Date.now(),
18987
+ jobType: "planner",
18988
+ metadata: {
18989
+ error: result.error ?? null,
18990
+ file: result.file ?? null,
18991
+ itemTitle: result.item?.title ?? null,
18992
+ sliced: result.sliced
18993
+ },
18994
+ projectDir,
18995
+ providerKey: resolveJobProvider(config, "slicer"),
18996
+ startedAt,
18997
+ stderr: result.error,
18998
+ stdout: result.file ? `Created ${result.file}` : void 0
18999
+ });
19000
+ } catch {
19001
+ }
19002
+ }
17730
19003
  if (!options.dryRun && result.sliced) {
17731
19004
  await sendNotifications(config, {
17732
19005
  event: "run_succeeded",
@@ -17745,6 +19018,22 @@ function sliceCommand(program2) {
17745
19018
  }
17746
19019
  process.exit(exitCode);
17747
19020
  } catch (err) {
19021
+ try {
19022
+ recordJobOutcome({
19023
+ config,
19024
+ exitCode: 1,
19025
+ finishedAt: Date.now(),
19026
+ jobType: "planner",
19027
+ metadata: {
19028
+ error: err instanceof Error ? err.message : String(err)
19029
+ },
19030
+ projectDir,
19031
+ providerKey: resolveJobProvider(config, "slicer"),
19032
+ startedAt,
19033
+ stderr: err instanceof Error ? err.message : String(err)
19034
+ });
19035
+ } catch {
19036
+ }
17748
19037
  spinner.fail("Failed to execute planner command");
17749
19038
  error(`${err instanceof Error ? err.message : String(err)}`);
17750
19039
  process.exit(1);
@@ -18407,7 +19696,7 @@ import { spawn as spawn8 } from "child_process";
18407
19696
  import chalk7 from "chalk";
18408
19697
  import { Command as Command2 } from "commander";
18409
19698
  var logger6 = createLogger("queue");
18410
- var VALID_JOB_TYPES3 = [
19699
+ var VALID_JOB_TYPES2 = [
18411
19700
  "executor",
18412
19701
  "reviewer",
18413
19702
  "qa",
@@ -18515,18 +19804,18 @@ function createQueueCommand() {
18515
19804
  }
18516
19805
  });
18517
19806
  queue.command("clear").description("Clear pending jobs from the queue").option("--type <type>", "Only clear jobs of this type").option("--all", "Clear all entries including running (dangerous)").action((opts) => {
18518
- if (opts.type && !VALID_JOB_TYPES3.includes(opts.type)) {
19807
+ if (opts.type && !VALID_JOB_TYPES2.includes(opts.type)) {
18519
19808
  console.error(chalk7.red(`Invalid job type: ${opts.type}`));
18520
- console.error(chalk7.dim(`Valid types: ${VALID_JOB_TYPES3.join(", ")}`));
19809
+ console.error(chalk7.dim(`Valid types: ${VALID_JOB_TYPES2.join(", ")}`));
18521
19810
  process.exit(1);
18522
19811
  }
18523
19812
  const count = clearQueue(opts.type);
18524
19813
  console.log(chalk7.green(`Cleared ${count} pending job(s) from the queue.`));
18525
19814
  });
18526
19815
  queue.command("enqueue <job-type> <project-dir>").description("Manually enqueue a job").option("--env <json>", "JSON object of environment variables to store", "{}").option("--provider-key <key>", "Provider bucket key (e.g. claude-native, codex)").action((jobType, projectDir, opts) => {
18527
- if (!VALID_JOB_TYPES3.includes(jobType)) {
19816
+ if (!VALID_JOB_TYPES2.includes(jobType)) {
18528
19817
  console.error(chalk7.red(`Invalid job type: ${jobType}`));
18529
- console.error(chalk7.dim(`Valid types: ${VALID_JOB_TYPES3.join(", ")}`));
19818
+ console.error(chalk7.dim(`Valid types: ${VALID_JOB_TYPES2.join(", ")}`));
18530
19819
  process.exit(1);
18531
19820
  }
18532
19821
  let envVars = {};
@@ -18630,9 +19919,9 @@ function createQueueCommand() {
18630
19919
  queue.command("claim <job-type> <project-dir>").description(
18631
19920
  "Atomically claim a concurrency slot and insert a running entry (used by cron scripts)"
18632
19921
  ).option("--provider-key <key>", "Provider bucket key (e.g. claude-native, codex)").option("--pid <pid>", "PID of the calling process (stored for stale-job detection)").action((jobType, projectDir, opts) => {
18633
- if (!VALID_JOB_TYPES3.includes(jobType)) {
19922
+ if (!VALID_JOB_TYPES2.includes(jobType)) {
18634
19923
  console.error(`Invalid job type: ${jobType}`);
18635
- console.error(`Valid types: ${VALID_JOB_TYPES3.join(", ")}`);
19924
+ console.error(`Valid types: ${VALID_JOB_TYPES2.join(", ")}`);
18636
19925
  process.exit(1);
18637
19926
  }
18638
19927
  const queueConfig = loadConfig(projectDir).queue;
@@ -19039,12 +20328,14 @@ function resolveCommand(program2) {
19039
20328
  const spinner = createSpinner("Running PR resolver...");
19040
20329
  spinner.start();
19041
20330
  try {
20331
+ const startedAt = Date.now();
19042
20332
  await maybeApplyCronSchedulingDelay(config, "pr-resolver", projectDir);
19043
20333
  const { exitCode, stdout, stderr } = await executeScriptWithOutput(
19044
20334
  scriptPath,
19045
20335
  [projectDir],
19046
20336
  envVars
19047
20337
  );
20338
+ const finishedAt = Date.now();
19048
20339
  const scriptResult = parseScriptResult(`${stdout}
19049
20340
  ${stderr}`);
19050
20341
  if (exitCode === 0) {
@@ -19059,6 +20350,27 @@ ${stderr}`);
19059
20350
  spinner.fail(`PR resolver exited with code ${exitCode}`);
19060
20351
  }
19061
20352
  const notificationEvent = exitCode === 0 ? "pr_resolver_completed" : "pr_resolver_failed";
20353
+ if (!options.dryRun) {
20354
+ try {
20355
+ recordJobOutcome({
20356
+ config,
20357
+ exitCode,
20358
+ finishedAt,
20359
+ jobType: "pr-resolver",
20360
+ metadata: {
20361
+ providerCommand: envVars.NW_PROVIDER_CMD,
20362
+ providerLabel: envVars.NW_PROVIDER_LABEL
20363
+ },
20364
+ projectDir,
20365
+ providerKey: envVars.NW_PROVIDER_KEY ?? resolveJobProvider(config, "pr-resolver"),
20366
+ scriptResult,
20367
+ startedAt,
20368
+ stderr,
20369
+ stdout
20370
+ });
20371
+ } catch {
20372
+ }
20373
+ }
19062
20374
  await sendNotifications(config, {
19063
20375
  event: notificationEvent,
19064
20376
  projectName: path45.basename(projectDir),
@@ -19157,12 +20469,14 @@ function mergeCommand(program2) {
19157
20469
  const spinner = createSpinner("Running merge orchestrator...");
19158
20470
  spinner.start();
19159
20471
  try {
20472
+ const startedAt = Date.now();
19160
20473
  await maybeApplyCronSchedulingDelay(config, "merger", projectDir);
19161
20474
  const { exitCode, stdout, stderr } = await executeScriptWithOutput(
19162
20475
  scriptPath,
19163
20476
  [projectDir],
19164
20477
  envVars
19165
20478
  );
20479
+ const finishedAt = Date.now();
19166
20480
  const scriptResult = parseScriptResult(`${stdout}
19167
20481
  ${stderr}`);
19168
20482
  if (exitCode === 0) {
@@ -19179,6 +20493,30 @@ ${stderr}`);
19179
20493
  const mergedCount = parseInt(scriptResult?.data?.merged ?? "0", 10);
19180
20494
  const failedCount = parseInt(scriptResult?.data?.failed ?? "0", 10);
19181
20495
  const notificationEvent = resolveMergeNotificationEvent(exitCode, mergedCount, failedCount);
20496
+ if (!options.dryRun) {
20497
+ try {
20498
+ recordJobOutcome({
20499
+ config,
20500
+ exitCode,
20501
+ finishedAt,
20502
+ jobType: "merger",
20503
+ metadata: {
20504
+ failedCount,
20505
+ mergedCount,
20506
+ providerCommand: envVars.NW_PROVIDER_CMD,
20507
+ providerLabel: envVars.NW_PROVIDER_LABEL
20508
+ },
20509
+ minReviewScore: config.merger.minReviewScore,
20510
+ projectDir,
20511
+ providerKey: envVars.NW_PROVIDER_KEY ?? resolveJobProvider(config, "merger"),
20512
+ scriptResult,
20513
+ startedAt,
20514
+ stderr,
20515
+ stdout
20516
+ });
20517
+ } catch {
20518
+ }
20519
+ }
19182
20520
  if (notificationEvent) {
19183
20521
  await sendNotifications(config, {
19184
20522
  event: notificationEvent,