pi-plans 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +47 -22
- package/docs/assets/pi-plans-logo.svg +2 -2
- package/index.ts +116 -28
- package/package.json +1 -1
- package/references/pi-planning-workflow.md +11 -17
- package/references/plan-artifact-template.md +1 -1
- package/references/state-and-config.md +1 -1
- package/skills/debug-and-plan/SKILL.md +1 -1
- package/skills/plan-big/SKILL.md +1 -1
- package/skills/plan-normal/SKILL.md +1 -1
- package/skills/plan-small/SKILL.md +1 -1
- package/skills/plan-with-refs/SKILL.md +1 -1
- package/src/exec.ts +861 -28
- package/src/execution-panel.ts +159 -23
- package/src/plan.ts +106 -0
- package/src/state.ts +36 -0
- package/tests/exec.test.ts +919 -44
- package/tests/execution-panel.test.ts +44 -8
- package/tests/plan.test.ts +84 -1
- package/tests/plans.test.ts +36 -0
- package/tests/state.test.ts +60 -0
- package/tools/execute-plan.ts +46 -6
- package/tools/plans.ts +34 -1
package/src/execution-panel.ts
CHANGED
|
@@ -2,7 +2,15 @@ import * as fs from "node:fs";
|
|
|
2
2
|
import { spawnSync } from "node:child_process";
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
5
|
-
import
|
|
5
|
+
import {
|
|
6
|
+
extractCoverage,
|
|
7
|
+
resolveImplStatuses,
|
|
8
|
+
shortImplDescription,
|
|
9
|
+
type CheckItem,
|
|
10
|
+
type ImplDisplayState,
|
|
11
|
+
type ImplItem,
|
|
12
|
+
type ImplMarkerState,
|
|
13
|
+
} from "./plan.ts";
|
|
6
14
|
|
|
7
15
|
/** SGR runs plus OSC hyperlinks: escaped bytes, no display columns. */
|
|
8
16
|
const ANSI_PATTERN = /\x1b\[[0-9;]*[A-Za-z]|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g;
|
|
@@ -125,6 +133,8 @@ export interface ExecutionPanelState {
|
|
|
125
133
|
export interface ExecutionPanelExecutionLike {
|
|
126
134
|
planPath: string;
|
|
127
135
|
items: CheckItem[];
|
|
136
|
+
implItems?: ImplItem[];
|
|
137
|
+
implStatus?: Record<string, ImplMarkerState>;
|
|
128
138
|
panel?: ExecutionPanelState;
|
|
129
139
|
}
|
|
130
140
|
|
|
@@ -395,24 +405,124 @@ function renderItemLines(item: CheckItem, summary: ItemDiffSummary | undefined,
|
|
|
395
405
|
return lines;
|
|
396
406
|
}
|
|
397
407
|
|
|
408
|
+
function formatElapsed(startedAt: string): string {
|
|
409
|
+
const total = Math.max(0, Math.floor((Date.now() - Date.parse(startedAt)) / 1000));
|
|
410
|
+
const h = String(Math.floor(total / 3600)).padStart(2, "0");
|
|
411
|
+
const m = String(Math.floor((total % 3600) / 60)).padStart(2, "0");
|
|
412
|
+
const sec = String(total % 60).padStart(2, "0");
|
|
413
|
+
return `${h}:${m}:${sec}`;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function formatToks(tokens: number): string {
|
|
417
|
+
const n = Math.max(0, Math.round(tokens));
|
|
418
|
+
return n < 1000 ? String(n) : `${(n / 1000).toFixed(1)}k`;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Single source for the ⌛ progress count (I-based with zero-coverage
|
|
423
|
+
* exclusion, VC fallback) — shared by the footer line and the panel header.
|
|
424
|
+
*/
|
|
425
|
+
export function computeExecutionProgress(execution: ExecutionPanelExecutionLike & {
|
|
426
|
+
usage?: { inToks: number; outToks: number };
|
|
427
|
+
implStatus?: Record<string, ImplMarkerState>;
|
|
428
|
+
}): { done: number; total: number } {
|
|
429
|
+
const implItems = execution.implItems ?? [];
|
|
430
|
+
if (implItems.length) {
|
|
431
|
+
const statuses = resolveImplStatuses(implItems, execution.items, execution.implStatus);
|
|
432
|
+
const counted = implItems.filter((impl) =>
|
|
433
|
+
execution.items.some((item) => extractCoverage(item.text).includes(impl.id)),
|
|
434
|
+
);
|
|
435
|
+
if (counted.length > 0) {
|
|
436
|
+
return {
|
|
437
|
+
done: counted.filter((impl) => statuses[impl.id] === "vc-passed").length,
|
|
438
|
+
total: counted.length,
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
return {
|
|
443
|
+
done: execution.items.filter((item) => item.done).length,
|
|
444
|
+
total: execution.items.length,
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* The `⌛ plans x/y: spent ...` line. Plain text — callers apply theme color.
|
|
450
|
+
* Shared by the footer line (collapsed) and the panel header (expanded).
|
|
451
|
+
*/
|
|
452
|
+
export function formatExecutionStatusLine(
|
|
453
|
+
execution: ExecutionPanelExecutionLike & {
|
|
454
|
+
startedAt: string;
|
|
455
|
+
usage: { inToks: number; outToks: number };
|
|
456
|
+
implStatus?: Record<string, ImplMarkerState>;
|
|
457
|
+
},
|
|
458
|
+
): string {
|
|
459
|
+
const progress = computeExecutionProgress(execution);
|
|
460
|
+
return `⌛ plans ${progress.done}/${progress.total}: spent ${formatElapsed(execution.startedAt)} · ${formatToks(execution.usage.inToks)} in-toks · ${formatToks(execution.usage.outToks)} out-toks`;
|
|
461
|
+
}
|
|
462
|
+
|
|
398
463
|
function renderPanelLines(execution: ExecutionPanelExecutionLike, theme: ThemeLike, width: number): string[] {
|
|
464
|
+
// Expanded detail view only: the collapsed count lives in the footer, and
|
|
465
|
+
// the expanded header re-renders the same line above the item list.
|
|
399
466
|
const panel = ensurePanelState(execution);
|
|
400
|
-
|
|
401
|
-
const
|
|
402
|
-
const header =
|
|
403
|
-
|
|
404
|
-
|
|
467
|
+
if (!panel.expanded) return [""];
|
|
468
|
+
const lines: string[] = [];
|
|
469
|
+
const header = formatExecutionStatusLine(
|
|
470
|
+
execution as ExecutionPanelExecutionLike & {
|
|
471
|
+
startedAt: string;
|
|
472
|
+
usage: { inToks: number; outToks: number };
|
|
473
|
+
},
|
|
474
|
+
);
|
|
475
|
+
lines.push(truncateAnsi(theme.fg("accent", header), width));
|
|
476
|
+
if (execution.implItems?.length) {
|
|
477
|
+
lines.push(...renderImplGroupedLines(execution, theme, width));
|
|
478
|
+
} else {
|
|
479
|
+
// Legacy fallback: plans without a parsable Implementation Items section.
|
|
480
|
+
for (const item of execution.items) {
|
|
481
|
+
const summary = panel.itemSummaries[item.id]?.summary;
|
|
482
|
+
lines.push(...renderItemLines(item, summary, theme, width));
|
|
483
|
+
}
|
|
405
484
|
}
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
485
|
+
return lines.length ? lines : [""];
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
const IMPL_LABELS: Record<ImplDisplayState, string> = {
|
|
489
|
+
pending: "[Pending]",
|
|
490
|
+
implementing: "[Implementing]",
|
|
491
|
+
implemented: "[Implemented]",
|
|
492
|
+
validating: "[Validating]",
|
|
493
|
+
"vc-passed": "[VC passed]",
|
|
494
|
+
};
|
|
495
|
+
|
|
496
|
+
const IMPL_COLORS: Record<ImplDisplayState, string> = {
|
|
497
|
+
pending: "muted",
|
|
498
|
+
implementing: "accent",
|
|
499
|
+
implemented: "accent",
|
|
500
|
+
validating: "warning",
|
|
501
|
+
"vc-passed": "success",
|
|
502
|
+
};
|
|
503
|
+
|
|
504
|
+
function renderImplGroupedLines(execution: ExecutionPanelExecutionLike, theme: ThemeLike, width: number): string[] {
|
|
505
|
+
const implItems = execution.implItems ?? [];
|
|
506
|
+
const statuses = resolveImplStatuses(implItems, execution.items, execution.implStatus);
|
|
507
|
+
const lines: string[] = [];
|
|
508
|
+
|
|
509
|
+
for (const impl of implItems) {
|
|
510
|
+
const status = statuses[impl.id] ?? "pending";
|
|
511
|
+
let line = `${IMPL_LABELS[status]} ${impl.id}: ${shortImplDescription(impl.text)}`;
|
|
512
|
+
if (status === "vc-passed") {
|
|
513
|
+
// Final state: strike through the entire line.
|
|
514
|
+
line = theme.strikethrough(theme.fg(IMPL_COLORS[status], line));
|
|
515
|
+
} else {
|
|
516
|
+
line = theme.fg(IMPL_COLORS[status], line);
|
|
517
|
+
}
|
|
518
|
+
lines.push(truncateAnsi(line, width));
|
|
410
519
|
}
|
|
411
520
|
return lines;
|
|
412
521
|
}
|
|
413
522
|
|
|
414
523
|
interface WidgetThemeSource {
|
|
415
524
|
theme: ThemeLike | null;
|
|
525
|
+
requestRender: (() => void) | null;
|
|
416
526
|
}
|
|
417
527
|
|
|
418
528
|
let renderCacheWidth: number | null = null;
|
|
@@ -449,7 +559,7 @@ class ExecutionPanelWidget implements WidgetLike {
|
|
|
449
559
|
// a full TUI relayout mid-stream). Registration is tied to the host `ctx.ui`
|
|
450
560
|
// instance so replacement hosts (extension reload, tests) register afresh.
|
|
451
561
|
let panelRef: { current: ExecutionPanelExecutionLike | null } = { current: null };
|
|
452
|
-
const themeSource: WidgetThemeSource = { theme: null };
|
|
562
|
+
const themeSource: WidgetThemeSource = { theme: null, requestRender: null };
|
|
453
563
|
let registeredUi: unknown = null;
|
|
454
564
|
|
|
455
565
|
export function refreshExecutionPanel(ctx: ExtensionContext, execution: ExecutionPanelExecutionLike | null): void {
|
|
@@ -457,26 +567,52 @@ export function refreshExecutionPanel(ctx: ExtensionContext, execution: Executio
|
|
|
457
567
|
clearExecutionPanel(ctx);
|
|
458
568
|
return;
|
|
459
569
|
}
|
|
570
|
+
if (!execution.panel?.expanded) {
|
|
571
|
+
// Collapsed: footer carries the ⌛ line; no panel widget is needed. Clear
|
|
572
|
+
// only when a live expanded widget exists, so repeated progress refreshes
|
|
573
|
+
// do not re-register or tear down the widget slot.
|
|
574
|
+
const hadWidget = panelRef.current !== null || registeredUi !== null;
|
|
575
|
+
panelRef.current = null;
|
|
576
|
+
registeredUi = null;
|
|
577
|
+
themeSource.requestRender = null;
|
|
578
|
+
invalidateRenderCache();
|
|
579
|
+
if (hadWidget) ctx.ui.setWidget(WIDGET_ID, undefined);
|
|
580
|
+
ctx.ui.setStatus("pi-plans", ctx.ui.theme.fg("accent", formatExecutionStatusLine(
|
|
581
|
+
execution as ExecutionPanelExecutionLike & {
|
|
582
|
+
startedAt: string;
|
|
583
|
+
usage: { inToks: number; outToks: number };
|
|
584
|
+
},
|
|
585
|
+
)));
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
460
588
|
panelRef.current = execution;
|
|
461
589
|
invalidateRenderCache(); // next render always reflects the latest state
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
590
|
+
const sameHost = registeredUi === ctx.ui;
|
|
591
|
+
if (!sameHost) {
|
|
592
|
+
registeredUi = ctx.ui;
|
|
593
|
+
themeSource.requestRender = null;
|
|
594
|
+
ctx.ui.setWidget(
|
|
595
|
+
WIDGET_ID,
|
|
596
|
+
(tui, theme) => {
|
|
597
|
+
themeSource.theme = theme as ThemeLike;
|
|
598
|
+
const render = (tui as { requestRender?: unknown }).requestRender;
|
|
599
|
+
themeSource.requestRender = typeof render === "function" ? () => (render as () => void)() : null;
|
|
600
|
+
return new ExecutionPanelWidget(themeSource);
|
|
601
|
+
},
|
|
602
|
+
{ placement: "belowEditor" },
|
|
603
|
+
);
|
|
465
604
|
}
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
return new ExecutionPanelWidget(themeSource);
|
|
472
|
-
},
|
|
473
|
-
{ placement: "belowEditor" },
|
|
474
|
-
);
|
|
605
|
+
// Expanded: the panel renders the ⌛ header itself. Clear the footer copy
|
|
606
|
+
// only when the view is first mounted or the host changes, then request a
|
|
607
|
+
// render for in-place progress updates.
|
|
608
|
+
if (!sameHost) ctx.ui.setStatus("pi-plans", undefined);
|
|
609
|
+
themeSource.requestRender?.();
|
|
475
610
|
}
|
|
476
611
|
|
|
477
612
|
export function clearExecutionPanel(ctx: ExtensionContext): void {
|
|
478
613
|
panelRef.current = null;
|
|
479
614
|
registeredUi = null;
|
|
615
|
+
themeSource.requestRender = null;
|
|
480
616
|
invalidateRenderCache();
|
|
481
617
|
ctx.ui.setWidget(WIDGET_ID, undefined);
|
|
482
618
|
}
|
package/src/plan.ts
CHANGED
|
@@ -35,6 +35,112 @@ export function scanDoneMarkers(text: string): string[] {
|
|
|
35
35
|
return [...text.matchAll(/\[DONE:(VC-\d+)\]/g)].map((match) => match[1]);
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
export type ImplMarkerState = "implemented" | "validating";
|
|
39
|
+
|
|
40
|
+
export interface ImplMarker {
|
|
41
|
+
id: string;
|
|
42
|
+
state: ImplMarkerState;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Extract every [I-xxx:implemented|validating] marker from an assistant message. */
|
|
46
|
+
export function scanImplMarkers(text: string): ImplMarker[] {
|
|
47
|
+
return [...text.matchAll(/\[(I-\d+):(implemented|validating)\]/g)].map((match) => ({
|
|
48
|
+
id: match[1],
|
|
49
|
+
state: match[2] as ImplMarkerState,
|
|
50
|
+
}));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface ImplItem {
|
|
54
|
+
id: string;
|
|
55
|
+
text: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Parse the `## Implementation Items` section of a PLAN_vN.md. Strict grammar:
|
|
60
|
+
* top-level `- `I-001`: text` single lines only; multi-line bodies and nested
|
|
61
|
+
* sub-bullets are ignored (text stops at end of the first line).
|
|
62
|
+
*/
|
|
63
|
+
export function parseImplItems(planText: string): ImplItem[] {
|
|
64
|
+
const lines = planText.split("\n");
|
|
65
|
+
const headerIndex = lines.findIndex((line) => /^##\s+Implementation Items\s*$/.test(line.trim()));
|
|
66
|
+
if (headerIndex < 0) return [];
|
|
67
|
+
const items: ImplItem[] = [];
|
|
68
|
+
const seen = new Set<string>();
|
|
69
|
+
for (let i = headerIndex + 1; i < lines.length; i++) {
|
|
70
|
+
const line = lines[i];
|
|
71
|
+
if (/^##\s/.test(line.trim())) break; // next section ends the items
|
|
72
|
+
const match = line.match(/^\s*-\s+`(I-\d+)`\s*:\s+(.*)$/);
|
|
73
|
+
if (!match) continue;
|
|
74
|
+
const id = match[1];
|
|
75
|
+
if (seen.has(id)) continue;
|
|
76
|
+
seen.add(id);
|
|
77
|
+
items.push({ id, text: match[2].trim() });
|
|
78
|
+
}
|
|
79
|
+
return items;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* One-sentence description: cut at the first sentence boundary (。/.) or
|
|
84
|
+
* semicolon (;/;), then cap at 80 characters with an ellipsis.
|
|
85
|
+
*/
|
|
86
|
+
export function shortImplDescription(text: string): string {
|
|
87
|
+
const clipped = text.split(/[。;;]/)[0] ?? text;
|
|
88
|
+
const sentence = clipped.split(/(?<=[.])\s/)[0] ?? clipped;
|
|
89
|
+
const trimmed = sentence.trim();
|
|
90
|
+
if (trimmed.length <= 80) return trimmed;
|
|
91
|
+
return `${trimmed.slice(0, 79)}…`;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Extract the covered I-ids from a VC checklist line's coverage clause
|
|
96
|
+
* ("`VC-001` covers `I-002` and `I-003`; pass condition: ..." → ["I-002",
|
|
97
|
+
* "I-003"]). Only references before the first ";" count.
|
|
98
|
+
*/
|
|
99
|
+
export function extractCoverage(vcText: string): string[] {
|
|
100
|
+
const clause = vcText.split(";")[0] ?? "";
|
|
101
|
+
return [...clause.matchAll(/\bI-\d+\b/g)].map((match) => match[0]);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export type ImplDisplayState = "pending" | "implementing" | "implemented" | "validating" | "vc-passed";
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Resolve the display state for every I-item. Precedence: vc-passed (all
|
|
108
|
+
* covering VCs done, final) > explicit marker (validating / implemented) >
|
|
109
|
+
* derivation (some covering VC done → validating; first I with no covering
|
|
110
|
+
* VC done → implementing; rest → pending).
|
|
111
|
+
*/
|
|
112
|
+
export function resolveImplStatuses(
|
|
113
|
+
implItems: ImplItem[],
|
|
114
|
+
items: CheckItem[],
|
|
115
|
+
implStatus: Record<string, ImplMarkerState> | undefined,
|
|
116
|
+
): Record<string, ImplDisplayState> {
|
|
117
|
+
const result: Record<string, ImplDisplayState> = {};
|
|
118
|
+
let frontierAssigned = false;
|
|
119
|
+
for (const impl of implItems) {
|
|
120
|
+
const coverage = items.filter((item) => extractCoverage(item.text).includes(impl.id));
|
|
121
|
+
if (coverage.length > 0 && coverage.every((item) => item.done)) {
|
|
122
|
+
result[impl.id] = "vc-passed";
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
const marker = implStatus?.[impl.id];
|
|
126
|
+
if (marker === "validating" || marker === "implemented") {
|
|
127
|
+
result[impl.id] = marker;
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
if (coverage.some((item) => item.done)) {
|
|
131
|
+
result[impl.id] = "validating";
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
if (!frontierAssigned) {
|
|
135
|
+
result[impl.id] = "implementing";
|
|
136
|
+
frontierAssigned = true;
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
result[impl.id] = "pending";
|
|
140
|
+
}
|
|
141
|
+
return result;
|
|
142
|
+
}
|
|
143
|
+
|
|
38
144
|
export interface PlanVersionFile {
|
|
39
145
|
path: string;
|
|
40
146
|
version: number;
|
package/src/state.ts
CHANGED
|
@@ -34,11 +34,18 @@ export interface RoleConfig {
|
|
|
34
34
|
confirmed_at: string | null;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
export interface ExecutionConfig {
|
|
38
|
+
model_selector: string | null;
|
|
39
|
+
source: SettingSource;
|
|
40
|
+
updated_at: string | null;
|
|
41
|
+
}
|
|
42
|
+
|
|
37
43
|
export interface PlansConfig {
|
|
38
44
|
schema: number;
|
|
39
45
|
language: LanguageConfig;
|
|
40
46
|
reviewer: RoleConfig;
|
|
41
47
|
criticizer: RoleConfig;
|
|
48
|
+
execution: ExecutionConfig;
|
|
42
49
|
artifact_root: string;
|
|
43
50
|
artifact_root_source: SettingSource;
|
|
44
51
|
artifact_root_updated_at: string | null;
|
|
@@ -69,6 +76,11 @@ export const DEFAULT_CONFIG: PlansConfig = {
|
|
|
69
76
|
name_prefix: "pi-plans-criticizer",
|
|
70
77
|
confirmed_at: null,
|
|
71
78
|
},
|
|
79
|
+
execution: {
|
|
80
|
+
model_selector: null,
|
|
81
|
+
source: "unset",
|
|
82
|
+
updated_at: null,
|
|
83
|
+
},
|
|
72
84
|
artifact_root: DEFAULT_ARTIFACT_ROOT,
|
|
73
85
|
artifact_root_source: "unset",
|
|
74
86
|
artifact_root_updated_at: null,
|
|
@@ -316,6 +328,22 @@ export function setArtifactRoot(workdir: string, artifactRoot: string, source: "
|
|
|
316
328
|
return { config, stateRoot, notices };
|
|
317
329
|
}
|
|
318
330
|
|
|
331
|
+
export interface SetExecutionModelOptions {
|
|
332
|
+
modelSelector?: string;
|
|
333
|
+
source: "user" | "auto";
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
export function setExecutionModel(workdir: string, options: SetExecutionModelOptions): EnsureResult {
|
|
337
|
+
const { config, stateRoot, notices } = ensureState(workdir);
|
|
338
|
+
config.execution = {
|
|
339
|
+
model_selector: options.modelSelector === undefined || options.modelSelector === "inherit" ? null : options.modelSelector,
|
|
340
|
+
source: options.source,
|
|
341
|
+
updated_at: utcNow(),
|
|
342
|
+
};
|
|
343
|
+
atomicWriteJson(path.join(stateRoot, "config.json"), config);
|
|
344
|
+
return { config, stateRoot, notices };
|
|
345
|
+
}
|
|
346
|
+
|
|
319
347
|
export interface SetRoleOptions {
|
|
320
348
|
role: "reviewer" | "criticizer";
|
|
321
349
|
mode?: string;
|
|
@@ -359,6 +387,7 @@ export interface StartRunOptions {
|
|
|
359
387
|
topic: string;
|
|
360
388
|
skill: string;
|
|
361
389
|
requestText: string;
|
|
390
|
+
onStart?: (run: RunInfo) => void;
|
|
362
391
|
}
|
|
363
392
|
|
|
364
393
|
export interface StartRunResult {
|
|
@@ -408,6 +437,13 @@ export function startRun(workdir: string, options: StartRunOptions): StartRunRes
|
|
|
408
437
|
run_dir: runDir,
|
|
409
438
|
artifact_dir: artifactDir,
|
|
410
439
|
} satisfies ActiveInfo);
|
|
440
|
+
if (options.onStart) {
|
|
441
|
+
try {
|
|
442
|
+
options.onStart(run);
|
|
443
|
+
} catch {
|
|
444
|
+
/* marker failure is non-fatal: planning start should not abort when the entry appender rejects */
|
|
445
|
+
}
|
|
446
|
+
}
|
|
411
447
|
return { run, notices };
|
|
412
448
|
}
|
|
413
449
|
|