@magnusekdahl/parallix 1.1.0 → 1.2.0
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/config/integration-pipelines.json +9 -0
- package/config/workflow.config.schema.json +12 -0
- package/docs/adr/0032-mission-refinement-state-and-usage-budget-signals.md +10 -8
- package/docs/adr/0036-mission-sizing-and-dependency-wave-heuristics.md +14 -12
- package/docs/adr/0041-integration-pipeline-gates.md +1 -1
- package/docs/adr/0047-per-mission-change-size-budget.md +161 -0
- package/docs/adr/index.md +1 -0
- package/docs/use-cases.md +44 -0
- package/lib/agents/agents.js +149 -100
- package/lib/agents/claude-telemetry.js +14 -10
- package/lib/agents/claude.js +32 -11
- package/lib/agents/codex-telemetry.js +32 -19
- package/lib/agents/codex.js +36 -11
- package/lib/agents/limit-hit.js +42 -25
- package/lib/agents/mistral-telemetry.js +1 -1
- package/lib/agents/mistral.js +13 -3
- package/lib/agents/opencode-export.js +11 -5
- package/lib/agents/opencode-telemetry.js +75 -90
- package/lib/agents/opencode.js +86 -29
- package/lib/agents/stage-telemetry.js +3 -7
- package/lib/commands/active.js +139 -94
- package/lib/commands/checkpoint.js +3 -1
- package/lib/commands/config.js +5 -3
- package/lib/commands/coverage-gate.js +17 -10
- package/lib/commands/diff.js +11 -5
- package/lib/commands/draft.js +95 -55
- package/lib/commands/handoff.js +253 -65
- package/lib/commands/integrate.js +288 -188
- package/lib/commands/mission-start.js +39 -34
- package/lib/commands/rebase.js +32 -21
- package/lib/commands/repair-handoff.js +21 -18
- package/lib/commands/resolve-conflict.js +5 -2
- package/lib/commands/review.js +1 -0
- package/lib/commands/setup-review.js +1 -0
- package/lib/commands/stats-backfill.js +50 -41
- package/lib/commands/stats.js +653 -204
- package/lib/commands/status.js +33 -28
- package/lib/core/fmt.js +58 -15
- package/lib/core/git.js +7 -5
- package/lib/core/gitignore.js +6 -4
- package/lib/core/mission-utils.js +161 -88
- package/lib/core/nels.js +199 -0
- package/lib/core/persistent-data-migration.js +55 -23
- package/lib/core/product-config.js +44 -23
- package/lib/core/runtime-matrix.js +15 -2
- package/lib/core/spawn-tee.js +40 -13
- package/lib/core/state-map.js +32 -13
- package/lib/core/storage.js +19 -5
- package/lib/core/subagent-limit.js +28 -0
- package/lib/core/verification.js +29 -12
- package/lib/review/rebase.js +11 -3
- package/lib/review/review-adapter.js +93 -6
- package/lib/review/review-artifacts.js +89 -23
- package/lib/review/review-commands.js +139 -45
- package/lib/review/review-events.js +103 -52
- package/lib/review/review-loop.js +117 -87
- package/lib/review/review-polling.js +25 -5
- package/lib/review/review-prompts.js +36 -5
- package/lib/review/review-state.js +39 -15
- package/lib/tools/backlog.js +162 -71
- package/lib/tools/forgejo.js +290 -129
- package/lib/tools/gatekeeper.js +15 -0
- package/lib/tools/redgreen.js +220 -0
- package/lib/tools/sessions.js +12 -5
- package/lib/tools/setup-review.js +170 -70
- package/package.json +11 -3
- package/prompts/draft.md +6 -0
- package/templates/mission-scaffold.md +1 -1
package/lib/commands/diff.js
CHANGED
|
@@ -4,6 +4,7 @@ const { git } = require('../core/git');
|
|
|
4
4
|
const { getPrimaryBranch, missionBranchName, inferSlug, resolveWorktree } = require('../core/mission-utils');
|
|
5
5
|
const fmt = require('../core/fmt');
|
|
6
6
|
|
|
7
|
+
/** @param {string[]} args @param {{gitFn?: Function, spawnSyncFn?: Function, getPrimaryBranchFn?: Function, missionBranchNameFn?: Function, inferSlugFn?: Function, resolveWorktreeFn?: Function, exitFn?: Function, failFn?: Function}} opts */
|
|
7
8
|
async function diff(args, {
|
|
8
9
|
gitFn = git,
|
|
9
10
|
spawnSyncFn = childProcess.spawnSync,
|
|
@@ -14,6 +15,7 @@ async function diff(args, {
|
|
|
14
15
|
exitFn = process.exit,
|
|
15
16
|
failFn = fmt.log.fail,
|
|
16
17
|
} = {}) {
|
|
18
|
+
/** @param {string} a */
|
|
17
19
|
const explicitSlug = args.filter(a => !a.startsWith('--'))[0];
|
|
18
20
|
const slug = inferSlugFn(explicitSlug);
|
|
19
21
|
|
|
@@ -34,7 +36,7 @@ async function diff(args, {
|
|
|
34
36
|
try {
|
|
35
37
|
primary = getPrimaryBranchFn(worktree);
|
|
36
38
|
} catch (err) {
|
|
37
|
-
failFn(err.message);
|
|
39
|
+
failFn(/** @type{Error} */(err).message);
|
|
38
40
|
exitFn(1);
|
|
39
41
|
return;
|
|
40
42
|
}
|
|
@@ -43,30 +45,34 @@ async function diff(args, {
|
|
|
43
45
|
const target = `${primary}..HEAD`;
|
|
44
46
|
|
|
45
47
|
// Detect configured diff tool/pager
|
|
48
|
+
/** @param {string} key */
|
|
46
49
|
const getGitConfig = (key) => {
|
|
47
50
|
const result = gitFn(['-C', worktree, 'config', '--get', key]);
|
|
48
51
|
return result.status === 0 ? result.stdout.trim() : null;
|
|
49
52
|
};
|
|
50
53
|
|
|
54
|
+
/** @param {string} cmd */
|
|
51
55
|
const isSpecializedTool = (cmd) => {
|
|
52
|
-
if (!cmd) return false;
|
|
56
|
+
if (!cmd) {return false;}
|
|
53
57
|
const parts = cmd.split(/\s+/);
|
|
54
58
|
// Handle cases like "LESS=FRX less" or "/usr/bin/delta"
|
|
59
|
+
/** @param {string} p */
|
|
55
60
|
const exePart = parts.find(p => !p.includes('='));
|
|
56
|
-
if (!exePart) return false;
|
|
61
|
+
if (!exePart) {return false;}
|
|
57
62
|
const exe = path.basename(exePart).toLowerCase();
|
|
58
63
|
|
|
59
64
|
// Explicitly reject known default pagers
|
|
60
|
-
if (['less', 'cat', 'more'].includes(exe)) return false;
|
|
65
|
+
if (['less', 'cat', 'more'].includes(exe)) {return false;}
|
|
61
66
|
|
|
62
67
|
// Accept known specialized tools documented in review-tooling.md
|
|
63
|
-
if (['delta', 'difft', 'diff-so-fancy'].includes(exe)) return true;
|
|
68
|
+
if (['delta', 'difft', 'diff-so-fancy'].includes(exe)) {return true;}
|
|
64
69
|
|
|
65
70
|
// Fallback: if it's not a known default, and we have a value, treat it as specialized
|
|
66
71
|
// for now to avoid breaking custom setups like "vimdiff", but we've rejected plain "less".
|
|
67
72
|
return true;
|
|
68
73
|
};
|
|
69
74
|
|
|
75
|
+
/** @param {childProcess.SpawnSyncReturns<string>} result @param {string} toolName */
|
|
70
76
|
const handleLaunchResult = (result, toolName) => {
|
|
71
77
|
if (result.error) {
|
|
72
78
|
failFn(`Failed to launch ${toolName}: ${result.error.message}`);
|
package/lib/commands/draft.js
CHANGED
|
@@ -4,7 +4,7 @@ const crypto = require('crypto');
|
|
|
4
4
|
const fmt = require('../core/fmt');
|
|
5
5
|
const { git, getWorktreeStatus } = require('../core/git');
|
|
6
6
|
const { startDraftAgent, selectAgent, readAgentConfigOrExit } = require('../agents/agents');
|
|
7
|
-
const { resolveTaskFile, enforceTaskAssignee, reportTaskResolution, checkBacklogIntegrity, transitionTask,
|
|
7
|
+
const { resolveTaskFile, enforceTaskAssignee, reportTaskResolution, checkBacklogIntegrity, transitionTask, getTaskStorage } = require('../tools/backlog');
|
|
8
8
|
const { findMissionArea, findMissionDir, inferSlug, getMissionYear, resolveMainRepo, conventionalWorktreePath, squashTrailingBacklogNoiseIntoPreviousMission, resolveWorktree, getPrimaryBranch, missionBranchName, missionDirForSlug, detectLaunchBaseBranch } = require('../core/mission-utils');
|
|
9
9
|
const { transitionVirtual } = require('../core/state-map');
|
|
10
10
|
const stats = require('./stats');
|
|
@@ -17,7 +17,7 @@ const DRAFT_PROMPT_PATH = path.join(__dirname, '..', '..', 'prompts', 'draft.md'
|
|
|
17
17
|
const MISSION_SCAFFOLD_PATH = path.join(__dirname, '..', '..', 'templates', 'mission-scaffold.md');
|
|
18
18
|
const SYNTHETIC_SLUG_PREFIX = 'adhoc-';
|
|
19
19
|
|
|
20
|
-
function slugifyDraftIntent(value) {
|
|
20
|
+
function slugifyDraftIntent(/** @type {string} */ value) {
|
|
21
21
|
return String(value || '')
|
|
22
22
|
.trim()
|
|
23
23
|
.toLowerCase()
|
|
@@ -28,7 +28,7 @@ function slugifyDraftIntent(value) {
|
|
|
28
28
|
.slice(0, 64);
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
function syntheticTaskId(slug, seed) {
|
|
31
|
+
function syntheticTaskId(/** @type {string} */ slug, /** @type {string} */ seed) {
|
|
32
32
|
const hash = crypto.createHash('sha1').update(String(seed || slug)).digest('hex').slice(0, 8).toUpperCase();
|
|
33
33
|
const prefix = slug.startsWith(SYNTHETIC_SLUG_PREFIX) ? 'ADHOC' : 'TASK';
|
|
34
34
|
const base = slug
|
|
@@ -38,9 +38,9 @@ function syntheticTaskId(slug, seed) {
|
|
|
38
38
|
return `${prefix}-${base}-${hash}`;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
function resolveDraftTarget(rawInput, cwd = process.cwd()) {
|
|
41
|
+
function resolveDraftTarget(/** @type {string} */ rawInput, cwd = process.cwd()) {
|
|
42
42
|
const explicit = String(rawInput || '').trim();
|
|
43
|
-
if (!explicit) return null;
|
|
43
|
+
if (!explicit) {return null;}
|
|
44
44
|
|
|
45
45
|
if (explicit.toLowerCase().startsWith('task-')) {
|
|
46
46
|
return {
|
|
@@ -90,7 +90,7 @@ function resolveDraftTarget(rawInput, cwd = process.cwd()) {
|
|
|
90
90
|
};
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
-
async function runDraftCommand(args, {
|
|
93
|
+
async function runDraftCommand(/** @type {string[]} */ args, {
|
|
94
94
|
inferSlugFn = inferSlug,
|
|
95
95
|
resolveMainRepoFn = resolveMainRepo,
|
|
96
96
|
conventionalWorktreePathFn = conventionalWorktreePath,
|
|
@@ -136,9 +136,9 @@ async function runDraftCommand(args, {
|
|
|
136
136
|
const syntheticTask = draftTarget.syntheticTask;
|
|
137
137
|
|
|
138
138
|
// Allow operators to pin the agent family via CLI flag instead of WORKFLOW_AGENT env var.
|
|
139
|
-
function flagValue(arr, flag, name) {
|
|
139
|
+
function flagValue(/** @type {string[]} */ arr, /** @type {string} */ flag, /** @type {string} */ name) {
|
|
140
140
|
const i = arr.indexOf(flag);
|
|
141
|
-
if (i === -1) return null;
|
|
141
|
+
if (i === -1) {return null;}
|
|
142
142
|
const v = arr[i + 1];
|
|
143
143
|
if (!v || v.startsWith('--')) {
|
|
144
144
|
errorFn(fmt.status('FAIL', `Missing value for --${name}. Usage: px draft <slug> --${name} <family>`));
|
|
@@ -178,7 +178,7 @@ async function runDraftCommand(args, {
|
|
|
178
178
|
try {
|
|
179
179
|
launchBase = detectLaunchBaseBranchFn(process.cwd());
|
|
180
180
|
} catch (error) {
|
|
181
|
-
errorFn(fmt.status('FAIL', error.message));
|
|
181
|
+
errorFn(fmt.status('FAIL', /** @type {any} */ (error).message));
|
|
182
182
|
exitFn(1);
|
|
183
183
|
return;
|
|
184
184
|
}
|
|
@@ -208,9 +208,9 @@ async function runDraftCommand(args, {
|
|
|
208
208
|
errorFn(fmt.status('FAIL', `Backlog integrity issues detected for ${normalizedSlug}:`));
|
|
209
209
|
relevantIssues.forEach(issue => {
|
|
210
210
|
if (issue.type === 'duplicate-completed') {
|
|
211
|
-
logFn(` - ${fmt.path(issue.file)}: task ${fmt.bold(issue.taskId)} already has a canonical copy in ${fmt.path(issue.canonicalFile)}; this backlog/tasks copy is stale.`);
|
|
211
|
+
logFn(` - ${fmt.path(/** @type {any} */ (issue).file)}: task ${fmt.bold(/** @type {any} */ (issue).taskId)} already has a canonical copy in ${fmt.path(/** @type {any} */ (issue).canonicalFile)}; this backlog/tasks copy is stale.`);
|
|
212
212
|
} else {
|
|
213
|
-
logFn(` - ${fmt.path(issue.file)}: filename ID (${fmt.bold(issue.filenameId)}) does not match frontmatter ID (${fmt.bold(issue.frontmatterId)})`);
|
|
213
|
+
logFn(` - ${fmt.path(/** @type {any} */ (issue).file)}: filename ID (${fmt.bold(/** @type {any} */ (issue).filenameId)}) does not match frontmatter ID (${fmt.bold(/** @type {any} */ (issue).frontmatterId)})`);
|
|
214
214
|
}
|
|
215
215
|
});
|
|
216
216
|
logFn('Repair: Fix filename/id mismatch, or remove the stale backlog/tasks copy of a completed/archived task, before drafting.');
|
|
@@ -244,7 +244,7 @@ async function runDraftCommand(args, {
|
|
|
244
244
|
ensureMissionBaseBranchRecordedFn(missionFile, recordedBase, { logFn });
|
|
245
245
|
|
|
246
246
|
logFn(fmt.bold('Step 4: Ensuring Backlog task exists in worktree...'));
|
|
247
|
-
if (!bootstrapBacklogTaskFn(targetWorktree, mainRepo, normalizedSlug, { logFn, errorFn, syntheticTask })) {
|
|
247
|
+
if (!bootstrapBacklogTaskFn(targetWorktree, mainRepo, normalizedSlug, { logFn, errorFn, syntheticTask: /** @type {null | undefined} */ (syntheticTask) })) {
|
|
248
248
|
const { tasksDir } = getTaskStorage(targetWorktree);
|
|
249
249
|
const taskDirHint = path.relative(targetWorktree, tasksDir).split(path.sep).join('/');
|
|
250
250
|
errorFn(fmt.status('FAIL', `Backlog task for ${normalizedSlug} could not be prepared in the mission worktree.`));
|
|
@@ -276,23 +276,25 @@ async function runDraftCommand(args, {
|
|
|
276
276
|
const agentConfig = readAgentConfigOrExitFn();
|
|
277
277
|
const agent = preselectedAgent || selectAgentFn('draft', { config: agentConfig });
|
|
278
278
|
|
|
279
|
-
|
|
279
|
+
// @ts-expect-error buildDraftPrompt type mismatch
|
|
280
|
+
const prompt = buildDraftPrompt(normalizedSlug, { rootDir: mainRepo, worktree: targetWorktree || '' });
|
|
280
281
|
logFn('Launching draft agent...');
|
|
281
282
|
const { agent: actualAgent, result } = await startDraftAgentFn({
|
|
282
283
|
prompt,
|
|
284
|
+
// @ts-expect-error worktree not in type
|
|
283
285
|
worktree: targetWorktree,
|
|
284
286
|
agent
|
|
285
287
|
});
|
|
286
|
-
logFn(`Draft agent family: ${fmt.agent(actualAgent)}`);
|
|
288
|
+
logFn(`Draft agent family: ${fmt.agent(/** @type {any} */ (actualAgent))}`);
|
|
287
289
|
|
|
288
290
|
if (result.error) {
|
|
289
|
-
errorFn(fmt.status('FAIL', `Could not start draft agent (${fmt.agent(actualAgent)}): ${result.error.message}`));
|
|
291
|
+
errorFn(fmt.status('FAIL', `Could not start draft agent (${fmt.agent(/** @type {any} */ (actualAgent))}): ${/** @type {any} */ (result.error).message}`));
|
|
290
292
|
exitFn(1);
|
|
291
293
|
return;
|
|
292
294
|
}
|
|
293
295
|
|
|
294
|
-
if (typeof result.status === 'number' && result.status !== 0) {
|
|
295
|
-
errorFn(fmt.status('FAIL', `Draft agent (${fmt.agent(actualAgent)}) exited with status ${result.status}.`));
|
|
296
|
+
if (typeof /** @type {any} */ (result).status === 'number' && /** @type {any} */ (result).status !== 0) {
|
|
297
|
+
errorFn(fmt.status('FAIL', `Draft agent (${fmt.agent(/** @type {any} */ (actualAgent))}) exited with status ${/** @type {any} */ (result).status}.`));
|
|
296
298
|
exitFn(result.status || 1);
|
|
297
299
|
return;
|
|
298
300
|
}
|
|
@@ -315,6 +317,7 @@ async function runDraftCommand(args, {
|
|
|
315
317
|
agentFamily: actualAgent,
|
|
316
318
|
result,
|
|
317
319
|
log: logFn,
|
|
320
|
+
// @ts-expect-error reportTaskResolutionFn accepts extra properties
|
|
318
321
|
error: errorFn
|
|
319
322
|
});
|
|
320
323
|
|
|
@@ -328,6 +331,7 @@ async function runDraftCommand(args, {
|
|
|
328
331
|
const restartOk = await restartDraftAgentFn(normalizedSlug, targetWorktree, {
|
|
329
332
|
logFn,
|
|
330
333
|
errorFn,
|
|
334
|
+
// @ts-expect-error restartDraftAgentFn accepts extra properties
|
|
331
335
|
exitFn
|
|
332
336
|
});
|
|
333
337
|
if (!restartOk) {
|
|
@@ -355,12 +359,12 @@ async function runDraftCommand(args, {
|
|
|
355
359
|
try {
|
|
356
360
|
enforceDraftCommitSafetyFn({ slug: normalizedSlug, worktree: targetWorktree, logFn, errorFn });
|
|
357
361
|
} catch (error) {
|
|
358
|
-
errorFn(fmt.status('FAIL', error.message));
|
|
362
|
+
errorFn(fmt.status('FAIL', /** @type {any} */ (error).message));
|
|
359
363
|
exitFn(1);
|
|
360
364
|
return;
|
|
361
365
|
}
|
|
362
366
|
|
|
363
|
-
if (!transitionVirtualFn(transitionTaskFn, normalizedSlug, 'ready', { rootDir: targetWorktree, log: logFn })) {
|
|
367
|
+
if (!transitionVirtualFn(transitionTaskFn, normalizedSlug, 'ready', /** @type {{ rootDir: string, log: Function }} */ ({ rootDir: targetWorktree, log: logFn }))) {
|
|
364
368
|
errorFn(fmt.status('FAIL', `Could not transition task ${normalizedSlug} to ready status.`));
|
|
365
369
|
exitFn(1);
|
|
366
370
|
return;
|
|
@@ -369,18 +373,24 @@ async function runDraftCommand(args, {
|
|
|
369
373
|
logFn('\n' + fmt.status('INFO', `Next: ${fmt.command(`cd ${targetWorktree}`)}`));
|
|
370
374
|
}
|
|
371
375
|
|
|
376
|
+
// @ts-expect-error implicit any on args/deps
|
|
372
377
|
async function draft(args, deps) {
|
|
373
378
|
return runDraftCommand(args, deps);
|
|
374
379
|
}
|
|
375
380
|
|
|
376
381
|
function recordDraftImplementer({
|
|
382
|
+
// @ts-expect-error implicit any binding elements
|
|
377
383
|
selected,
|
|
384
|
+
// @ts-expect-error implicit any binding elements
|
|
378
385
|
actual,
|
|
386
|
+
// @ts-expect-error implicit any binding elements
|
|
379
387
|
taskResolution,
|
|
380
388
|
log = fmt.log.plain,
|
|
381
389
|
enforceTaskAssigneeFn = enforceTaskAssignee,
|
|
382
390
|
gitFn = git,
|
|
391
|
+
// @ts-expect-error implicit any binding elements
|
|
383
392
|
slug,
|
|
393
|
+
// @ts-expect-error implicit any binding elements
|
|
384
394
|
worktree
|
|
385
395
|
}) {
|
|
386
396
|
if (!taskResolution || !taskResolution.ok || !actual) {
|
|
@@ -408,6 +418,7 @@ function recordDraftImplementer({
|
|
|
408
418
|
return actual;
|
|
409
419
|
}
|
|
410
420
|
|
|
421
|
+
// @ts-expect-error implicit any on mainRepo/branchName
|
|
411
422
|
function ensureMissionBranch(mainRepo, branchName, {
|
|
412
423
|
gitFn = git,
|
|
413
424
|
logFn = fmt.log.plain,
|
|
@@ -436,6 +447,7 @@ function ensureMissionBranch(mainRepo, branchName, {
|
|
|
436
447
|
* detached-HEAD launch) or when the correct line is already present. Replaces a
|
|
437
448
|
* stale line in place, otherwise inserts the line just under the title.
|
|
438
449
|
*/
|
|
450
|
+
// @ts-expect-error implicit any on missionFile/baseBranch
|
|
439
451
|
function ensureMissionBaseBranchRecorded(missionFile, baseBranch, { logFn = fmt.log.plain } = {}) {
|
|
440
452
|
if (!baseBranch || !missionFile || !fs.existsSync(missionFile)) {
|
|
441
453
|
return false;
|
|
@@ -463,6 +475,7 @@ function ensureMissionBaseBranchRecorded(missionFile, baseBranch, { logFn = fmt.
|
|
|
463
475
|
return true;
|
|
464
476
|
}
|
|
465
477
|
|
|
478
|
+
// @ts-expect-error implicit any on mainRepo/targetWorktree/branchName
|
|
466
479
|
function ensureWorktree(mainRepo, targetWorktree, branchName, {
|
|
467
480
|
existsFn = fs.existsSync,
|
|
468
481
|
gitFn = git,
|
|
@@ -484,11 +497,12 @@ function ensureWorktree(mainRepo, targetWorktree, branchName, {
|
|
|
484
497
|
gitFn(['-C', mainRepo, 'worktree', 'add', targetWorktree, branchName]);
|
|
485
498
|
logFn(fmt.status('PASS', `Created worktree at ${fmt.path(targetWorktree)}.`));
|
|
486
499
|
} catch (error) {
|
|
487
|
-
errorFn(fmt.status('FAIL', `Could not create worktree: ${error.message}`));
|
|
500
|
+
errorFn(fmt.status('FAIL', `Could not create worktree: ${/** @type {any} */ (error).message}`));
|
|
488
501
|
exitFn(1);
|
|
489
502
|
}
|
|
490
503
|
}
|
|
491
504
|
|
|
505
|
+
// @ts-expect-error implicit any on targetWorktree
|
|
492
506
|
function ensureGraphifyWorkspace(targetWorktree, { logFn = fmt.log.plain } = {}) {
|
|
493
507
|
const targetPath = path.join(targetWorktree, 'graphify-out');
|
|
494
508
|
|
|
@@ -511,6 +525,7 @@ function ensureGraphifyWorkspace(targetWorktree, { logFn = fmt.log.plain } = {})
|
|
|
511
525
|
return true;
|
|
512
526
|
}
|
|
513
527
|
|
|
528
|
+
// @ts-expect-error implicit any on targetWorktree
|
|
514
529
|
function ensureGraphifyIgnore(targetWorktree, { gitFn = git, logFn = fmt.log.plain } = {}) {
|
|
515
530
|
const targetPath = path.join(targetWorktree, '.graphifyignore');
|
|
516
531
|
|
|
@@ -538,12 +553,13 @@ function ensureGraphifyIgnore(targetWorktree, { gitFn = git, logFn = fmt.log.pla
|
|
|
538
553
|
gitFn(['-C', gitRoot, 'commit', '-m', 'workflow: add .graphifyignore to exclude .workflow/ from graphify']);
|
|
539
554
|
logFn(fmt.status('PASS', `Created and committed .graphifyignore in ${fmt.path(gitRoot)}.`));
|
|
540
555
|
} catch (error) {
|
|
541
|
-
logFn(fmt.status('WARN', `Created .graphifyignore but could not commit: ${error.message}. It will be picked up by the draft safety harness.`));
|
|
556
|
+
logFn(fmt.status('WARN', `Created .graphifyignore but could not commit: ${/** @type {any} */ (error).message}. It will be picked up by the draft safety harness.`));
|
|
542
557
|
}
|
|
543
558
|
|
|
544
559
|
return true;
|
|
545
560
|
}
|
|
546
561
|
|
|
562
|
+
// @ts-expect-error implicit any on targetWorktree/slug
|
|
547
563
|
function ensureMissionFile(targetWorktree, slug, { logFn = fmt.log.plain } = {}) {
|
|
548
564
|
const missionDir = missionDirForSlug(targetWorktree, slug);
|
|
549
565
|
if (!fs.existsSync(missionDir)) {
|
|
@@ -563,6 +579,7 @@ function ensureMissionFile(targetWorktree, slug, { logFn = fmt.log.plain } = {})
|
|
|
563
579
|
return missionFile;
|
|
564
580
|
}
|
|
565
581
|
|
|
582
|
+
// @ts-expect-error implicit any on mainRepo
|
|
566
583
|
function ensureDraftRepoConfigCommitted(mainRepo, {
|
|
567
584
|
getWorktreeStatusFn = getWorktreeStatus,
|
|
568
585
|
errorFn = fmt.log.plainError
|
|
@@ -593,6 +610,7 @@ function ensureDraftRepoConfigCommitted(mainRepo, {
|
|
|
593
610
|
return false;
|
|
594
611
|
}
|
|
595
612
|
|
|
613
|
+
// @ts-expect-error implicit any on targetWorktree/mainRepo/slug
|
|
596
614
|
function bootstrapBacklogTask(targetWorktree, mainRepo, slug, {
|
|
597
615
|
resolveTaskFileFn = resolveTaskFile,
|
|
598
616
|
reportTaskResolutionFn = reportTaskResolution,
|
|
@@ -614,11 +632,11 @@ function bootstrapBacklogTask(targetWorktree, mainRepo, slug, {
|
|
|
614
632
|
|
|
615
633
|
if (syntheticTask) {
|
|
616
634
|
const { tasksDir } = getTaskStorage(targetWorktree);
|
|
617
|
-
const taskPath = path.join(tasksDir, `${slug} - ${slugifyDraftIntent(syntheticTask.title || slug) || 'mission'}.md`);
|
|
635
|
+
const taskPath = path.join(tasksDir, `${slug} - ${slugifyDraftIntent(/** @type {any} */ (syntheticTask).title || slug) || 'mission'}.md`);
|
|
618
636
|
const body = [
|
|
619
637
|
'---',
|
|
620
|
-
`id: ${syntheticTask.id || syntheticTaskId(slug, syntheticTask.intent || slug)}`,
|
|
621
|
-
`title: ${syntheticTask.title || slug}`,
|
|
638
|
+
`id: ${/** @type {any} */ (syntheticTask).id || syntheticTaskId(slug, /** @type {any} */ (syntheticTask).intent || slug)}`,
|
|
639
|
+
`title: ${/** @type {any} */ (syntheticTask).title || slug}`,
|
|
622
640
|
'status: backlog',
|
|
623
641
|
'assignee: []',
|
|
624
642
|
"created_date: '" + new Date().toISOString().slice(0, 16).replace('T', ' ') + "'",
|
|
@@ -629,7 +647,7 @@ function bootstrapBacklogTask(targetWorktree, mainRepo, slug, {
|
|
|
629
647
|
'',
|
|
630
648
|
'## Description',
|
|
631
649
|
'',
|
|
632
|
-
syntheticTask.intent || `Synthetic task created for ${slug}.`,
|
|
650
|
+
/** @type {any} */ (syntheticTask).intent || `Synthetic task created for ${slug}.`,
|
|
633
651
|
''
|
|
634
652
|
].join('\n');
|
|
635
653
|
|
|
@@ -643,7 +661,7 @@ function bootstrapBacklogTask(targetWorktree, mainRepo, slug, {
|
|
|
643
661
|
gitFn(['-C', targetWorktree, 'commit', '-m', `backlog(${slug}): create synthetic task`]);
|
|
644
662
|
logFn(fmt.status('PASS', 'Committed synthetic task in worktree.'));
|
|
645
663
|
} catch (error) {
|
|
646
|
-
errorFn(fmt.status('FAIL', `Could not commit synthetic task: ${error.message}`));
|
|
664
|
+
errorFn(fmt.status('FAIL', `Could not commit synthetic task: ${/** @type {any} */ (error).message}`));
|
|
647
665
|
return false;
|
|
648
666
|
}
|
|
649
667
|
return true;
|
|
@@ -656,10 +674,12 @@ function bootstrapBacklogTask(targetWorktree, mainRepo, slug, {
|
|
|
656
674
|
return false;
|
|
657
675
|
}
|
|
658
676
|
|
|
677
|
+
// @ts-expect-error mainResolution.taskFile may be undefined
|
|
659
678
|
const relativePath = path.relative(mainRepo, mainResolution.taskFile);
|
|
660
|
-
const targetPath = path.join(targetWorktree, relativePath);
|
|
679
|
+
const targetPath = path.join(targetWorktree || '', relativePath);
|
|
661
680
|
|
|
662
681
|
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
682
|
+
// @ts-expect-error mainResolution.taskFile may be undefined
|
|
663
683
|
fs.copyFileSync(mainResolution.taskFile, targetPath);
|
|
664
684
|
|
|
665
685
|
logFn(fmt.status('PASS', `Bootstrapped ${fmt.path(relativePath)} from main repo.`));
|
|
@@ -669,17 +689,19 @@ function bootstrapBacklogTask(targetWorktree, mainRepo, slug, {
|
|
|
669
689
|
gitFn(['-C', targetWorktree, 'commit', '-m', `backlog(${slug}): bootstrap task from ${getPrimaryBranch()}`]);
|
|
670
690
|
logFn(fmt.status('PASS', 'Committed bootstrapped task in worktree.'));
|
|
671
691
|
} catch (error) {
|
|
672
|
-
errorFn(fmt.status('FAIL', `Could not commit bootstrapped task: ${error.message}`));
|
|
692
|
+
errorFn(fmt.status('FAIL', `Could not commit bootstrapped task: ${/** @type {any} */ (error).message}`));
|
|
673
693
|
return false;
|
|
674
694
|
}
|
|
675
695
|
|
|
676
696
|
return true;
|
|
677
697
|
}
|
|
678
698
|
|
|
699
|
+
// @ts-expect-error implicit any on rootDir
|
|
679
700
|
function resolveVerifyCmd(rootDir) {
|
|
680
|
-
return formatVerificationCommand(
|
|
701
|
+
return formatVerificationCommand(undefined, rootDir);
|
|
681
702
|
}
|
|
682
703
|
|
|
704
|
+
// @ts-expect-error implicit any on slug/promptRoot
|
|
683
705
|
function resolveTaskPath(slug, promptRoot) {
|
|
684
706
|
const resolution = resolveTaskFile(slug, promptRoot);
|
|
685
707
|
if (resolution && resolution.ok && resolution.taskFile) {
|
|
@@ -688,6 +710,7 @@ function resolveTaskPath(slug, promptRoot) {
|
|
|
688
710
|
return path.join(promptRoot, 'backlog', 'tasks', `<${slug}>.md`);
|
|
689
711
|
}
|
|
690
712
|
|
|
713
|
+
// @ts-expect-error implicit any on taskPath
|
|
691
714
|
function resolveClassificationInstructions(taskPath) {
|
|
692
715
|
if (taskPath && fs.existsSync(taskPath)) {
|
|
693
716
|
const content = fs.readFileSync(taskPath, 'utf8');
|
|
@@ -695,9 +718,10 @@ function resolveClassificationInstructions(taskPath) {
|
|
|
695
718
|
return 'because this task was synthesized by the harness, preserve the `unknown` label unless you have concrete repo-specific evidence to replace it. Do not add a separate frontmatter field for mission type.';
|
|
696
719
|
}
|
|
697
720
|
}
|
|
698
|
-
return 'set exactly one of `ai_sdlc` or `user_value` in the Backlog task labels —
|
|
721
|
+
return 'set exactly one of `ai_sdlc` or `user_value` in the Backlog task labels — plus optionally `bug` for bug-fix missions. Use `ai_sdlc` for workflow, prompt, or agent-fix work; use `user_value` for everything else (including code tech debt). Do not add a separate frontmatter field for mission type.';
|
|
699
722
|
}
|
|
700
723
|
|
|
724
|
+
// @ts-expect-error implicit any on slug/rootDir/worktree
|
|
701
725
|
function buildDraftPrompt(slug, { rootDir = process.cwd(), worktree = null } = {}) {
|
|
702
726
|
const template = fs.readFileSync(DRAFT_PROMPT_PATH, 'utf8');
|
|
703
727
|
const promptRoot = worktree || rootDir;
|
|
@@ -715,10 +739,12 @@ function buildDraftPrompt(slug, { rootDir = process.cwd(), worktree = null } = {
|
|
|
715
739
|
.replaceAll('{{verifyCmd}}', resolveVerifyCmd(promptRoot));
|
|
716
740
|
}
|
|
717
741
|
|
|
742
|
+
// @ts-expect-error implicit any on slug
|
|
718
743
|
function fallbackDraftCommitMessage(slug) {
|
|
719
744
|
return `draft(${slug}): capture agent output`;
|
|
720
745
|
}
|
|
721
746
|
|
|
747
|
+
// @ts-expect-error implicit any on slug/worktree
|
|
722
748
|
function validateDraftClassification(slug, worktree, {
|
|
723
749
|
resolveMissionClassificationFn = stats.resolveMissionClassification,
|
|
724
750
|
errorFn = fmt.log.plainError
|
|
@@ -726,19 +752,20 @@ function validateDraftClassification(slug, worktree, {
|
|
|
726
752
|
try {
|
|
727
753
|
const { classification, error: classificationError } = resolveMissionClassificationFn(slug, worktree);
|
|
728
754
|
if (!classification) {
|
|
729
|
-
if (classificationError) errorFn(fmt.status('FAIL', classificationError));
|
|
755
|
+
if (classificationError) {errorFn(fmt.status('FAIL', classificationError));}
|
|
730
756
|
return { ok: true, classification: null };
|
|
731
757
|
}
|
|
732
758
|
return { ok: true, classification };
|
|
733
759
|
} catch (error) {
|
|
734
|
-
if (error.message.includes('Missing or invalid classification')) {
|
|
760
|
+
if (/** @type {any} */ (error).message.includes('Missing or invalid classification')) {
|
|
735
761
|
return { ok: true, classification: null };
|
|
736
762
|
}
|
|
737
|
-
errorFn(fmt.status('FAIL', error.message));
|
|
763
|
+
errorFn(fmt.status('FAIL', /** @type {any} */ (error).message));
|
|
738
764
|
return { ok: false, reason: 'invalid-classification' };
|
|
739
765
|
}
|
|
740
766
|
}
|
|
741
767
|
|
|
768
|
+
// @ts-expect-error implicit any on slug/worktree
|
|
742
769
|
function normalizeDraftClassification(slug, worktree, {
|
|
743
770
|
resolveMissionClassificationFn = stats.resolveMissionClassification,
|
|
744
771
|
errorFn = fmt.log.plainError
|
|
@@ -746,59 +773,62 @@ function normalizeDraftClassification(slug, worktree, {
|
|
|
746
773
|
try {
|
|
747
774
|
const { classification, error: classificationError } = resolveMissionClassificationFn(slug, worktree);
|
|
748
775
|
if (!classification) {
|
|
749
|
-
if (classificationError) errorFn(fmt.status('FAIL', classificationError));
|
|
776
|
+
if (classificationError) {errorFn(fmt.status('FAIL', classificationError));}
|
|
750
777
|
return { ok: false, reason: 'missing-classification' };
|
|
751
778
|
}
|
|
752
779
|
return { ok: true, classification };
|
|
753
780
|
} catch (error) {
|
|
754
|
-
if (error.message.includes('Missing or invalid classification')) {
|
|
781
|
+
if (/** @type {any} */ (error).message.includes('Missing or invalid classification')) {
|
|
755
782
|
return { ok: false, reason: 'missing-classification' };
|
|
756
783
|
}
|
|
757
|
-
errorFn(fmt.status('FAIL', error.message));
|
|
784
|
+
errorFn(fmt.status('FAIL', /** @type {any} */ (error).message));
|
|
758
785
|
return { ok: false, reason: 'invalid-classification' };
|
|
759
786
|
}
|
|
760
787
|
}
|
|
761
788
|
|
|
789
|
+
// @ts-expect-error implicit any on slug/rootDir/worktree
|
|
762
790
|
function buildRestartPrompt(slug, { rootDir = process.cwd(), worktree = null } = {}) {
|
|
763
791
|
return `${buildDraftPrompt(slug, { rootDir, worktree })}
|
|
764
792
|
|
|
765
793
|
Focused repair:
|
|
766
|
-
- update the backlog task so labels contain exactly one of \`ai_sdlc\` or \`user_value\`
|
|
794
|
+
- update the backlog task so labels contain exactly one of \`ai_sdlc\` or \`user_value\` (plus optionally \`bug\` if this is a bug fix)
|
|
767
795
|
- use \`ai_sdlc\` for workflow, prompt, or agent-fix work; use \`user_value\` for everything else, including standard code tech debt
|
|
768
796
|
- do not add a separate frontmatter field for mission type
|
|
769
|
-
- if both labels are present, keep only the correct one
|
|
797
|
+
- if both classification labels are present, keep only the correct one
|
|
770
798
|
`;
|
|
771
799
|
}
|
|
772
800
|
|
|
801
|
+
// @ts-expect-error implicit any on slug/worktree
|
|
773
802
|
async function restartDraftAgent(slug, worktree, {
|
|
774
803
|
selectAgentFn = selectAgent,
|
|
775
804
|
startDraftAgentFn = startDraftAgent,
|
|
776
805
|
readAgentConfigOrExitFn = readAgentConfigOrExit,
|
|
777
806
|
logFn = fmt.log.plain,
|
|
778
|
-
errorFn = fmt.log.plainError
|
|
779
|
-
exitFn = process.exit
|
|
807
|
+
errorFn = fmt.log.plainError
|
|
780
808
|
} = {}) {
|
|
781
809
|
const agentConfig = readAgentConfigOrExitFn();
|
|
782
810
|
const agent = selectAgentFn('draft', { config: agentConfig });
|
|
783
811
|
|
|
784
812
|
const prompt = buildRestartPrompt(slug, { rootDir: worktree, worktree });
|
|
785
813
|
logFn('Relaunching draft agent to repair mission type labels...');
|
|
814
|
+
// @ts-expect-error startDraftAgentFn accepts extra properties
|
|
786
815
|
const { agent: actualAgent, result } = await startDraftAgentFn({ prompt, worktree, agent });
|
|
787
|
-
logFn(`Restart draft agent family: ${fmt.agent(actualAgent)}`);
|
|
816
|
+
logFn(`Restart draft agent family: ${fmt.agent(/** @type {any} */ (actualAgent))}`);
|
|
788
817
|
|
|
789
818
|
if (result.error) {
|
|
790
|
-
errorFn(fmt.status('FAIL', `Could not restart draft agent (${fmt.agent(actualAgent)}): ${result.error.message}`));
|
|
819
|
+
errorFn(fmt.status('FAIL', `Could not restart draft agent (${fmt.agent(/** @type {any} */ (actualAgent))}): ${/** @type {any} */ (result.error).message}`));
|
|
791
820
|
return false;
|
|
792
821
|
}
|
|
793
822
|
|
|
794
|
-
if (typeof result.status === 'number' && result.status !== 0) {
|
|
795
|
-
errorFn(fmt.status('FAIL', `Restart draft agent (${fmt.agent(actualAgent)}) exited with status ${result.status}.`));
|
|
823
|
+
if (typeof /** @type {any} */ (result).status === 'number' && /** @type {any} */ (result).status !== 0) {
|
|
824
|
+
errorFn(fmt.status('FAIL', `Restart draft agent (${fmt.agent(/** @type {any} */ (actualAgent))}) exited with status ${/** @type {any} */ (result).status}.`));
|
|
796
825
|
return false;
|
|
797
826
|
}
|
|
798
827
|
|
|
799
828
|
return true;
|
|
800
829
|
}
|
|
801
830
|
|
|
831
|
+
// @ts-expect-error implicit any on entry
|
|
802
832
|
function parseDirtyEntry(entry) {
|
|
803
833
|
const match = entry.match(/^(.{1,2})\s+(.*)$/);
|
|
804
834
|
const status = (match ? match[1] : entry.slice(0, 2)).padEnd(2, ' ');
|
|
@@ -816,20 +846,24 @@ function parseDirtyEntry(entry) {
|
|
|
816
846
|
return { status, filePath, sourcePath };
|
|
817
847
|
}
|
|
818
848
|
|
|
849
|
+
// @ts-expect-error implicit any on status
|
|
819
850
|
function isUnmergedStatus(status) {
|
|
820
851
|
return ['DD', 'AU', 'UD', 'UA', 'DU', 'AA', 'UU'].includes(status);
|
|
821
852
|
}
|
|
822
853
|
|
|
854
|
+
// @ts-expect-error implicit any on status
|
|
823
855
|
function isDeletedStatus(status) {
|
|
824
856
|
return status.includes('D') && !isUnmergedStatus(status);
|
|
825
857
|
}
|
|
826
858
|
|
|
859
|
+
// @ts-expect-error implicit any on filePath/slug
|
|
827
860
|
function isMissionTaskPath(filePath, slug) {
|
|
828
|
-
if (!filePath) return false;
|
|
861
|
+
if (!filePath) {return false;}
|
|
829
862
|
const taskPattern = new RegExp(`^backlog/(?:tasks|completed)/[^/]*${slug}(?:\\b|[^/]*$)`);
|
|
830
863
|
return taskPattern.test(filePath);
|
|
831
864
|
}
|
|
832
865
|
|
|
866
|
+
// @ts-expect-error implicit any on filePath/slug/worktree
|
|
833
867
|
function isExpectedDraftPath(filePath, slug, worktree) {
|
|
834
868
|
const missionDir = findMissionDir(slug, worktree);
|
|
835
869
|
const missionPrefix = missionDir
|
|
@@ -838,21 +872,23 @@ function isExpectedDraftPath(filePath, slug, worktree) {
|
|
|
838
872
|
return filePath.startsWith(missionPrefix) || isMissionTaskPath(filePath, slug);
|
|
839
873
|
}
|
|
840
874
|
|
|
875
|
+
// @ts-expect-error implicit any on dirtyEntries/slug/worktree
|
|
841
876
|
function classifyDraftEntries(dirtyEntries, slug, worktree) {
|
|
842
877
|
const parsedEntries = dirtyEntries.map(parseDirtyEntry);
|
|
843
|
-
const conflictEntries = parsedEntries.filter(entry => isUnmergedStatus(entry.status));
|
|
844
|
-
const stagedEntries = parsedEntries.filter(entry => !isUnmergedStatus(entry.status));
|
|
845
|
-
const expectedEntries = stagedEntries.filter(entry => isExpectedDraftPath(entry.filePath, slug, worktree));
|
|
846
|
-
const unexpectedEntries = stagedEntries.filter(entry => !isExpectedDraftPath(entry.filePath, slug, worktree));
|
|
878
|
+
const conflictEntries = parsedEntries.filter((/** @type {any} */ entry) => isUnmergedStatus(entry.status));
|
|
879
|
+
const stagedEntries = parsedEntries.filter((/** @type {any} */ entry) => !isUnmergedStatus(entry.status));
|
|
880
|
+
const expectedEntries = stagedEntries.filter((/** @type {any} */ entry) => isExpectedDraftPath(entry.filePath, slug, worktree));
|
|
881
|
+
const unexpectedEntries = stagedEntries.filter((/** @type {any} */ entry) => !isExpectedDraftPath(entry.filePath, slug, worktree));
|
|
847
882
|
|
|
848
883
|
return { conflictEntries, expectedEntries, unexpectedEntries };
|
|
849
884
|
}
|
|
850
885
|
|
|
851
|
-
|
|
852
|
-
|
|
886
|
+
// @ts-expect-error implicit any on slug/worktree/conflictEntries
|
|
887
|
+
function resolveMissionSpecificDraftConflicts({ slug, worktree, conflictEntries, gitImpl = git, logFn = fmt.log.plain }) {
|
|
888
|
+
const sharedConflicts = conflictEntries.filter((/** @type {any} */ entry) => !isExpectedDraftPath(entry.filePath, slug, worktree));
|
|
853
889
|
if (sharedConflicts.length > 0) {
|
|
854
890
|
const area = findMissionArea(findMissionDir(slug, worktree) || missionDirForSlug(worktree, slug));
|
|
855
|
-
const sharedFiles = sharedConflicts.map(entry => entry.filePath);
|
|
891
|
+
const sharedFiles = sharedConflicts.map((/** @type {any} */ entry) => entry.filePath);
|
|
856
892
|
throw new Error(
|
|
857
893
|
`Draft safety harness found shared-file conflicts: ${sharedFiles.join(', ')}. ` +
|
|
858
894
|
`Run "px resolve-conflict ${slug}" from ${worktree}, then re-run ${formatVerificationCommand(area, worktree)}.`
|
|
@@ -871,6 +907,7 @@ function resolveMissionSpecificDraftConflicts({ slug, worktree, conflictEntries,
|
|
|
871
907
|
}
|
|
872
908
|
}
|
|
873
909
|
|
|
910
|
+
// @ts-expect-error implicit any on slug/worktree/dirtyEntries
|
|
874
911
|
function enforceDraftCommitSafety({ slug, worktree, dirtyEntries = getWorktreeStatus(worktree), gitImpl = git, logFn = fmt.log.plain, errorFn = fmt.log.plainError }) {
|
|
875
912
|
if (dirtyEntries.length === 0) {
|
|
876
913
|
logFn(fmt.status('PASS', 'Draft safety harness: no uncommitted changes left behind.'));
|
|
@@ -883,6 +920,7 @@ function enforceDraftCommitSafety({ slug, worktree, dirtyEntries = getWorktreeSt
|
|
|
883
920
|
}
|
|
884
921
|
|
|
885
922
|
const { conflictEntries, expectedEntries, unexpectedEntries } = classifyDraftEntries(dirtyEntries, slug, worktree);
|
|
923
|
+
// @ts-expect-error errorFn not in type
|
|
886
924
|
resolveMissionSpecificDraftConflicts({ slug, worktree, conflictEntries, gitImpl, logFn, errorFn });
|
|
887
925
|
|
|
888
926
|
const deletedTaskEntries = [...expectedEntries, ...unexpectedEntries].filter(entry =>
|
|
@@ -937,6 +975,7 @@ function enforceDraftCommitSafety({ slug, worktree, dirtyEntries = getWorktreeSt
|
|
|
937
975
|
return true;
|
|
938
976
|
}
|
|
939
977
|
|
|
978
|
+
// @ts-expect-error implicit any on mainRepo
|
|
940
979
|
function ensureRepoExists(mainRepo, exitFn = process.exit, errorFn = fmt.log.fail) {
|
|
941
980
|
if (!fs.existsSync(mainRepo)) {
|
|
942
981
|
errorFn(`Main repository not found at ${mainRepo}. Please ensure it exists or set PRIMARY_WORKTREE.`);
|
|
@@ -952,8 +991,9 @@ function ensureRepoExists(mainRepo, exitFn = process.exit, errorFn = fmt.log.fai
|
|
|
952
991
|
* families record honest zeros with provider/model set to the family name.
|
|
953
992
|
* Best-effort: a failure here must never fail the draft.
|
|
954
993
|
*/
|
|
955
|
-
|
|
956
|
-
|
|
994
|
+
// @ts-expect-error implicit any on slug/rootDir/agentFamily/result
|
|
995
|
+
function recordDraftStats({ slug, rootDir, agentFamily, result, log = fmt.log.plain }) {
|
|
996
|
+
if (!result) {return;}
|
|
957
997
|
let durationMinutes = 0;
|
|
958
998
|
if (result.startedAt && result.endedAt) {
|
|
959
999
|
durationMinutes = (Date.parse(result.endedAt) - Date.parse(result.startedAt)) / 60000;
|
|
@@ -971,7 +1011,7 @@ function recordDraftStats({ slug, rootDir, agentFamily, result, log = fmt.log.pl
|
|
|
971
1011
|
log(fmt.status('INFO', `Draft stats recorded: ${slug} stage=draft provider=${row.provider} model=${row.model} input_tokens=${row.input_tokens} tool_calls=${row.tool_calls}`));
|
|
972
1012
|
} catch (err) {
|
|
973
1013
|
// Best-effort: never escalate to the fatal `error` channel.
|
|
974
|
-
log(fmt.status('WARN', `Could not record draft stats for ${slug}: ${err.message}`));
|
|
1014
|
+
log(fmt.status('WARN', `Could not record draft stats for ${slug}: ${/** @type {any} */ (err).message}`));
|
|
975
1015
|
}
|
|
976
1016
|
}
|
|
977
1017
|
|