@magnusekdahl/parallix 1.3.2 → 1.3.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 (50) hide show
  1. package/docs/agents.md +1 -1
  2. package/docs/use-cases.md +1 -1
  3. package/lib/agents/agents.js +20 -3
  4. package/lib/agents/agents.ts +14 -3
  5. package/lib/agents/mistral-telemetry.js +122 -23
  6. package/lib/agents/mistral-telemetry.ts +141 -26
  7. package/lib/agents/mistral.js +128 -14
  8. package/lib/agents/mistral.ts +145 -5
  9. package/lib/commands/active.js +69 -39
  10. package/lib/commands/active.ts +97 -60
  11. package/lib/commands/config.ts +3 -3
  12. package/lib/commands/coverage-gate.ts +1 -1
  13. package/lib/commands/draft.js +1 -1
  14. package/lib/commands/draft.ts +1 -1
  15. package/lib/commands/handoff.js +13 -8
  16. package/lib/commands/handoff.ts +24 -18
  17. package/lib/commands/rebase.js +1 -1
  18. package/lib/commands/rebase.ts +2 -2
  19. package/lib/commands/repair-handoff.js +141 -20
  20. package/lib/commands/repair-handoff.ts +185 -45
  21. package/lib/commands/resolve-conflict.js +1 -1
  22. package/lib/commands/resolve-conflict.ts +2 -2
  23. package/lib/commands/stats-backfill.ts +10 -10
  24. package/lib/commands/stats.js +38 -11
  25. package/lib/commands/stats.ts +40 -96
  26. package/lib/core/fmt.ts +2 -2
  27. package/lib/core/git.ts +2 -2
  28. package/lib/core/gitignore.ts +2 -2
  29. package/lib/core/mission-utils.js +2 -2
  30. package/lib/core/mission-utils.ts +2 -2
  31. package/lib/core/persistent-data-migration.ts +2 -2
  32. package/lib/core/spawn-tee.ts +1 -1
  33. package/lib/core/state-map.ts +2 -2
  34. package/lib/core/storage.ts +1 -1
  35. package/lib/core/verification.ts +1 -1
  36. package/lib/review/rebase.ts +12 -12
  37. package/lib/review/review-artifacts.ts +35 -35
  38. package/lib/review/review-commands.ts +40 -40
  39. package/lib/review/review-events.ts +12 -12
  40. package/lib/review/review-loop.js +236 -7
  41. package/lib/review/review-loop.ts +338 -22
  42. package/lib/review/review-polling.ts +6 -6
  43. package/lib/review/review-prompts.js +8 -4
  44. package/lib/review/review-prompts.ts +12 -8
  45. package/lib/review/review-state.js +1 -1
  46. package/lib/review/review-state.ts +2 -2
  47. package/package.json +3 -2
  48. package/prompts/review-verbose.md +1 -1
  49. package/prompts/review.md +1 -1
  50. package/px.js +8 -4
@@ -12,13 +12,13 @@ import {
12
12
  import { findMissionDir } from '../core/mission-utils.js';
13
13
 
14
14
  interface StatsAugmented {
15
- resolveMissionClassification: (slug: string, rootDir?: string) => { classification?: string; source?: string };
16
- _internals: Record<string, (...args: unknown[]) => unknown>;
17
- resolveStatsPath: (options?: { ensureDir?: boolean }) => string;
18
- resolveStatsRepoName: (rootDir: string) => string;
19
- loadStatsCsv: (filePath: string, options?: { rootDir?: string }) => { rows: Record<string, string>[] };
20
- deriveImplementerAndFixRounds: (slug: string, rootDir?: string) => { implementer: string; prFixRounds: number; source: string };
21
- upsertStatsRow: (row: Record<string, string>, options: { filePath: string; rootDir?: string }) => { changed: boolean };
15
+ resolveMissionClassification: (_slug: string, _rootDir?: string) => { classification?: string; source?: string };
16
+ _internals: Record<string, (..._args: unknown[]) => unknown>;
17
+ resolveStatsPath: (_options?: { ensureDir?: boolean }) => string;
18
+ resolveStatsRepoName: (_rootDir: string) => string;
19
+ loadStatsCsv: (_filePath: string, _options?: { rootDir?: string }) => { rows: Record<string, string>[] };
20
+ deriveImplementerAndFixRounds: (_slug: string, _rootDir?: string) => { implementer: string; prFixRounds: number; source: string };
21
+ upsertStatsRow: (_row: Record<string, string>, _options: { filePath: string; rootDir?: string }) => { changed: boolean };
22
22
  }
23
23
 
24
24
  function getStats(): StatsAugmented {
@@ -175,7 +175,7 @@ function resolveHistoricalClassification(slug: string, taskFile: string, rootDir
175
175
  }
176
176
  // Classification missing or invalid — fall through to fallbacks.
177
177
  const classificationValue = getTaskFrontmatterValue(taskFile, 'classification');
178
- const legacy = (s._internals as Record<string, (v: string) => string | null>).normalizeClassification(classificationValue ?? '');
178
+ const legacy = (s._internals as Record<string, (_v: string) => string | null>).normalizeClassification(classificationValue ?? '');
179
179
  if (legacy) {
180
180
  return { value: legacy, source: 'backlog-classification' };
181
181
  }
@@ -347,8 +347,8 @@ Notes:
347
347
  }
348
348
 
349
349
  interface BackfillOptions {
350
- log?: (msg: string) => string | null;
351
- error?: (msg: string) => string | null;
350
+ log?: (_msg: string) => string | null;
351
+ error?: (_msg: string) => string | null;
352
352
  rootDir?: string;
353
353
  }
354
354
 
@@ -76,16 +76,19 @@ const storage = __importStar(require("../core/storage.js"));
76
76
  // and one-time header migration of legacy stats files (task-1251).
77
77
  const LEGACY_HEADERS = ['date', 'mission', 'classification', 'implementer', 'pr_fix_rounds'];
78
78
  exports.LEGACY_HEADERS = LEGACY_HEADERS;
79
- // Extended 21-column telemetry schema (task-1314 + task-1251). Legacy 5-column rows are
80
- // migrated in-memory on load: the legacy columns are preserved and the new
81
- // columns default to '' (text) or '0' (numeric). On the next write the file
82
- // header is upgraded and existing rows gain the new columns.
79
+ // Extended 22-column telemetry schema (task-1314 + task-1251 + task-1380). Legacy
80
+ // 5-column rows are migrated in-memory on load: the legacy columns are preserved
81
+ // and the new columns default to '' (text) or '0' (numeric). On the next write the
82
+ // file header is upgraded and existing rows gain the new columns. The `closed`
83
+ // column (task-1380) stores 'yes' for closed/integrated missions and is empty for
84
+ // in-progress stage rows; filtering by `closed === 'yes'` excludes in-progress
85
+ // missions from weekly and range mission counts.
83
86
  const STATS_HEADERS = [
84
87
  'date', 'repo', 'mission', 'classification', 'implementer', 'pr_fix_rounds',
85
88
  'provider', 'model', 'implementer_agent', 'reviewer_agent', 'stage',
86
89
  'input_tokens', 'output_tokens', 'cached_tokens', 'context_tokens',
87
90
  'tool_calls', 'openai_usage_before', 'openai_usage_after',
88
- 'openai_usage_delta', 'duration_minutes', 'cost_usd'
91
+ 'openai_usage_delta', 'duration_minutes', 'cost_usd', 'closed'
89
92
  ];
90
93
  exports.STATS_HEADERS = STATS_HEADERS;
91
94
  // Columns coerced to non-negative integers on canonicalization.
@@ -271,16 +274,30 @@ function loadStatsCsv(filePath = null, options = {}) {
271
274
  if (data.headers.length === 0) {
272
275
  return { headers: [...STATS_HEADERS], rows: [] };
273
276
  }
277
+ // Detect whether the loaded CSV already has the `closed` column (task-1380).
278
+ // Legacy CSVs (pre-closed) lack the column; their rows represent completed
279
+ // missions written at integration time, so default `closed` to 'yes' for
280
+ // backward compatibility. Modern CSVs already have the column set per-row.
281
+ const hasClosedColumn = data.headers.includes('closed');
282
+ const migratedRows = data.rows.map((row) => {
283
+ const normalized = normalizeStatsRow(row, { rootDir: options.rootDir });
284
+ if (!hasClosedColumn) {
285
+ // Legacy CSV: all rows are from integration time, treat as closed.
286
+ return { ...normalized, closed: 'yes' };
287
+ }
288
+ return { ...normalized, closed: row.closed || '' };
289
+ });
274
290
  return {
275
291
  headers: [...STATS_HEADERS],
276
- rows: data.rows.map((row) => normalizeStatsRow(row, { rootDir: options.rootDir })),
292
+ rows: migratedRows,
277
293
  };
278
294
  }
279
295
  /**
280
296
  * Map any row (legacy 5-column or full 21-column) to the full schema, defaulting
281
297
  * missing text columns to '' and numeric columns to '0'. `stage` defaults to
282
298
  * 'default' so legacy rows and integration rows share the (repo, mission, stage)
283
- * upsert key.
299
+ * upsert key. `closed` defaults to '' (unset) — the backward-compat default of
300
+ * 'yes' for legacy CSV rows is applied exclusively in `loadStatsCsv`.
284
301
  */
285
302
  function normalizeStatsRow(row = {}, options = {}) {
286
303
  const repo = String(row.repo || options.repo || resolveStatsRepoName(options.rootDir)).trim();
@@ -306,6 +323,7 @@ function normalizeStatsRow(row = {}, options = {}) {
306
323
  openai_usage_delta: row.openai_usage_delta || '0',
307
324
  duration_minutes: row.duration_minutes || '0',
308
325
  cost_usd: row.cost_usd || '0',
326
+ closed: row.closed || '',
309
327
  };
310
328
  }
311
329
  /**
@@ -695,11 +713,14 @@ function rowInWindow(row, window) {
695
713
  */
696
714
  function summarizeMissionWindow(rows, window) {
697
715
  const windowRows = rows.filter(row => rowInWindow(row, window));
716
+ // Filter to only closed missions (task-1380): rows without closed:'yes' are
717
+ // in-progress stage rows and should not inflate mission counts.
718
+ const closedRows = windowRows.filter(row => row.closed === 'yes');
698
719
  // Deduplicate by mission so multi-stage telemetry rows don't inflate counts.
699
720
  // One row per unique repo+mission pair is kept (first occurrence is sufficient
700
721
  // since classification is stable across stages for the same mission in a repo).
701
722
  const seenMissions = new Set();
702
- const uniqueMissions = windowRows.filter(row => {
723
+ const uniqueMissions = closedRows.filter(row => {
703
724
  const key = statsMissionKey(row);
704
725
  if (seenMissions.has(key)) {
705
726
  return false;
@@ -712,7 +733,7 @@ function summarizeMissionWindow(rows, window) {
712
733
  const unknown = uniqueMissions.filter(row => normalizeClassification(row.classification) === 'unknown').length;
713
734
  const validMissions = uniqueMissions.filter(row => normalizeClassification(row.classification) !== null);
714
735
  return {
715
- rows: windowRows,
736
+ rows: closedRows,
716
737
  total: validMissions.length,
717
738
  userValue,
718
739
  aiSdlc,
@@ -760,10 +781,12 @@ function summarizeAgentWindow(rows, window, options = {}) {
760
781
  const opts = options;
761
782
  const { rootDir = null, deriveFixRoundsFn = deriveFixRoundsLocalAuthoritative } = opts;
762
783
  const windowRows = rows.filter(row => rowInWindow(row, window));
784
+ // Filter to only closed missions (task-1380).
785
+ const closedWindowRows = windowRows.filter(row => row.closed === 'yes');
763
786
  // Only count missions with a valid classification so the agent table totals
764
787
  // align with the mission-count table (which also excludes null/invalid
765
788
  // classifications via summarizeMissionWindow → validMissions).
766
- const validWindowRows = windowRows.filter(row => normalizeClassification(row.classification) !== null);
789
+ const validWindowRows = closedWindowRows.filter(row => normalizeClassification(row.classification) !== null);
767
790
  // Deduplicate globally by (repo, mission) first so each mission is counted
768
791
  // exactly once across all agent groups — matching the mission-count table.
769
792
  // Prefer the row where model === implementer (the implementer's own model),
@@ -988,7 +1011,8 @@ function renderMissionPhaseReport(rows, slug, options = {}) {
988
1011
  const opts = options;
989
1012
  const wantedRepo = String(opts.repo || resolveStatsRepoName(opts.rootDir)).trim();
990
1013
  const missionRows = (rows || []).filter(row => String(row.mission || '').trim().toLowerCase() === wanted &&
991
- String(row.repo || '').trim() === wantedRepo);
1014
+ String(row.repo || '').trim() === wantedRepo &&
1015
+ row.closed === 'yes');
992
1016
  const byStage = new Map();
993
1017
  for (const row of missionRows) {
994
1018
  const stage = String(row.stage || 'default').trim().toLowerCase() || 'default';
@@ -1438,6 +1462,7 @@ function canonicalizeStatsRow(row, options = {}) {
1438
1462
  classification: /** @type{string|number|boolean|undefined} */ (normalizeClassification(row.classification)),
1439
1463
  implementer: /** @type{string|number|boolean|undefined} */ (normalizeImplementer(row.implementer)),
1440
1464
  stage: String(row.stage || '').trim().toLowerCase() || 'default',
1465
+ closed: row.closed || '',
1441
1466
  };
1442
1467
  for (const key of USAGE_NUMBERS) {
1443
1468
  canonical[key] = String(Math.max(0, Number.parseInt(String(/** @type{any} */ (normalized)[key]), 10) || 0));
@@ -1516,6 +1541,7 @@ function recordIntegrationStats(options = {}) {
1516
1541
  classification,
1517
1542
  implementer: implementerInfo.implementer,
1518
1543
  pr_fix_rounds: implementerInfo.prFixRounds,
1544
+ closed: 'yes',
1519
1545
  }, { filePath, rootDir });
1520
1546
  return {
1521
1547
  ...result,
@@ -1982,6 +2008,7 @@ stats._internals = {
1982
2008
  deriveFinalImplementerFromBranchHistory,
1983
2009
  deriveImplementerAndFixRoundsFromPrComments,
1984
2010
  deriveImplementerAndFixRounds,
2011
+ summarizeMissionWindow,
1985
2012
  summarizeAgentWindow,
1986
2013
  colorAverageFixRounds,
1987
2014
  colorMissionCounts,
@@ -28,11 +28,6 @@ interface StatsOptions {
28
28
  exit?: Function;
29
29
  }
30
30
 
31
- interface CsvData {
32
- headers: string[];
33
- rows: Record<string, string>[];
34
- }
35
-
36
31
  interface NormalizeStatsRowOptions {
37
32
  repo?: string;
38
33
  rootDir?: string;
@@ -42,67 +37,6 @@ interface LoadStatsCsvOptions {
42
37
  rootDir?: string;
43
38
  }
44
39
 
45
- interface TelemetryToStatsOptions {
46
- agentFamily: string;
47
- durationMinutes?: number;
48
- model?: string;
49
- }
50
-
51
- interface UpsertStatsRowOptions {
52
- filePath?: string;
53
- rootDir?: string;
54
- }
55
-
56
- interface RecordStageStatsOptions {
57
- slug: string;
58
- stage: string;
59
- rootDir?: string;
60
- filePath?: string;
61
- date?: string;
62
- implementer?: string;
63
- reviewer?: string;
64
- prFixRounds?: string;
65
- telemetry?: unknown;
66
- durationMinutes?: number;
67
- model?: string | null;
68
- }
69
-
70
- interface RecordIntegrationStatsOptions {
71
- slug: string;
72
- rootDir?: string;
73
- filePath?: string;
74
- date?: string;
75
- }
76
-
77
- interface RecordActiveStatsOptions {
78
- stage?: string;
79
- slug: string;
80
- rootDir?: string;
81
- prFixRounds?: string;
82
- model?: string;
83
- }
84
-
85
- interface RecordReviewStatsOptions {
86
- stage?: string;
87
- slug: string;
88
- rootDir?: string;
89
- reviewer?: string;
90
- implementer?: string;
91
- prFixRounds?: string;
92
- model?: string;
93
- }
94
-
95
- interface RenderWeeklyStatsReportOptions {
96
- today?: Date | string;
97
- rootDir?: string | null;
98
- }
99
-
100
- interface RenderRangeStatsReportOptions {
101
- rootDir?: string | null;
102
- from?: string;
103
- to?: string;
104
- }
105
-
106
40
  interface StatsRow {
107
41
  date?: string;
108
42
  repo?: string;
@@ -137,16 +71,6 @@ interface StatsRow {
137
71
  missions?: number;
138
72
  }
139
73
 
140
- interface MissionStats {
141
- implementer: string;
142
- missions: number;
143
- averageFixRounds: string;
144
- }
145
-
146
- interface AccModeOptions {
147
- mode?: 'sum' | 'max' | 'replace';
148
- }
149
-
150
74
  interface StatsCsvPathOptions {
151
75
  filePath?: string;
152
76
  rootDir?: string;
@@ -154,14 +78,6 @@ interface StatsCsvPathOptions {
154
78
  forWrite?: boolean;
155
79
  }
156
80
 
157
- interface StatsCmdOptions {
158
- log?: Function;
159
- error?: Function;
160
- exit?: Function;
161
- rootDir?: string;
162
- }
163
-
164
-
165
81
  import * as fs from 'node:fs';
166
82
  import * as path from 'node:path';
167
83
 
@@ -172,7 +88,7 @@ import { readReviewState } from '../review/review-state.js';
172
88
  import * as reviewEvents from '../review/review-events.js';
173
89
  import { git } from '../core/git.js';
174
90
  import { migrateStats } from '../core/persistent-data-migration.js';
175
- import { getPrimaryWorktree, findMissionDir } from '../core/mission-utils.js';
91
+ import { findMissionDir } from '../core/mission-utils.js';
176
92
  import * as forgejo from '../tools/forgejo.js';
177
93
  import * as storage from '../core/storage.js';
178
94
 
@@ -180,16 +96,19 @@ import * as storage from '../core/storage.js';
180
96
  // and one-time header migration of legacy stats files (task-1251).
181
97
  const LEGACY_HEADERS = ['date', 'mission', 'classification', 'implementer', 'pr_fix_rounds'];
182
98
 
183
- // Extended 21-column telemetry schema (task-1314 + task-1251). Legacy 5-column rows are
184
- // migrated in-memory on load: the legacy columns are preserved and the new
185
- // columns default to '' (text) or '0' (numeric). On the next write the file
186
- // header is upgraded and existing rows gain the new columns.
99
+ // Extended 22-column telemetry schema (task-1314 + task-1251 + task-1380). Legacy
100
+ // 5-column rows are migrated in-memory on load: the legacy columns are preserved
101
+ // and the new columns default to '' (text) or '0' (numeric). On the next write the
102
+ // file header is upgraded and existing rows gain the new columns. The `closed`
103
+ // column (task-1380) stores 'yes' for closed/integrated missions and is empty for
104
+ // in-progress stage rows; filtering by `closed === 'yes'` excludes in-progress
105
+ // missions from weekly and range mission counts.
187
106
  const STATS_HEADERS = [
188
107
  'date', 'repo', 'mission', 'classification', 'implementer', 'pr_fix_rounds',
189
108
  'provider', 'model', 'implementer_agent', 'reviewer_agent', 'stage',
190
109
  'input_tokens', 'output_tokens', 'cached_tokens', 'context_tokens',
191
110
  'tool_calls', 'openai_usage_before', 'openai_usage_after',
192
- 'openai_usage_delta', 'duration_minutes', 'cost_usd'
111
+ 'openai_usage_delta', 'duration_minutes', 'cost_usd', 'closed'
193
112
  ];
194
113
 
195
114
  // Columns coerced to non-negative integers on canonicalization.
@@ -382,9 +301,23 @@ function loadStatsCsv(filePath: string | null = null, options: LoadStatsCsvOptio
382
301
  return { headers: [...STATS_HEADERS], rows: [] };
383
302
  }
384
303
 
304
+ // Detect whether the loaded CSV already has the `closed` column (task-1380).
305
+ // Legacy CSVs (pre-closed) lack the column; their rows represent completed
306
+ // missions written at integration time, so default `closed` to 'yes' for
307
+ // backward compatibility. Modern CSVs already have the column set per-row.
308
+ const hasClosedColumn = data.headers.includes('closed');
309
+ const migratedRows = data.rows.map((row: Record<string, string>) => {
310
+ const normalized = normalizeStatsRow(row, { rootDir: options.rootDir });
311
+ if (!hasClosedColumn) {
312
+ // Legacy CSV: all rows are from integration time, treat as closed.
313
+ return { ...normalized, closed: 'yes' };
314
+ }
315
+ return { ...normalized, closed: row.closed || '' };
316
+ });
317
+
385
318
  return {
386
319
  headers: [...STATS_HEADERS],
387
- rows: data.rows.map((row: Record<string, string>) => normalizeStatsRow(row, { rootDir: options.rootDir })),
320
+ rows: migratedRows,
388
321
  };
389
322
  }
390
323
 
@@ -393,7 +326,8 @@ function loadStatsCsv(filePath: string | null = null, options: LoadStatsCsvOptio
393
326
  * Map any row (legacy 5-column or full 21-column) to the full schema, defaulting
394
327
  * missing text columns to '' and numeric columns to '0'. `stage` defaults to
395
328
  * 'default' so legacy rows and integration rows share the (repo, mission, stage)
396
- * upsert key.
329
+ * upsert key. `closed` defaults to '' (unset) — the backward-compat default of
330
+ * 'yes' for legacy CSV rows is applied exclusively in `loadStatsCsv`.
397
331
  */
398
332
  function normalizeStatsRow(row: StatsRow = {} as StatsRow, options: NormalizeStatsRowOptions = {} as NormalizeStatsRowOptions) {
399
333
  const repo = String(row.repo || options.repo || resolveStatsRepoName(options.rootDir)).trim();
@@ -419,6 +353,7 @@ function normalizeStatsRow(row: StatsRow = {} as StatsRow, options: NormalizeSta
419
353
  openai_usage_delta: row.openai_usage_delta || '0',
420
354
  duration_minutes: row.duration_minutes || '0',
421
355
  cost_usd: row.cost_usd || '0',
356
+ closed: row.closed || '',
422
357
  };
423
358
  }
424
359
 
@@ -819,11 +754,14 @@ function rowInWindow(row, window) {
819
754
  */
820
755
  function summarizeMissionWindow(rows, window) {
821
756
  const windowRows = rows.filter(row => rowInWindow(row, window));
757
+ // Filter to only closed missions (task-1380): rows without closed:'yes' are
758
+ // in-progress stage rows and should not inflate mission counts.
759
+ const closedRows = windowRows.filter(row => row.closed === 'yes');
822
760
  // Deduplicate by mission so multi-stage telemetry rows don't inflate counts.
823
761
  // One row per unique repo+mission pair is kept (first occurrence is sufficient
824
762
  // since classification is stable across stages for the same mission in a repo).
825
763
  const seenMissions = new Set();
826
- const uniqueMissions = windowRows.filter(row => {
764
+ const uniqueMissions = closedRows.filter(row => {
827
765
  const key = statsMissionKey(row);
828
766
  if (seenMissions.has(key)) {return false;}
829
767
  seenMissions.add(key);
@@ -834,7 +772,7 @@ function summarizeMissionWindow(rows, window) {
834
772
  const unknown = uniqueMissions.filter(row => normalizeClassification(row.classification) === 'unknown').length;
835
773
  const validMissions = uniqueMissions.filter(row => normalizeClassification(row.classification) !== null);
836
774
  return {
837
- rows: windowRows,
775
+ rows: closedRows,
838
776
  total: validMissions.length,
839
777
  userValue,
840
778
  aiSdlc,
@@ -882,10 +820,12 @@ function summarizeAgentWindow(rows, window, options = {}) {
882
820
  const opts = options;
883
821
  const { rootDir = null, deriveFixRoundsFn = deriveFixRoundsLocalAuthoritative } = opts;
884
822
  const windowRows = rows.filter(row => rowInWindow(row, window));
823
+ // Filter to only closed missions (task-1380).
824
+ const closedWindowRows = windowRows.filter(row => row.closed === 'yes');
885
825
  // Only count missions with a valid classification so the agent table totals
886
826
  // align with the mission-count table (which also excludes null/invalid
887
827
  // classifications via summarizeMissionWindow → validMissions).
888
- const validWindowRows = windowRows.filter(row => normalizeClassification(row.classification) !== null);
828
+ const validWindowRows = closedWindowRows.filter(row => normalizeClassification(row.classification) !== null);
889
829
  // Deduplicate globally by (repo, mission) first so each mission is counted
890
830
  // exactly once across all agent groups — matching the mission-count table.
891
831
  // Prefer the row where model === implementer (the implementer's own model),
@@ -1139,7 +1079,8 @@ function renderMissionPhaseReport(rows, slug, options = {}) {
1139
1079
  const wantedRepo = String(opts.repo || resolveStatsRepoName(opts.rootDir)).trim();
1140
1080
  const missionRows = (rows || []).filter(row =>
1141
1081
  String(row.mission || '').trim().toLowerCase() === wanted &&
1142
- String(row.repo || '').trim() === wantedRepo
1082
+ String(row.repo || '').trim() === wantedRepo &&
1083
+ row.closed === 'yes'
1143
1084
  );
1144
1085
 
1145
1086
  const byStage = new Map();
@@ -1620,6 +1561,7 @@ function canonicalizeStatsRow(row, options = {}) {
1620
1561
  classification: /** @type{string|number|boolean|undefined} */(normalizeClassification(row.classification)),
1621
1562
  implementer: /** @type{string|number|boolean|undefined} */(normalizeImplementer(row.implementer)),
1622
1563
  stage: String(row.stage || '').trim().toLowerCase() || 'default',
1564
+ closed: row.closed || '',
1623
1565
  };
1624
1566
  for (const key of USAGE_NUMBERS) {
1625
1567
  canonical[key] = String(Math.max(0, Number.parseInt(String(/** @type{any} */(normalized)[key]), 10) || 0));
@@ -1709,6 +1651,7 @@ function recordIntegrationStats(options = {}) {
1709
1651
  classification,
1710
1652
  implementer: implementerInfo.implementer,
1711
1653
  pr_fix_rounds: implementerInfo.prFixRounds,
1654
+ closed: 'yes',
1712
1655
  }, { filePath, rootDir });
1713
1656
 
1714
1657
  return {
@@ -2184,6 +2127,7 @@ if (typeof module !== 'undefined') { module.exports = stats; }
2184
2127
  deriveFinalImplementerFromBranchHistory,
2185
2128
  deriveImplementerAndFixRoundsFromPrComments,
2186
2129
  deriveImplementerAndFixRounds,
2130
+ summarizeMissionWindow,
2187
2131
  summarizeAgentWindow,
2188
2132
  colorAverageFixRounds,
2189
2133
  colorMissionCounts,
package/lib/core/fmt.ts CHANGED
@@ -98,8 +98,8 @@ export function branch(text: string): string { return colorize('magenta', text);
98
98
  export function sha(text: string): string { return colorize('yellow', text); }
99
99
  export function command(text: string): string { return colorize('green', text); }
100
100
 
101
- type LogFn = (...args: unknown[]) => void;
102
- type LogFunc = (text: string) => string | null;
101
+ type LogFn = (..._args: unknown[]) => void;
102
+ type LogFunc = (_text: string) => string | null;
103
103
 
104
104
  type Logger = {
105
105
  log: LogFn;
package/lib/core/git.ts CHANGED
@@ -1,4 +1,4 @@
1
- import childProcess, { spawnSync } from 'node:child_process';
1
+ import { spawnSync } from 'node:child_process';
2
2
  import type { SpawnSyncOptions } from 'node:child_process';
3
3
  import * as fsMod from 'node:fs';
4
4
  import * as pathMod from 'node:path';
@@ -90,7 +90,7 @@ interface RebaseStateResult {
90
90
  }
91
91
 
92
92
  interface GitRunner {
93
- (args: string[]): GitResult;
93
+ (_args: string[]): GitResult;
94
94
  }
95
95
 
96
96
  interface DetectRebaseOptions {
@@ -24,11 +24,11 @@ interface EnsureOptions {
24
24
  lstatSyncFn?: typeof fs.lstatSync;
25
25
  readFileSyncFn?: typeof fs.readFileSync;
26
26
  writeFileSyncFn?: typeof fs.writeFileSync;
27
- logFn?: (msg: string) => void;
27
+ logFn?: (_msg: string) => void;
28
28
  }
29
29
 
30
30
  type EnsureWorkflowGitignoreFn = {
31
- (rootDir: string, options?: EnsureOptions): GitignoreResult;
31
+ (_rootDir: string, _options?: EnsureOptions): GitignoreResult;
32
32
  WORKFLOW_ENTRIES: typeof WORKFLOW_ENTRIES;
33
33
  ensureWorkflowGitignore: EnsureWorkflowGitignoreFn;
34
34
  };
@@ -986,7 +986,7 @@ function findMissionDocInBranches(slug, rootDir = process.cwd(), gitRunner) {
986
986
  return candidates;
987
987
  }
988
988
  }
989
- catch (e) {
989
+ catch (_e) {
990
990
  return candidates;
991
991
  }
992
992
  const branches = branchResult.stdout.trim().split('\n')
@@ -1002,7 +1002,7 @@ function findMissionDocInBranches(slug, rootDir = process.cwd(), gitRunner) {
1002
1002
  break;
1003
1003
  }
1004
1004
  }
1005
- catch (err) {
1005
+ catch (_err) {
1006
1006
  // ignore
1007
1007
  }
1008
1008
  }
@@ -929,7 +929,7 @@ export function findMissionDocInBranches(slug: string, rootDir: string = process
929
929
  try {
930
930
  branchResult = runner(['-C', rootDir, 'branch', '-a', '--format=%(refname:short)']);
931
931
  if (branchResult.status !== 0) {return candidates;}
932
- } catch (e) {
932
+ } catch (_e) {
933
933
  return candidates;
934
934
  }
935
935
 
@@ -946,7 +946,7 @@ export function findMissionDocInBranches(slug: string, rootDir: string = process
946
946
  candidates.push({ branch, path: f });
947
947
  break;
948
948
  }
949
- } catch (err) {
949
+ } catch (_err) {
950
950
  // ignore
951
951
  }
952
952
  }
@@ -176,7 +176,7 @@ function migrateStats(options: { sourcePaths?: string[]; sourcePath?: string; de
176
176
 
177
177
  interface BlocklistSource { filePath: string; payload: Record<string, unknown>; blocklist: Record<string, unknown>; }
178
178
 
179
- function readBlocklistSource(filePath: string, warn: (...args: unknown[]) => void, hardFailure = false): BlocklistSource | null {
179
+ function readBlocklistSource(filePath: string, warn: (..._args: unknown[]) => void, hardFailure = false): BlocklistSource | null {
180
180
  if (!filePath || !fs.existsSync(filePath)) {return null;}
181
181
  try {
182
182
  const payload = JSON.parse(fs.readFileSync(filePath, 'utf8')) as Record<string, unknown>;
@@ -208,7 +208,7 @@ function sameValue(left: unknown, right: unknown): boolean {
208
208
  * @param options - Migration configuration (warn callback, destinationPath, sourcePaths)
209
209
  * @returns Result with destination path, merged blocklist, and conflict details
210
210
  */
211
- function migrateAgentBlocklists(options: { warn?: (...args: unknown[]) => void; destinationPath?: string; sourcePaths?: string[] } = {}): { destinationPath: string; blocklist: Record<string, unknown>; conflicts: unknown[] } {
211
+ function migrateAgentBlocklists(options: { warn?: (..._args: unknown[]) => void; destinationPath?: string; sourcePaths?: string[] } = {}): { destinationPath: string; blocklist: Record<string, unknown>; conflicts: unknown[] } {
212
212
  const opts = options;
213
213
  const warn = opts.warn || (() => {});
214
214
  const destinationPath = opts.destinationPath || storage.resolveAgentsLocalPath({ ensureDir: true });
@@ -17,7 +17,7 @@ interface SpawnTeeResult {
17
17
  }
18
18
 
19
19
  interface NoOutputWatchdog {
20
- onNoOutput?: (event: { command: string; args: string[]; pid: number | undefined; elapsedMs: number }) => void;
20
+ onNoOutput?: (_event: { command: string; args: string[]; pid: number | undefined; elapsedMs: number }) => void;
21
21
  initialDelayMs?: number;
22
22
  intervalMs?: number;
23
23
  }
@@ -81,11 +81,11 @@ export function toVirtual(actualState: string, map: Record<string, unknown> = lo
81
81
  }
82
82
 
83
83
  interface TransitionVirtualOptions {
84
- log?: (msg: string) => void;
84
+ log?: (_msg: string) => void;
85
85
  }
86
86
 
87
87
  interface TransitionTaskFn {
88
- (slug: string, actual: string, options: TransitionVirtualOptions): boolean;
88
+ (_slug: string, _actual: string, _options: TransitionVirtualOptions): boolean;
89
89
  }
90
90
 
91
91
  export function transitionVirtual(transitionTaskFn: TransitionTaskFn, slug: string, virtualState: string, options: TransitionVirtualOptions = {}, mapParam?: Record<string, unknown>): boolean {
@@ -89,7 +89,7 @@ export function resolveParallixHome(
89
89
  */
90
90
  export interface ResolveStatsOptions {
91
91
  ensureDir?: boolean;
92
- warn?: (...args: unknown[]) => void;
92
+ warn?: (..._args: unknown[]) => void;
93
93
  }
94
94
 
95
95
  export function resolveStatsPath(options: ResolveStatsOptions = {}): string {
@@ -21,7 +21,7 @@ interface GitResult {
21
21
  error?: Error | null;
22
22
  }
23
23
 
24
- export type GitFn = (args: string[], options?: GitOptions) => GitResult;
24
+ export type GitFn = (_args: string[], _options?: GitOptions) => GitResult;
25
25
 
26
26
  export interface VerificationAdapterConfig {
27
27
  command: string | null;
@@ -14,7 +14,7 @@ import { resolveWorktree, isMissionArtifact, isWorkflowGeneratedArtifact } from
14
14
  import { isProviderEnabled } from './review-adapter.js';
15
15
  import { spawnSync } from 'child_process';
16
16
 
17
- type RunFn = (cmd: string, args: string[], opts: { cwd?: string; encoding?: string; stdio?: unknown[] }) => { status: number | null; stdout?: string; stderr?: string };
17
+ type RunFn = (_cmd: string, _args: string[], _opts: { cwd?: string; encoding?: string; stdio?: unknown[] }) => { status: number | null; stdout?: string; stderr?: string };
18
18
 
19
19
  const _spawnSync = spawnSync as unknown as RunFn;
20
20
 
@@ -39,11 +39,11 @@ export async function commitSafeMissionArtifacts(slug: string, worktree: string,
39
39
  }: {
40
40
  taskFile?: string | null;
41
41
  gitFn?: typeof git;
42
- log?: (msg: string) => void;
43
- error?: (msg: string) => void;
44
- isMissionArtifactFn?: (file: string, slug: string, rootDir: string) => boolean;
45
- isWorkflowGeneratedArtifactFn?: (file: string) => boolean;
46
- resolveStatsRelPathFn?: (rootDir: string) => string | null;
42
+ log?: (_msg: string) => void;
43
+ error?: (_msg: string) => void;
44
+ isMissionArtifactFn?: (_file: string, _slug: string, _rootDir: string) => boolean;
45
+ isWorkflowGeneratedArtifactFn?: (_file: string) => boolean;
46
+ resolveStatsRelPathFn?: (_rootDir: string) => string | null;
47
47
  } = {}): Promise<{ ok: boolean; dirty: boolean; unsafe?: boolean }> {
48
48
  const rootDir = worktree || process.cwd();
49
49
  const statusResult = gitFn(['-C', rootDir, 'status', '--porcelain=v1', '-z']);
@@ -131,15 +131,15 @@ export async function rebaseBeforeReviewRound(slug: string, {
131
131
  legacyIsForgejoReviewEnabledFn = null,
132
132
  isForgejoReviewEnabledFn = null
133
133
  }: {
134
- runFn?: (cmd: string, args: string[], opts: { cwd?: string; encoding?: string; stdio?: unknown[] }) => { status: number | null; stdout?: string; stderr?: string };
134
+ runFn?: (_cmd: string, _args: string[], _opts: { cwd?: string; encoding?: string; stdio?: unknown[] }) => { status: number | null; stdout?: string; stderr?: string };
135
135
  gitFn?: typeof git;
136
136
  taskFile?: string | null;
137
137
  worktree?: string;
138
- log?: (msg: string) => void;
139
- error?: (msg: string) => void;
140
- isReviewProviderEnabledFn?: ((wt: string) => boolean) | undefined | null;
141
- legacyIsForgejoReviewEnabledFn?: ((wt: string) => boolean) | null;
142
- isForgejoReviewEnabledFn?: ((wt: string) => boolean) | null;
138
+ log?: (_msg: string) => void;
139
+ error?: (_msg: string) => void;
140
+ isReviewProviderEnabledFn?: ((_wt: string) => boolean) | undefined | null;
141
+ legacyIsForgejoReviewEnabledFn?: ((_wt: string) => boolean) | null;
142
+ isForgejoReviewEnabledFn?: ((_wt: string) => boolean) | null;
143
143
  } = {}): Promise<{ ok: boolean; sharedFileConflicts: boolean }> {
144
144
  const cleanup = await commitSafeMissionArtifacts(slug, worktree, { taskFile, gitFn, log, error });
145
145
  if (!cleanup.ok) {