@nathapp/nax 0.22.4 → 0.24.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/bin/nax.ts +20 -2
- package/docs/tdd/strategies.md +97 -0
- package/nax/features/central-run-registry/prd.json +105 -0
- package/nax/features/diagnose/acceptance.test.ts +3 -1
- package/package.json +3 -3
- package/src/cli/diagnose.ts +1 -1
- package/src/cli/status-features.ts +55 -7
- package/src/commands/index.ts +1 -0
- package/src/commands/logs.ts +87 -17
- package/src/commands/runs.ts +220 -0
- package/src/config/schemas.ts +3 -0
- package/src/execution/crash-recovery.ts +30 -7
- package/src/execution/lifecycle/run-setup.ts +6 -1
- package/src/execution/runner.ts +8 -0
- package/src/execution/sequential-executor.ts +4 -0
- package/src/execution/status-writer.ts +42 -0
- package/src/pipeline/subscribers/events-writer.ts +121 -0
- package/src/pipeline/subscribers/registry.ts +73 -0
- package/src/version.ts +23 -0
- package/test/e2e/plan-analyze-run.test.ts +5 -0
- package/test/integration/cli/cli-diagnose.test.ts +3 -1
- package/test/integration/cli/cli-logs.test.ts +40 -17
- package/test/integration/execution/feature-status-write.test.ts +302 -0
- package/test/integration/execution/status-file-integration.test.ts +1 -1
- package/test/integration/execution/status-writer.test.ts +112 -0
- package/test/unit/cli-status-project-level.test.ts +283 -0
- package/test/unit/commands/logs.test.ts +63 -22
- package/test/unit/commands/runs.test.ts +303 -0
- package/test/unit/config/quality-commands-schema.test.ts +72 -0
- package/test/unit/execution/sfc-004-dead-code-cleanup.test.ts +89 -0
- package/test/unit/pipeline/subscribers/events-writer.test.ts +227 -0
- package/test/unit/pipeline/subscribers/registry.test.ts +149 -0
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for project-level status display in status-features.ts
|
|
3
|
+
*
|
|
4
|
+
* Verifies that nax status shows current run info from nax/status.json at the top.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
8
|
+
import { mkdirSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
9
|
+
import { tmpdir } from "node:os";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
import { displayFeatureStatus } from "../../src/cli/status";
|
|
12
|
+
import type { NaxStatusFile } from "../../src/execution/status-file";
|
|
13
|
+
import type { PRD } from "../../src/prd";
|
|
14
|
+
|
|
15
|
+
describe("displayFeatureStatus - Project-level status (nax/status.json)", () => {
|
|
16
|
+
let testDir: string;
|
|
17
|
+
let originalCwd: string;
|
|
18
|
+
let consoleOutput: string[];
|
|
19
|
+
const originalLog = console.log;
|
|
20
|
+
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
// Create temp directory for test
|
|
23
|
+
const rawTestDir = join(tmpdir(), `nax-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
24
|
+
mkdirSync(rawTestDir, { recursive: true });
|
|
25
|
+
testDir = realpathSync(rawTestDir);
|
|
26
|
+
originalCwd = process.cwd();
|
|
27
|
+
|
|
28
|
+
// Mock console.log to capture output
|
|
29
|
+
consoleOutput = [];
|
|
30
|
+
console.log = mock((message: string) => {
|
|
31
|
+
consoleOutput.push(message);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
afterEach(() => {
|
|
36
|
+
// Restore original CWD and console.log
|
|
37
|
+
process.chdir(originalCwd);
|
|
38
|
+
console.log = originalLog;
|
|
39
|
+
|
|
40
|
+
// Clean up test directory
|
|
41
|
+
if (testDir) {
|
|
42
|
+
rmSync(testDir, { recursive: true, force: true });
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
function createTestPRD(featureName: string): PRD {
|
|
47
|
+
return {
|
|
48
|
+
project: "test-project",
|
|
49
|
+
feature: featureName,
|
|
50
|
+
branchName: `feat/${featureName}`,
|
|
51
|
+
createdAt: "2026-01-01T00:00:00.000Z",
|
|
52
|
+
updatedAt: "2026-01-01T00:00:00.000Z",
|
|
53
|
+
userStories: [
|
|
54
|
+
{
|
|
55
|
+
id: "US-001",
|
|
56
|
+
title: "First story",
|
|
57
|
+
description: "Test story 1",
|
|
58
|
+
acceptanceCriteria: ["AC-1"],
|
|
59
|
+
tags: [],
|
|
60
|
+
dependencies: [],
|
|
61
|
+
status: "passed",
|
|
62
|
+
passes: true,
|
|
63
|
+
escalations: [],
|
|
64
|
+
attempts: 1,
|
|
65
|
+
},
|
|
66
|
+
],
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function createProjectStatus(feature: string, overrides: Partial<NaxStatusFile> = {}): NaxStatusFile {
|
|
71
|
+
return {
|
|
72
|
+
version: 1,
|
|
73
|
+
run: {
|
|
74
|
+
id: "run-2026-01-01T00-00-00-000Z",
|
|
75
|
+
feature,
|
|
76
|
+
startedAt: "2026-01-01T10:00:00.000Z",
|
|
77
|
+
status: "running",
|
|
78
|
+
dryRun: false,
|
|
79
|
+
pid: process.pid, // Use current process PID (alive)
|
|
80
|
+
...overrides.run,
|
|
81
|
+
},
|
|
82
|
+
progress: {
|
|
83
|
+
total: 5,
|
|
84
|
+
passed: 2,
|
|
85
|
+
failed: 1,
|
|
86
|
+
paused: 0,
|
|
87
|
+
blocked: 0,
|
|
88
|
+
pending: 2,
|
|
89
|
+
...overrides.progress,
|
|
90
|
+
},
|
|
91
|
+
cost: {
|
|
92
|
+
spent: 0.5678,
|
|
93
|
+
limit: null,
|
|
94
|
+
...overrides.cost,
|
|
95
|
+
},
|
|
96
|
+
current: {
|
|
97
|
+
storyId: "US-002",
|
|
98
|
+
title: "Test current story",
|
|
99
|
+
complexity: "medium",
|
|
100
|
+
tddStrategy: "red-green-refactor",
|
|
101
|
+
model: "claude-opus",
|
|
102
|
+
attempt: 1,
|
|
103
|
+
phase: "implementation",
|
|
104
|
+
},
|
|
105
|
+
iterations: 10,
|
|
106
|
+
updatedAt: "2026-01-01T10:30:00.000Z",
|
|
107
|
+
durationMs: 1800000,
|
|
108
|
+
...overrides,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
describe("AC1: Shows project-level current run info at top", () => {
|
|
113
|
+
test("displays current run info when active run exists in nax/status.json", async () => {
|
|
114
|
+
// Setup: Create project with feature and project-level status
|
|
115
|
+
const naxDir = join(testDir, "nax");
|
|
116
|
+
const featuresDir = join(naxDir, "features");
|
|
117
|
+
mkdirSync(featuresDir, { recursive: true });
|
|
118
|
+
writeFileSync(join(naxDir, "config.json"), "{}");
|
|
119
|
+
|
|
120
|
+
const featureDir = join(featuresDir, "current-feature");
|
|
121
|
+
mkdirSync(featureDir, { recursive: true });
|
|
122
|
+
const prd = createTestPRD("current-feature");
|
|
123
|
+
writeFileSync(join(featureDir, "prd.json"), JSON.stringify(prd, null, 2));
|
|
124
|
+
|
|
125
|
+
// Create project-level status.json
|
|
126
|
+
const projectStatus = createProjectStatus("current-feature", {
|
|
127
|
+
run: {
|
|
128
|
+
id: "run-2026-01-01T00-00-00-000Z",
|
|
129
|
+
feature: "current-feature",
|
|
130
|
+
startedAt: "2026-01-01T10:00:00.000Z",
|
|
131
|
+
status: "running",
|
|
132
|
+
dryRun: false,
|
|
133
|
+
pid: process.pid,
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
mkdirSync(join(naxDir), { recursive: true });
|
|
137
|
+
writeFileSync(join(naxDir, "status.json"), JSON.stringify(projectStatus, null, 2));
|
|
138
|
+
|
|
139
|
+
// Act
|
|
140
|
+
await displayFeatureStatus({ dir: testDir });
|
|
141
|
+
|
|
142
|
+
// Assert
|
|
143
|
+
const output = consoleOutput.join("\n");
|
|
144
|
+
expect(output).toContain("⚡ Currently Running:");
|
|
145
|
+
expect(output).toContain("current-feature");
|
|
146
|
+
expect(output).toContain("run-2026-01-01T00-00-00-000Z");
|
|
147
|
+
expect(output).toContain("2026-01-01T10:00:00.000Z");
|
|
148
|
+
expect(output).toContain("2/5 stories");
|
|
149
|
+
expect(output).toContain("$0.5678");
|
|
150
|
+
expect(output).toContain("US-002");
|
|
151
|
+
expect(output).toContain("Test current story");
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test("does not show current run info when nax/status.json missing", async () => {
|
|
155
|
+
// Setup: Create project with feature but no project-level status
|
|
156
|
+
const naxDir = join(testDir, "nax");
|
|
157
|
+
const featuresDir = join(naxDir, "features");
|
|
158
|
+
mkdirSync(featuresDir, { recursive: true });
|
|
159
|
+
writeFileSync(join(naxDir, "config.json"), "{}");
|
|
160
|
+
|
|
161
|
+
const featureDir = join(featuresDir, "no-status-feature");
|
|
162
|
+
mkdirSync(featureDir, { recursive: true });
|
|
163
|
+
const prd = createTestPRD("no-status-feature");
|
|
164
|
+
writeFileSync(join(featureDir, "prd.json"), JSON.stringify(prd, null, 2));
|
|
165
|
+
|
|
166
|
+
// Act (no status.json created)
|
|
167
|
+
await displayFeatureStatus({ dir: testDir });
|
|
168
|
+
|
|
169
|
+
// Assert
|
|
170
|
+
const output = consoleOutput.join("\n");
|
|
171
|
+
expect(output).not.toContain("⚡ Currently Running:");
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test("shows crashed run detected when PID is dead", async () => {
|
|
175
|
+
// Setup: Create project with feature and crashed status
|
|
176
|
+
const naxDir = join(testDir, "nax");
|
|
177
|
+
const featuresDir = join(naxDir, "features");
|
|
178
|
+
mkdirSync(featuresDir, { recursive: true });
|
|
179
|
+
writeFileSync(join(naxDir, "config.json"), "{}");
|
|
180
|
+
|
|
181
|
+
const featureDir = join(featuresDir, "crashed-feature");
|
|
182
|
+
mkdirSync(featureDir, { recursive: true });
|
|
183
|
+
const prd = createTestPRD("crashed-feature");
|
|
184
|
+
writeFileSync(join(featureDir, "prd.json"), JSON.stringify(prd, null, 2));
|
|
185
|
+
|
|
186
|
+
// Create project-level status.json with dead PID
|
|
187
|
+
const projectStatus = createProjectStatus("crashed-feature", {
|
|
188
|
+
run: {
|
|
189
|
+
id: "run-2026-01-01T00-00-00-000Z",
|
|
190
|
+
feature: "crashed-feature",
|
|
191
|
+
startedAt: "2026-01-01T10:00:00.000Z",
|
|
192
|
+
status: "running",
|
|
193
|
+
dryRun: false,
|
|
194
|
+
pid: 999999, // Non-existent PID
|
|
195
|
+
},
|
|
196
|
+
});
|
|
197
|
+
writeFileSync(join(naxDir, "status.json"), JSON.stringify(projectStatus, null, 2));
|
|
198
|
+
|
|
199
|
+
// Act
|
|
200
|
+
await displayFeatureStatus({ dir: testDir });
|
|
201
|
+
|
|
202
|
+
// Assert
|
|
203
|
+
const output = consoleOutput.join("\n");
|
|
204
|
+
expect(output).toContain("💥 Crashed Run Detected:");
|
|
205
|
+
expect(output).toContain("999999");
|
|
206
|
+
expect(output).toContain("dead");
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
test("shows crash info when run status is 'crashed'", async () => {
|
|
210
|
+
// Setup: Create project with feature and crashed status
|
|
211
|
+
const naxDir = join(testDir, "nax");
|
|
212
|
+
const featuresDir = join(naxDir, "features");
|
|
213
|
+
mkdirSync(featuresDir, { recursive: true });
|
|
214
|
+
writeFileSync(join(naxDir, "config.json"), "{}");
|
|
215
|
+
|
|
216
|
+
const featureDir = join(featuresDir, "crashed-feature");
|
|
217
|
+
mkdirSync(featureDir, { recursive: true });
|
|
218
|
+
const prd = createTestPRD("crashed-feature");
|
|
219
|
+
writeFileSync(join(featureDir, "prd.json"), JSON.stringify(prd, null, 2));
|
|
220
|
+
|
|
221
|
+
// Create project-level status.json with crashed status
|
|
222
|
+
const projectStatus = createProjectStatus("crashed-feature", {
|
|
223
|
+
run: {
|
|
224
|
+
id: "run-2026-01-01T00-00-00-000Z",
|
|
225
|
+
feature: "crashed-feature",
|
|
226
|
+
startedAt: "2026-01-01T10:00:00.000Z",
|
|
227
|
+
status: "crashed",
|
|
228
|
+
dryRun: false,
|
|
229
|
+
pid: 12345,
|
|
230
|
+
crashedAt: "2026-01-01T10:15:00.000Z",
|
|
231
|
+
crashSignal: "SIGKILL",
|
|
232
|
+
},
|
|
233
|
+
});
|
|
234
|
+
writeFileSync(join(naxDir, "status.json"), JSON.stringify(projectStatus, null, 2));
|
|
235
|
+
|
|
236
|
+
// Act
|
|
237
|
+
await displayFeatureStatus({ dir: testDir });
|
|
238
|
+
|
|
239
|
+
// Assert
|
|
240
|
+
const output = consoleOutput.join("\n");
|
|
241
|
+
expect(output).toContain("💥 Crashed Run Detected:");
|
|
242
|
+
expect(output).toContain("SIGKILL");
|
|
243
|
+
expect(output).toContain("2026-01-01T10:15:00.000Z");
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
describe("AC2: Shows per-feature historical status", () => {
|
|
248
|
+
test("shows feature-level status from nax/features/<feature>/status.json", async () => {
|
|
249
|
+
// Setup: Create project with feature and feature-level status
|
|
250
|
+
const naxDir = join(testDir, "nax");
|
|
251
|
+
const featuresDir = join(naxDir, "features");
|
|
252
|
+
mkdirSync(featuresDir, { recursive: true });
|
|
253
|
+
writeFileSync(join(naxDir, "config.json"), "{}");
|
|
254
|
+
|
|
255
|
+
const featureDir = join(featuresDir, "test-feature");
|
|
256
|
+
mkdirSync(featureDir, { recursive: true });
|
|
257
|
+
const prd = createTestPRD("test-feature");
|
|
258
|
+
writeFileSync(join(featureDir, "prd.json"), JSON.stringify(prd, null, 2));
|
|
259
|
+
|
|
260
|
+
// Create feature-level status.json
|
|
261
|
+
const featureStatus = createProjectStatus("test-feature", {
|
|
262
|
+
run: {
|
|
263
|
+
id: "run-feature-level",
|
|
264
|
+
feature: "test-feature",
|
|
265
|
+
startedAt: "2026-01-01T09:00:00.000Z",
|
|
266
|
+
status: "completed",
|
|
267
|
+
dryRun: false,
|
|
268
|
+
pid: 12345,
|
|
269
|
+
},
|
|
270
|
+
});
|
|
271
|
+
writeFileSync(join(featureDir, "status.json"), JSON.stringify(featureStatus, null, 2));
|
|
272
|
+
|
|
273
|
+
// Act
|
|
274
|
+
await displayFeatureStatus({ dir: testDir, feature: "test-feature" });
|
|
275
|
+
|
|
276
|
+
// Assert
|
|
277
|
+
const output = consoleOutput.join("\n");
|
|
278
|
+
// Feature details view should show the feature-level status
|
|
279
|
+
expect(output).toContain("test-feature");
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
});
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
17
17
|
import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
18
18
|
import { join } from "node:path";
|
|
19
|
-
import { type LogsOptions, logsCommand } from "../../../src/commands/logs";
|
|
19
|
+
import { type LogsOptions, _deps, logsCommand } from "../../../src/commands/logs";
|
|
20
20
|
|
|
21
21
|
const TEST_WORKSPACE = join(import.meta.dir, "..", "..", "tmp", "logs-test");
|
|
22
22
|
|
|
@@ -110,13 +110,43 @@ function cleanup(projectDir: string) {
|
|
|
110
110
|
|
|
111
111
|
describe("logsCommand", () => {
|
|
112
112
|
let projectDir: string;
|
|
113
|
+
let registryDir: string;
|
|
114
|
+
let originalGetRunsDir: () => string;
|
|
113
115
|
|
|
114
116
|
beforeEach(() => {
|
|
115
117
|
projectDir = setupTestProject("test-feature");
|
|
118
|
+
|
|
119
|
+
// Set up a temp registry dir and override _deps
|
|
120
|
+
registryDir = join(TEST_WORKSPACE, `registry-${Date.now()}`);
|
|
121
|
+
mkdirSync(registryDir, { recursive: true });
|
|
122
|
+
originalGetRunsDir = _deps.getRunsDir;
|
|
123
|
+
_deps.getRunsDir = () => registryDir;
|
|
124
|
+
|
|
125
|
+
// Create registry entries pointing to the test runs
|
|
126
|
+
const runsDir = join(projectDir, "nax", "features", "test-feature", "runs");
|
|
127
|
+
|
|
128
|
+
for (const runId of ["2026-02-27T10-00-00", "2026-02-26T09-00-00"]) {
|
|
129
|
+
const entryDir = join(registryDir, `testproject-test-feature-${runId}`);
|
|
130
|
+
mkdirSync(entryDir, { recursive: true });
|
|
131
|
+
writeFileSync(
|
|
132
|
+
join(entryDir, "meta.json"),
|
|
133
|
+
JSON.stringify({
|
|
134
|
+
runId,
|
|
135
|
+
project: "testproject",
|
|
136
|
+
feature: "test-feature",
|
|
137
|
+
workdir: projectDir,
|
|
138
|
+
statusPath: join(projectDir, "nax", "features", "test-feature", "status.json"),
|
|
139
|
+
eventsDir: runsDir,
|
|
140
|
+
registeredAt: "2026-02-27T10:00:00.000Z",
|
|
141
|
+
}),
|
|
142
|
+
);
|
|
143
|
+
}
|
|
116
144
|
});
|
|
117
145
|
|
|
118
146
|
afterEach(() => {
|
|
147
|
+
_deps.getRunsDir = originalGetRunsDir;
|
|
119
148
|
cleanup(projectDir);
|
|
149
|
+
cleanup(registryDir);
|
|
120
150
|
});
|
|
121
151
|
|
|
122
152
|
describe("default behavior (latest run formatted)", () => {
|
|
@@ -259,33 +289,46 @@ describe("logsCommand", () => {
|
|
|
259
289
|
});
|
|
260
290
|
});
|
|
261
291
|
|
|
262
|
-
describe("--run (
|
|
263
|
-
test("displays
|
|
264
|
-
const options: LogsOptions = {
|
|
265
|
-
dir: projectDir,
|
|
266
|
-
run: "2026-02-26T09-00-00",
|
|
267
|
-
};
|
|
292
|
+
describe("--run (registry-based run selection)", () => {
|
|
293
|
+
test("displays run resolved from central registry by exact runId", async () => {
|
|
294
|
+
const options: LogsOptions = { run: "2026-02-26T09-00-00" };
|
|
268
295
|
|
|
269
|
-
// Should display the older run
|
|
270
296
|
await expect(logsCommand(options)).resolves.toBeUndefined();
|
|
271
297
|
});
|
|
272
298
|
|
|
273
|
-
test("
|
|
274
|
-
const options: LogsOptions = {
|
|
275
|
-
dir: projectDir,
|
|
276
|
-
run: "2026-01-01T00-00-00",
|
|
277
|
-
};
|
|
299
|
+
test("displays run resolved from central registry by prefix match", async () => {
|
|
300
|
+
const options: LogsOptions = { run: "2026-02-26" };
|
|
278
301
|
|
|
279
|
-
|
|
302
|
+
// Should match "2026-02-26T09-00-00"
|
|
303
|
+
await expect(logsCommand(options)).resolves.toBeUndefined();
|
|
280
304
|
});
|
|
281
305
|
|
|
282
|
-
test("
|
|
283
|
-
const options: LogsOptions = {
|
|
284
|
-
dir: projectDir,
|
|
285
|
-
run: "2026-02-26",
|
|
286
|
-
};
|
|
306
|
+
test("throws with clear error when runId not found in registry", async () => {
|
|
307
|
+
const options: LogsOptions = { run: "2026-01-01T00-00-00" };
|
|
287
308
|
|
|
288
|
-
|
|
309
|
+
await expect(logsCommand(options)).rejects.toThrow(/run not found in registry/i);
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
test("shows unavailable message when eventsDir does not exist", async () => {
|
|
313
|
+
// Add a registry entry pointing to a non-existent eventsDir
|
|
314
|
+
const entryDir = join(registryDir, "proj-feat-ghost-run");
|
|
315
|
+
mkdirSync(entryDir, { recursive: true });
|
|
316
|
+
writeFileSync(
|
|
317
|
+
join(entryDir, "meta.json"),
|
|
318
|
+
JSON.stringify({
|
|
319
|
+
runId: "ghost-run",
|
|
320
|
+
project: "proj",
|
|
321
|
+
feature: "feat",
|
|
322
|
+
workdir: "/nonexistent",
|
|
323
|
+
statusPath: "/nonexistent/nax/features/feat/status.json",
|
|
324
|
+
eventsDir: "/nonexistent/nax/features/feat/runs",
|
|
325
|
+
registeredAt: "2026-01-01T00:00:00.000Z",
|
|
326
|
+
}),
|
|
327
|
+
);
|
|
328
|
+
|
|
329
|
+
const options: LogsOptions = { run: "ghost-run" };
|
|
330
|
+
|
|
331
|
+
// Should resolve without throwing — prints unavailable message
|
|
289
332
|
await expect(logsCommand(options)).resolves.toBeUndefined();
|
|
290
333
|
});
|
|
291
334
|
});
|
|
@@ -350,7 +393,6 @@ describe("logsCommand", () => {
|
|
|
350
393
|
|
|
351
394
|
test("--run + --story + --level", async () => {
|
|
352
395
|
const options: LogsOptions = {
|
|
353
|
-
dir: projectDir,
|
|
354
396
|
run: "2026-02-27T10-00-00",
|
|
355
397
|
story: "US-001",
|
|
356
398
|
level: "info",
|
|
@@ -361,7 +403,6 @@ describe("logsCommand", () => {
|
|
|
361
403
|
|
|
362
404
|
test("all filters combined", async () => {
|
|
363
405
|
const options: LogsOptions = {
|
|
364
|
-
dir: projectDir,
|
|
365
406
|
run: "2026-02-27T10-00-00",
|
|
366
407
|
story: "US-001",
|
|
367
408
|
level: "info",
|