@try-works/dsh-recursive-mode 0.1.5 → 0.1.7
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/lib/bootstrap.d.ts +6 -3
- package/lib/client/board.d.ts +4 -1
- package/lib/client/contract.d.ts +16 -13
- package/lib/client/index.d.ts +1 -1
- package/lib/client/open-state.d.ts +20 -0
- package/lib/client/slots.d.ts +13 -1
- package/lib/client.js +160 -71
- package/lib/index.js +3391 -906
- package/lib/runtime.d.ts +12 -5
- package/lib/ts-lint.d.ts +205 -0
- package/package.json +1 -1
- package/src/bootstrap.ts +338 -331
- package/src/client/board.tsx +0 -0
- package/src/client/contract.ts +19 -22
- package/src/client/index.ts +1 -1
- package/src/client/open-state.ts +46 -0
- package/src/client/slots.ts +67 -33
- package/src/commands.ts +11 -0
- package/src/index.ts +238 -228
- package/src/runtime.ts +19 -23
- package/src/ts-lint.ts +2110 -0
- package/references/scripts/__pycache__/recursive_phase_rules.cpython-314.pyc +0 -0
- package/references/scripts/lint-recursive-run.ps1 +0 -25
- package/references/scripts/lint-recursive-run.py +0 -2870
- package/references/scripts/recursive-closeout.ps1 +0 -38
- package/references/scripts/recursive-closeout.py +0 -541
- package/references/scripts/recursive-init.ps1 +0 -515
- package/references/scripts/recursive-init.py +0 -356
- package/references/scripts/recursive-lock.ps1 +0 -26
- package/references/scripts/recursive-lock.py +0 -302
- package/references/scripts/recursive-review-bundle.ps1 +0 -58
- package/references/scripts/recursive-review-bundle.py +0 -503
- package/references/scripts/recursive-router-cli-configure.ps1 +0 -2
- package/references/scripts/recursive-router-cli-configure.py +0 -9
- package/references/scripts/recursive-router-cli-init.ps1 +0 -2
- package/references/scripts/recursive-router-cli-init.py +0 -9
- package/references/scripts/recursive-router-cli-invoke.ps1 +0 -2
- package/references/scripts/recursive-router-cli-invoke.py +0 -9
- package/references/scripts/recursive-router-cli-probe.ps1 +0 -2
- package/references/scripts/recursive-router-cli-probe.py +0 -9
- package/references/scripts/recursive-router-cli-resolve.ps1 +0 -2
- package/references/scripts/recursive-router-cli-resolve.py +0 -9
- package/references/scripts/recursive-router-cli-validate.ps1 +0 -2
- package/references/scripts/recursive-router-cli-validate.py +0 -9
- package/references/scripts/recursive-router-configure.ps1 +0 -27
- package/references/scripts/recursive-router-configure.py +0 -74
- package/references/scripts/recursive-router-init.ps1 +0 -17
- package/references/scripts/recursive-router-init.py +0 -29
- package/references/scripts/recursive-router-invoke.ps1 +0 -47
- package/references/scripts/recursive-router-invoke.py +0 -103
- package/references/scripts/recursive-router-probe.ps1 +0 -25
- package/references/scripts/recursive-router-probe.py +0 -44
- package/references/scripts/recursive-router-resolve.ps1 +0 -26
- package/references/scripts/recursive-router-resolve.py +0 -46
- package/references/scripts/recursive-router-validate.ps1 +0 -17
- package/references/scripts/recursive-router-validate.py +0 -27
- package/references/scripts/recursive-status.ps1 +0 -23
- package/references/scripts/recursive-status.py +0 -2124
- package/references/scripts/recursive-subagent-action.ps1 +0 -98
- package/references/scripts/recursive-subagent-action.py +0 -197
- package/references/scripts/recursive-training-extract.ps1 +0 -23
- package/references/scripts/recursive-training-extract.py +0 -99
- package/references/scripts/recursive-training-grpo.ps1 +0 -52
- package/references/scripts/recursive-training-grpo.py +0 -1341
- package/references/scripts/recursive-training-loader.ps1 +0 -78
- package/references/scripts/recursive-training-loader.py +0 -558
- package/references/scripts/recursive-training-mcp.ps1 +0 -29
- package/references/scripts/recursive-training-mcp.py +0 -261
- package/references/scripts/recursive-training-phase8-trigger.ps1 +0 -55
- package/references/scripts/recursive-training-phase8-trigger.py +0 -137
- package/references/scripts/recursive-training-sync.ps1 +0 -29
- package/references/scripts/recursive-training-sync.py +0 -183
- package/references/scripts/recursive_phase_rules.py +0 -367
- package/references/scripts/recursive_router_cli_lib.py +0 -2
- package/references/scripts/recursive_router_lib.py +0 -2282
- package/references/scripts/verify-locks.ps1 +0 -25
- package/references/scripts/verify-locks.py +0 -353
- package/scripts/__pycache__/lint-recursive-run.cpython-314.pyc +0 -0
- package/scripts/__pycache__/recursive_phase_rules.cpython-314.pyc +0 -0
- package/scripts/install-recursive-mode.ps1 +0 -956
- package/scripts/install-recursive-mode.py +0 -750
- package/scripts/lint-recursive-run.py +0 -2870
- package/scripts/recursive-closeout.py +0 -541
- package/scripts/recursive-init.py +0 -356
- package/scripts/recursive-lock.py +0 -302
- package/scripts/recursive-status.py +0 -2124
- package/scripts/recursive_phase_rules.py +0 -367
- package/scripts/recursive_router_lib.py +0 -2282
- package/scripts/verify-locks.py +0 -353
package/src/client/board.tsx
CHANGED
|
Binary file
|
package/src/client/contract.ts
CHANGED
|
@@ -33,23 +33,25 @@ export interface SessionSummaryRow {
|
|
|
33
33
|
agentPreset?: string
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
/**
|
|
36
|
+
/**
|
|
37
|
+
* The session-list snapshot (mirrors the REAL SessionListState). useSessions is
|
|
38
|
+
* a SnapshotSelectorHook over this state; the identity selector `(s) => s`
|
|
39
|
+
* yields the full value, which the gate reads directly (ids/byId/current).
|
|
40
|
+
*/
|
|
37
41
|
export interface SessionListStateLike {
|
|
38
42
|
ids: string[]
|
|
39
43
|
byId: Record<string, SessionSummaryRow>
|
|
40
44
|
current: string | undefined
|
|
41
45
|
}
|
|
42
46
|
|
|
43
|
-
/**
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
export
|
|
51
|
-
list: SnapshotStoreLike<SessionListStateLike>
|
|
52
|
-
}
|
|
47
|
+
/**
|
|
48
|
+
* The REAL selector-hook shape (ui-slots store.ts):
|
|
49
|
+
* SnapshotSelectorHook<T> = <S>(sel: (s: T) => S, eq?) => S
|
|
50
|
+
* The selector is REQUIRED — useSyncExternalStoreWithSelector needs it. Calling
|
|
51
|
+
* the hook with no selector passes sel=undefined and breaks the board mount.
|
|
52
|
+
* This is the run 10 live bug.
|
|
53
|
+
*/
|
|
54
|
+
export type SnapshotSelectorHook<T> = <S>(sel: (s: T) => S, eq?: (a: S, b: S) => boolean) => S
|
|
53
55
|
|
|
54
56
|
/** The slots registry surface the client injects into. */
|
|
55
57
|
export interface ClientSlots {
|
|
@@ -88,19 +90,14 @@ export interface LiveProjectionValue {
|
|
|
88
90
|
* The client-side preset gate (acceptance #3): the board/strip render ONLY when
|
|
89
91
|
* the CURRENT session's row reports agentPreset === 'recursive'. A non-recursive
|
|
90
92
|
* session (or a session whose preset is undefined) shows NOTHING — not an empty
|
|
91
|
-
* board, not a spinner.
|
|
93
|
+
* board, not a spinner. Consumes the SessionListStateLike DIRECTLY (run 10 fix).
|
|
92
94
|
*/
|
|
93
|
-
export function isRecursivePreset(
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
if (current === undefined) return false
|
|
97
|
-
return snapshot.byId[current]?.agentPreset === 'recursive'
|
|
95
|
+
export function isRecursivePreset(state: SessionListStateLike): boolean {
|
|
96
|
+
if (state.current === undefined) return false
|
|
97
|
+
return state.byId[state.current]?.agentPreset === 'recursive'
|
|
98
98
|
}
|
|
99
99
|
|
|
100
100
|
/** The current session's cwd (the client passes it as a fallback hint; the host resolves the root). */
|
|
101
|
-
export function currentSessionCwd(
|
|
102
|
-
|
|
103
|
-
const current = snapshot.current
|
|
104
|
-
if (current === undefined) return ''
|
|
105
|
-
return snapshot.byId[current]?.cwd ?? ''
|
|
101
|
+
export function currentSessionCwd(state: SessionListStateLike): string {
|
|
102
|
+
return state.current === undefined ? '' : (state.byId[state.current]?.cwd ?? '')
|
|
106
103
|
}
|
package/src/client/index.ts
CHANGED
|
@@ -24,7 +24,7 @@ export type { LiveProjectionSnapshot } from './use-live.ts'
|
|
|
24
24
|
export { fetchLiveState, subscribeLiveEvents } from './host-api.ts'
|
|
25
25
|
export type { LiveRecursiveState, LiveRecursiveFrame, LiveScope } from './host-api.ts'
|
|
26
26
|
export { isRecursivePreset, currentSessionCwd } from './contract.ts'
|
|
27
|
-
export type { SessionSummaryRow, SessionListStateLike,
|
|
27
|
+
export type { SessionSummaryRow, SessionListStateLike, SnapshotSelectorHook, ClientContext } from './contract.ts'
|
|
28
28
|
|
|
29
29
|
/** Required services (fiber inject waiting — the runtime must be up first). */
|
|
30
30
|
export const inject = ['slots', 'sessions', 'workspaces', 'connection']
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared board open-state (run 08 R1): module-level store behind the launcher,
|
|
3
|
+
* the shell.overlay entry, and the board<->inspector swap. Zero session events,
|
|
4
|
+
* zero host mutation — the client remains a read-only projection (R5/R9).
|
|
5
|
+
*/
|
|
6
|
+
import { useSyncExternalStore } from 'react'
|
|
7
|
+
|
|
8
|
+
export interface BoardSelection {
|
|
9
|
+
worktreeRoot: string;
|
|
10
|
+
runId: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface BoardState {
|
|
14
|
+
open: boolean;
|
|
15
|
+
selection: BoardSelection | null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface BoardOpenState {
|
|
19
|
+
get(): BoardState;
|
|
20
|
+
openBoard(): void;
|
|
21
|
+
close(): void;
|
|
22
|
+
openInspector(selection: BoardSelection): void;
|
|
23
|
+
backToBoard(): void;
|
|
24
|
+
subscribe(fn: () => void): () => void;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function createBoardState(initial: BoardState = { open: false, selection: null }): BoardOpenState {
|
|
28
|
+
let state: BoardState = initial;
|
|
29
|
+
const listeners = new Set<() => void>();
|
|
30
|
+
const emit = (): void => { for (const fn of listeners) fn() };
|
|
31
|
+
return {
|
|
32
|
+
get: () => state,
|
|
33
|
+
openBoard: () => { state = { ...state, open: true }; emit(); },
|
|
34
|
+
close: () => { state = { ...state, open: false, selection: null }; emit(); },
|
|
35
|
+
openInspector: (selection) => { state = { ...state, open: true, selection }; emit(); },
|
|
36
|
+
backToBoard: () => { state = { ...state, selection: null }; emit(); },
|
|
37
|
+
subscribe: (fn) => { listeners.add(fn); return () => { listeners.delete(fn) }; },
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** App-wide singleton shared by the launcher, overlay, board, and inspector. */
|
|
42
|
+
export const boardState = createBoardState();
|
|
43
|
+
|
|
44
|
+
export function useBoardState(store: BoardOpenState = boardState): BoardState {
|
|
45
|
+
return useSyncExternalStore(store.subscribe, store.get, store.get);
|
|
46
|
+
}
|
package/src/client/slots.ts
CHANGED
|
@@ -1,51 +1,75 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Slot registrations (SP2 R1): the sidebar launcher, the
|
|
3
|
-
* the conversation.input.dock status strip, and the
|
|
2
|
+
* Slot registrations (SP2 R1 + run 08 R1/R3/R4): the sidebar launcher, the
|
|
3
|
+
* shell.overlay board, the conversation.input.dock status strip, and the
|
|
4
|
+
* settings.section page.
|
|
4
5
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
6
|
+
* Run 08: the launcher now OPENS the shared board store (onClick), and the
|
|
7
|
+
* shell.overlay entry renders the board gated on that store + the recursive
|
|
8
|
+
* preset, swapping to the drill-down inspector when a run is selected.
|
|
8
9
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
10
|
+
* Run 10 (LIVE BUG FIX): useSessions is a SnapshotSelectorHook (NOT a zero-arg
|
|
11
|
+
* thunk) — it is called WITH a selector, e.g. useSessions((s) => s) for the
|
|
12
|
+
* full SessionListStateLike. The old code called useSessions() with NO selector
|
|
13
|
+
* and wrapped the garbage in a fake {list:{getSnapshot}} — so the preset gate
|
|
14
|
+
* never saw the recursive preset and the board never mounted (launcher click
|
|
15
|
+
* did nothing). The board/strip now consume the SessionListStateLike directly.
|
|
13
16
|
*
|
|
14
17
|
* READ-ONLY (R9): the client only GETs the live host route; no mutation.
|
|
15
18
|
*/
|
|
16
19
|
import { createElement, type ReactNode } from 'react'
|
|
17
|
-
import type { ClientContext,
|
|
20
|
+
import type { ClientContext, SessionListStateLike, SnapshotSelectorHook } from './contract.ts'
|
|
18
21
|
import { isRecursivePreset, currentSessionCwd } from './contract.ts'
|
|
19
22
|
import { Board } from './board.tsx'
|
|
23
|
+
import { Inspector } from './inspector.tsx'
|
|
20
24
|
import { StatusStrip } from './strip.tsx'
|
|
21
25
|
import { RecursiveSettings } from './settings.tsx'
|
|
22
26
|
import { useLiveProjection } from './use-live.ts'
|
|
27
|
+
import { boardState, useBoardState } from './open-state.ts'
|
|
23
28
|
|
|
24
|
-
/** Structural standard-prop face for the root scope (useSessions
|
|
29
|
+
/** Structural standard-prop face for the root scope (useSessions/useWorkspaces). */
|
|
25
30
|
interface RootSlotProps {
|
|
26
|
-
useSessions?:
|
|
27
|
-
useWorkspaces?:
|
|
31
|
+
useSessions?: SnapshotSelectorHook<SessionListStateLike>
|
|
32
|
+
useWorkspaces?: SnapshotSelectorHook<unknown>
|
|
28
33
|
}
|
|
29
34
|
|
|
30
35
|
/** Structural standard-prop face for the session scope (sessionId + useSessions). */
|
|
31
36
|
interface SessionSlotProps {
|
|
32
37
|
sessionId?: string
|
|
33
|
-
useSessions?:
|
|
38
|
+
useSessions?: SnapshotSelectorHook<SessionListStateLike>
|
|
34
39
|
useProjection?: unknown
|
|
35
40
|
}
|
|
36
41
|
|
|
42
|
+
export type OverlayContent = 'hidden' | 'board' | 'inspector'
|
|
43
|
+
|
|
44
|
+
/** Gate: hidden unless open AND recursive preset; inspector when a run is selected. */
|
|
45
|
+
export function overlayContent(state: { open: boolean; selection: unknown }, sessions: SessionListStateLike): OverlayContent {
|
|
46
|
+
if (!state.open) return 'hidden'
|
|
47
|
+
if (!isRecursivePreset(sessions)) return 'hidden'
|
|
48
|
+
if (state.selection !== null) return 'inspector'
|
|
49
|
+
return 'board'
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* THE FIX (run 10): useSessions is a SELECTOR hook — always call it WITH a
|
|
54
|
+
* selector. The identity selector `(s) => s` yields the full SessionListStateLike.
|
|
55
|
+
* Exported so the spec can assert the selector is actually passed.
|
|
56
|
+
*/
|
|
57
|
+
export function useRecursiveSessions(useSessions: SnapshotSelectorHook<SessionListStateLike>): SessionListStateLike {
|
|
58
|
+
return useSessions((s) => s)
|
|
59
|
+
}
|
|
60
|
+
|
|
37
61
|
export function registerSlots(ctx: ClientContext): () => void {
|
|
38
62
|
const disposers: (() => void)[] = []
|
|
39
63
|
|
|
40
|
-
// Board launcher in the sidebar footer action list
|
|
64
|
+
// Board launcher in the sidebar footer action list — OPENS the shared board store (run 08 R1).
|
|
41
65
|
disposers.push(ctx.slots.inject('sidebar.footer.action', () => ctx.slots.register({
|
|
42
66
|
name: 'sidebar.footer.action',
|
|
43
67
|
id: 'recursive',
|
|
44
68
|
order: 50,
|
|
45
69
|
label: 'Recursive runs',
|
|
46
|
-
}, () => createElement('button', { className: 'rec-launcher', title: 'Recursive runs' }, '⧉'))))
|
|
70
|
+
}, () => createElement('button', { className: 'rec-launcher', title: 'Recursive runs', onClick: () => boardState.openBoard() }, '⧉'))))
|
|
47
71
|
|
|
48
|
-
// Board panel overlay (root scope, cross-session read) — gated on the recursive preset.
|
|
72
|
+
// Board panel overlay (root scope, cross-session read) — gated on the recursive preset + shared store.
|
|
49
73
|
disposers.push(ctx.slots.inject('shell.overlay', () => ctx.slots.register({
|
|
50
74
|
name: 'shell.overlay',
|
|
51
75
|
id: 'recursive-board',
|
|
@@ -53,7 +77,7 @@ export function registerSlots(ctx: ClientContext): () => void {
|
|
|
53
77
|
}, (props: RootSlotProps) => {
|
|
54
78
|
const useSessions = props?.useSessions
|
|
55
79
|
if (useSessions === undefined) return null
|
|
56
|
-
return createElement(
|
|
80
|
+
return createElement(RecursiveBoardOverlay, { useSessions })
|
|
57
81
|
})))
|
|
58
82
|
|
|
59
83
|
// Status strip in the composer input dock (session scope) — gated the same way.
|
|
@@ -79,29 +103,39 @@ export function registerSlots(ctx: ClientContext): () => void {
|
|
|
79
103
|
}
|
|
80
104
|
|
|
81
105
|
/**
|
|
82
|
-
* Board
|
|
83
|
-
*
|
|
84
|
-
*
|
|
106
|
+
* Board overlay: subscribes to the shared board store, gates on open + recursive
|
|
107
|
+
* preset, and swaps board <-> inspector (run 08 R1/R3).
|
|
108
|
+
*
|
|
109
|
+
* Run 10: consumes the full SessionListStateLike through the identity selector
|
|
110
|
+
* (useSessions((s) => s)); no fake {list:{getSnapshot}} wrapper.
|
|
85
111
|
*/
|
|
86
|
-
function
|
|
87
|
-
const
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
|
|
112
|
+
function RecursiveBoardOverlay({ useSessions }: { useSessions: SnapshotSelectorHook<SessionListStateLike> }): ReactNode {
|
|
113
|
+
const board = useBoardState()
|
|
114
|
+
const list = useRecursiveSessions(useSessions)
|
|
115
|
+
const content = overlayContent(board, list)
|
|
116
|
+
if (content === 'hidden') return null
|
|
117
|
+
const cwd = currentSessionCwd(list)
|
|
91
118
|
const scope = { sessionId: list.current, cwd }
|
|
92
119
|
const snapshot = useLiveProjection(scope)
|
|
93
|
-
|
|
120
|
+
if (content === 'inspector' && board.selection !== null) {
|
|
121
|
+
return createElement(Inspector, {
|
|
122
|
+
runId: board.selection.runId,
|
|
123
|
+
worktreeRoot: board.selection.worktreeRoot,
|
|
124
|
+
snapshot,
|
|
125
|
+
onBackToToolDetails: () => boardState.backToBoard(),
|
|
126
|
+
})
|
|
127
|
+
}
|
|
128
|
+
return createElement(Board, { snapshot, onOpenInspector: (sel) => boardState.openInspector(sel) })
|
|
94
129
|
}
|
|
95
130
|
|
|
96
131
|
/**
|
|
97
132
|
* Strip gate: same preset gate, session-scoped sessionId; the strip renders
|
|
98
|
-
* nothing for a non-recursive session.
|
|
133
|
+
* nothing for a non-recursive session. Run 10: useSessions((s) => s) selector.
|
|
99
134
|
*/
|
|
100
|
-
function RecursiveStripGate({ useSessions, sessionId }: { useSessions:
|
|
101
|
-
const list = useSessions
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
const cwd = currentSessionCwd(sessions)
|
|
135
|
+
function RecursiveStripGate({ useSessions, sessionId }: { useSessions: SnapshotSelectorHook<SessionListStateLike>; sessionId?: string }): ReactNode {
|
|
136
|
+
const list = useRecursiveSessions(useSessions)
|
|
137
|
+
if (!isRecursivePreset(list)) return null
|
|
138
|
+
const cwd = currentSessionCwd(list)
|
|
105
139
|
const scope = { sessionId, cwd }
|
|
106
140
|
const snapshot = useLiveProjection(scope)
|
|
107
141
|
return createElement(StatusStrip, { snapshot })
|
package/src/commands.ts
CHANGED
|
@@ -13,6 +13,7 @@ import { existsSync, readdirSync, statSync } from 'node:fs'
|
|
|
13
13
|
import { join } from 'node:path'
|
|
14
14
|
import type { RecursiveRuntime } from './runtime.ts'
|
|
15
15
|
import { resolveControlPlaneRoot } from './workspace.ts'
|
|
16
|
+
import { bootstrapScaffold } from './bootstrap.ts'
|
|
16
17
|
|
|
17
18
|
export type RecursiveVerb = 'status' | 'spec' | 'worktree' | 'init' | 'lock' | 'qa' | 'closeout' | 'addendum' | 'review' | 'scratch' | 'bootstrap' | 'list' | 'help'
|
|
18
19
|
|
|
@@ -65,6 +66,16 @@ export function executeRecursiveCommand(root: string, rawInput: string): Recursi
|
|
|
65
66
|
case 'help': {
|
|
66
67
|
return { kind: 'success', text: HELP_TEXT + ' — see /.recursive/RECURSIVE.md' }
|
|
67
68
|
}
|
|
69
|
+
case 'bootstrap': {
|
|
70
|
+
// R6 (run 09): manual repair path — idempotent upsert scaffold repair.
|
|
71
|
+
// Adds missing control-plane files/dirs + re-upserts marked blocks;
|
|
72
|
+
// never overwrites user content or touches run/ artifacts.
|
|
73
|
+
const result = bootstrapScaffold(root)
|
|
74
|
+
const summary = result.created.length === 0
|
|
75
|
+
? 'scaffold already complete (no changes)'
|
|
76
|
+
: 'created ' + result.created.length + ' item(s), ' + result.existing.length + ' existing'
|
|
77
|
+
return { kind: 'success', text: 'recursive-mode scaffold repair: ' + summary + ' — root ' + root }
|
|
78
|
+
}
|
|
68
79
|
case 'closeout': {
|
|
69
80
|
const phaseMatch = arg.match(/--phase\s+(\d{2})/)
|
|
70
81
|
const phase = phaseMatch?.[1] ?? ''
|