@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.
- package/docs/agents.md +1 -1
- package/docs/use-cases.md +1 -1
- package/lib/agents/agents.js +20 -3
- package/lib/agents/agents.ts +14 -3
- package/lib/agents/mistral-telemetry.js +122 -23
- package/lib/agents/mistral-telemetry.ts +141 -26
- package/lib/agents/mistral.js +128 -14
- package/lib/agents/mistral.ts +145 -5
- package/lib/commands/active.js +69 -39
- package/lib/commands/active.ts +97 -60
- package/lib/commands/config.ts +3 -3
- package/lib/commands/coverage-gate.ts +1 -1
- package/lib/commands/draft.js +1 -1
- package/lib/commands/draft.ts +1 -1
- package/lib/commands/handoff.js +13 -8
- package/lib/commands/handoff.ts +24 -18
- package/lib/commands/rebase.js +1 -1
- package/lib/commands/rebase.ts +2 -2
- package/lib/commands/repair-handoff.js +141 -20
- package/lib/commands/repair-handoff.ts +185 -45
- package/lib/commands/resolve-conflict.js +1 -1
- package/lib/commands/resolve-conflict.ts +2 -2
- package/lib/commands/stats-backfill.ts +10 -10
- package/lib/commands/stats.js +38 -11
- package/lib/commands/stats.ts +40 -96
- package/lib/core/fmt.ts +2 -2
- package/lib/core/git.ts +2 -2
- package/lib/core/gitignore.ts +2 -2
- package/lib/core/mission-utils.js +2 -2
- package/lib/core/mission-utils.ts +2 -2
- package/lib/core/persistent-data-migration.ts +2 -2
- package/lib/core/spawn-tee.ts +1 -1
- package/lib/core/state-map.ts +2 -2
- package/lib/core/storage.ts +1 -1
- package/lib/core/verification.ts +1 -1
- package/lib/review/rebase.ts +12 -12
- package/lib/review/review-artifacts.ts +35 -35
- package/lib/review/review-commands.ts +40 -40
- package/lib/review/review-events.ts +12 -12
- package/lib/review/review-loop.js +236 -7
- package/lib/review/review-loop.ts +338 -22
- package/lib/review/review-polling.ts +6 -6
- package/lib/review/review-prompts.js +8 -4
- package/lib/review/review-prompts.ts +12 -8
- package/lib/review/review-state.js +1 -1
- package/lib/review/review-state.ts +2 -2
- package/package.json +3 -2
- package/prompts/review-verbose.md +1 -1
- package/prompts/review.md +1 -1
- 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: (
|
|
16
|
-
_internals: Record<string, (...
|
|
17
|
-
resolveStatsPath: (
|
|
18
|
-
resolveStatsRepoName: (
|
|
19
|
-
loadStatsCsv: (
|
|
20
|
-
deriveImplementerAndFixRounds: (
|
|
21
|
-
upsertStatsRow: (
|
|
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, (
|
|
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?: (
|
|
351
|
-
error?: (
|
|
350
|
+
log?: (_msg: string) => string | null;
|
|
351
|
+
error?: (_msg: string) => string | null;
|
|
352
352
|
rootDir?: string;
|
|
353
353
|
}
|
|
354
354
|
|
package/lib/commands/stats.js
CHANGED
|
@@ -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
|
|
80
|
-
// migrated in-memory on load: the legacy columns are preserved
|
|
81
|
-
// columns default to '' (text) or '0' (numeric). On the next write the
|
|
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:
|
|
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 =
|
|
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:
|
|
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 =
|
|
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,
|
package/lib/commands/stats.ts
CHANGED
|
@@ -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 {
|
|
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
|
|
184
|
-
// migrated in-memory on load: the legacy columns are preserved
|
|
185
|
-
// columns default to '' (text) or '0' (numeric). On the next write the
|
|
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:
|
|
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 =
|
|
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:
|
|
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 =
|
|
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 = (...
|
|
102
|
-
type LogFunc = (
|
|
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
|
|
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
|
-
(
|
|
93
|
+
(_args: string[]): GitResult;
|
|
94
94
|
}
|
|
95
95
|
|
|
96
96
|
interface DetectRebaseOptions {
|
package/lib/core/gitignore.ts
CHANGED
|
@@ -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?: (
|
|
27
|
+
logFn?: (_msg: string) => void;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
type EnsureWorkflowGitignoreFn = {
|
|
31
|
-
(
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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: (...
|
|
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?: (...
|
|
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 });
|
package/lib/core/spawn-tee.ts
CHANGED
|
@@ -17,7 +17,7 @@ interface SpawnTeeResult {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
interface NoOutputWatchdog {
|
|
20
|
-
onNoOutput?: (
|
|
20
|
+
onNoOutput?: (_event: { command: string; args: string[]; pid: number | undefined; elapsedMs: number }) => void;
|
|
21
21
|
initialDelayMs?: number;
|
|
22
22
|
intervalMs?: number;
|
|
23
23
|
}
|
package/lib/core/state-map.ts
CHANGED
|
@@ -81,11 +81,11 @@ export function toVirtual(actualState: string, map: Record<string, unknown> = lo
|
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
interface TransitionVirtualOptions {
|
|
84
|
-
log?: (
|
|
84
|
+
log?: (_msg: string) => void;
|
|
85
85
|
}
|
|
86
86
|
|
|
87
87
|
interface TransitionTaskFn {
|
|
88
|
-
(
|
|
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 {
|
package/lib/core/storage.ts
CHANGED
|
@@ -89,7 +89,7 @@ export function resolveParallixHome(
|
|
|
89
89
|
*/
|
|
90
90
|
export interface ResolveStatsOptions {
|
|
91
91
|
ensureDir?: boolean;
|
|
92
|
-
warn?: (...
|
|
92
|
+
warn?: (..._args: unknown[]) => void;
|
|
93
93
|
}
|
|
94
94
|
|
|
95
95
|
export function resolveStatsPath(options: ResolveStatsOptions = {}): string {
|
package/lib/core/verification.ts
CHANGED
|
@@ -21,7 +21,7 @@ interface GitResult {
|
|
|
21
21
|
error?: Error | null;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
export type GitFn = (
|
|
24
|
+
export type GitFn = (_args: string[], _options?: GitOptions) => GitResult;
|
|
25
25
|
|
|
26
26
|
export interface VerificationAdapterConfig {
|
|
27
27
|
command: string | null;
|
package/lib/review/rebase.ts
CHANGED
|
@@ -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 = (
|
|
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?: (
|
|
43
|
-
error?: (
|
|
44
|
-
isMissionArtifactFn?: (
|
|
45
|
-
isWorkflowGeneratedArtifactFn?: (
|
|
46
|
-
resolveStatsRelPathFn?: (
|
|
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?: (
|
|
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?: (
|
|
139
|
-
error?: (
|
|
140
|
-
isReviewProviderEnabledFn?: ((
|
|
141
|
-
legacyIsForgejoReviewEnabledFn?: ((
|
|
142
|
-
isForgejoReviewEnabledFn?: ((
|
|
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) {
|