@wkronmiller/lisa 0.1.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/LICENSE +21 -0
- package/README.md +407 -0
- package/bin/lisa-runtime.js +8797 -0
- package/bin/lisa.js +21 -0
- package/completion.ts +58 -0
- package/install.ps1 +51 -0
- package/install.sh +93 -0
- package/lisa.ts +6 -0
- package/package.json +66 -0
- package/skills/README.md +28 -0
- package/skills/claude-code/CLAUDE.md +151 -0
- package/skills/codex/AGENTS.md +151 -0
- package/skills/gemini/GEMINI.md +151 -0
- package/skills/opencode/AGENTS.md +152 -0
- package/src/cli.ts +85 -0
- package/src/harness/base-adapter.ts +47 -0
- package/src/harness/claude-code.ts +106 -0
- package/src/harness/codex.ts +80 -0
- package/src/harness/command.ts +173 -0
- package/src/harness/gemini.ts +74 -0
- package/src/harness/opencode.ts +84 -0
- package/src/harness/registry.ts +29 -0
- package/src/harness/runner.ts +19 -0
- package/src/harness/types.ts +73 -0
- package/src/output-mode.ts +32 -0
- package/src/skill/artifacts.ts +174 -0
- package/src/skill/cli.ts +29 -0
- package/src/skill/install.ts +317 -0
- package/src/spec/agent-guidance.ts +466 -0
- package/src/spec/cli.ts +151 -0
- package/src/spec/commands/check.ts +1 -0
- package/src/spec/commands/config.ts +146 -0
- package/src/spec/commands/diff.ts +1 -0
- package/src/spec/commands/generate.ts +1 -0
- package/src/spec/commands/guide.ts +1 -0
- package/src/spec/commands/harness-list.ts +36 -0
- package/src/spec/commands/implement.ts +1 -0
- package/src/spec/commands/import.ts +1 -0
- package/src/spec/commands/init.ts +1 -0
- package/src/spec/commands/status.ts +87 -0
- package/src/spec/config.ts +63 -0
- package/src/spec/diff.ts +791 -0
- package/src/spec/extensions/benchmark.ts +347 -0
- package/src/spec/extensions/registry.ts +59 -0
- package/src/spec/extensions/types.ts +56 -0
- package/src/spec/grammar/index.ts +14 -0
- package/src/spec/grammar/parser.ts +443 -0
- package/src/spec/grammar/types.ts +70 -0
- package/src/spec/grammar/validator.ts +104 -0
- package/src/spec/loader.ts +174 -0
- package/src/spec/local-config.ts +59 -0
- package/src/spec/parser.ts +226 -0
- package/src/spec/path-utils.ts +73 -0
- package/src/spec/planner.ts +299 -0
- package/src/spec/prompt-renderer.ts +318 -0
- package/src/spec/skill-content.ts +119 -0
- package/src/spec/types.ts +239 -0
- package/src/spec/validator.ts +443 -0
- package/src/spec/workflows/check.ts +1534 -0
- package/src/spec/workflows/diff.ts +209 -0
- package/src/spec/workflows/generate.ts +1270 -0
- package/src/spec/workflows/guide.ts +190 -0
- package/src/spec/workflows/implement.ts +797 -0
- package/src/spec/workflows/import.ts +986 -0
- package/src/spec/workflows/init.ts +548 -0
- package/src/spec/workflows/status.ts +22 -0
- package/src/spec/workspace.ts +541 -0
- package/uninstall.ps1 +21 -0
- package/uninstall.sh +22 -0
|
@@ -0,0 +1,548 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "fs";
|
|
2
|
+
import { dirname, join, relative, resolve } from "path";
|
|
3
|
+
import { createInterface } from "readline/promises";
|
|
4
|
+
import { stdin as input, stdout as output } from "process";
|
|
5
|
+
|
|
6
|
+
import { inspectHarnesses } from "../../harness/registry";
|
|
7
|
+
import type { HarnessInspection, Stage } from "../../harness/types";
|
|
8
|
+
import { resolveOutputMode } from "../../output-mode";
|
|
9
|
+
import { installLisaSkills, type SkillInstallResult } from "../../skill/install";
|
|
10
|
+
import { initializeExternalWorkspace, resolveBootstrapWorkspaceRoot, resolveWorkspaceLayoutFromRoot } from "../workspace";
|
|
11
|
+
|
|
12
|
+
interface InitCommandOptions {
|
|
13
|
+
force: boolean;
|
|
14
|
+
external: boolean;
|
|
15
|
+
globalPackRoots: string[];
|
|
16
|
+
help: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface DefaultHarnessSelection {
|
|
20
|
+
generate: string;
|
|
21
|
+
implement: string;
|
|
22
|
+
check: string;
|
|
23
|
+
import: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface InitWarnings {
|
|
27
|
+
noPortableHarnessDetected: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface ScaffoldFile {
|
|
31
|
+
path: string;
|
|
32
|
+
content: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface InitCommandIO {
|
|
36
|
+
ask?(question: string): Promise<string>;
|
|
37
|
+
print(message: string): void;
|
|
38
|
+
error(message: string): void;
|
|
39
|
+
close?(): void;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface InitCommandDeps {
|
|
43
|
+
inspectHarnesses?: (cwd: string) => Promise<HarnessInspection[]>;
|
|
44
|
+
installLocalSkills?: (workspaceRoot: string) => Promise<SkillInstallResult[]>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const PROFILE_NAMES: Record<Stage, string> = {
|
|
48
|
+
generate: "planner",
|
|
49
|
+
implement: "implementer",
|
|
50
|
+
check: "verifier",
|
|
51
|
+
import: "importer",
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const PREFERRED_HARNESSES_BY_STAGE: Record<Stage, string[]> = {
|
|
55
|
+
generate: ["opencode", "claude-code", "codex", "gemini"],
|
|
56
|
+
implement: ["opencode", "codex", "claude-code", "gemini"],
|
|
57
|
+
check: ["claude-code", "codex", "gemini", "opencode"],
|
|
58
|
+
import: ["opencode", "claude-code", "codex", "gemini"],
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const PROJECT_MD_TEMPLATE = `---
|
|
62
|
+
# Project-level defaults for Lisa spec workflows.
|
|
63
|
+
# Values here are included as harness context for all stages.
|
|
64
|
+
# Individual spec frontmatter values take precedence over these defaults.
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
# Project
|
|
68
|
+
|
|
69
|
+
- language: <!-- e.g. typescript, java, python -->
|
|
70
|
+
- framework: <!-- e.g. express, spring-boot, django -->
|
|
71
|
+
- default_code_paths: <!-- e.g. src/** -->
|
|
72
|
+
- default_test_commands: <!-- e.g. bun test ./tests -->
|
|
73
|
+
`;
|
|
74
|
+
|
|
75
|
+
const STARTER_FILES = [
|
|
76
|
+
["backend", ".gitkeep"],
|
|
77
|
+
["frontend", ".gitkeep"],
|
|
78
|
+
["environments", ".gitkeep"],
|
|
79
|
+
] as const;
|
|
80
|
+
|
|
81
|
+
const ALL_GUIDANCE_TARGETS = ["AGENTS.md", "CLAUDE.md", "GEMINI.md"] as const;
|
|
82
|
+
|
|
83
|
+
function createConsoleIO(): InitCommandIO {
|
|
84
|
+
if (resolveOutputMode() !== "interactive" || !input.isTTY || !output.isTTY) {
|
|
85
|
+
return {
|
|
86
|
+
print(message: string): void {
|
|
87
|
+
console.log(message);
|
|
88
|
+
},
|
|
89
|
+
error(message: string): void {
|
|
90
|
+
console.error(message);
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const rl = createInterface({ input, output });
|
|
96
|
+
return {
|
|
97
|
+
async ask(question: string): Promise<string> {
|
|
98
|
+
return rl.question(question);
|
|
99
|
+
},
|
|
100
|
+
print(message: string): void {
|
|
101
|
+
console.log(message);
|
|
102
|
+
},
|
|
103
|
+
error(message: string): void {
|
|
104
|
+
console.error(message);
|
|
105
|
+
},
|
|
106
|
+
close(): void {
|
|
107
|
+
rl.close();
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function printInitHelp(io: InitCommandIO): void {
|
|
113
|
+
io.print(`Lisa spec init
|
|
114
|
+
|
|
115
|
+
Usage:
|
|
116
|
+
lisa spec init [options]
|
|
117
|
+
|
|
118
|
+
Options:
|
|
119
|
+
--external Keep specs and runtime state outside the repository
|
|
120
|
+
--global-pack <path>
|
|
121
|
+
Attach an explicit global pack root for external mode (repeatable)
|
|
122
|
+
--force Overwrite the generated .specs/config.yaml scaffold
|
|
123
|
+
--help, -h Show this help
|
|
124
|
+
`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function parseInitArgs(args: string[]): InitCommandOptions {
|
|
128
|
+
const options: InitCommandOptions = {
|
|
129
|
+
force: false,
|
|
130
|
+
external: false,
|
|
131
|
+
globalPackRoots: [],
|
|
132
|
+
help: false,
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
136
|
+
const arg = args[index];
|
|
137
|
+
if (!arg) {
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (arg === "--help" || arg === "-h") {
|
|
142
|
+
options.help = true;
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (arg === "--force") {
|
|
147
|
+
options.force = true;
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (arg === "--external") {
|
|
152
|
+
options.external = true;
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (arg === "--global-pack") {
|
|
157
|
+
const value = args[index + 1];
|
|
158
|
+
if (!value) {
|
|
159
|
+
throw new Error("--global-pack requires a value.");
|
|
160
|
+
}
|
|
161
|
+
options.globalPackRoots.push(resolve(value));
|
|
162
|
+
index += 1;
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
throw new Error(`Unknown lisa spec init option: ${arg}`);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (!options.external && options.globalPackRoots.length > 0) {
|
|
170
|
+
throw new Error("--global-pack can only be used with --external.");
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return options;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function toPortablePath(path: string, workspacePath: string): string {
|
|
177
|
+
return relative(workspacePath, path).split("\\").join("/") || ".";
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function normalizeBooleanAnswer(answer: string, fallback = false): boolean {
|
|
181
|
+
const normalized = answer.trim().toLowerCase();
|
|
182
|
+
if (!normalized) {
|
|
183
|
+
return fallback;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (["y", "yes"].includes(normalized)) {
|
|
187
|
+
return true;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (["n", "no"].includes(normalized)) {
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return fallback;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async function promptYesNo(io: InitCommandIO, question: string, fallback = false): Promise<boolean> {
|
|
198
|
+
if (!io.ask) {
|
|
199
|
+
return fallback;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const label = fallback ? "Y/n" : "y/N";
|
|
203
|
+
const answer = await io.ask(`${question} [${label}]: `);
|
|
204
|
+
return normalizeBooleanAnswer(answer, fallback);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function selectDefaultHarness(
|
|
208
|
+
inspections: HarnessInspection[],
|
|
209
|
+
stage: Stage,
|
|
210
|
+
): string {
|
|
211
|
+
const availableHarnesses = new Set(
|
|
212
|
+
inspections
|
|
213
|
+
.filter((inspection) => (
|
|
214
|
+
inspection.availability.available
|
|
215
|
+
&& inspection.adapter.supportedStages.includes(stage)
|
|
216
|
+
))
|
|
217
|
+
.map((inspection) => inspection.adapter.id),
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
for (const harnessId of PREFERRED_HARNESSES_BY_STAGE[stage]) {
|
|
221
|
+
if (availableHarnesses.has(harnessId)) {
|
|
222
|
+
return harnessId;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return PREFERRED_HARNESSES_BY_STAGE[stage][0];
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function selectDefaultHarnessesForWorkspace(
|
|
230
|
+
inspections: HarnessInspection[],
|
|
231
|
+
): DefaultHarnessSelection {
|
|
232
|
+
return {
|
|
233
|
+
generate: selectDefaultHarness(inspections, "generate"),
|
|
234
|
+
implement: selectDefaultHarness(inspections, "implement"),
|
|
235
|
+
check: selectDefaultHarness(inspections, "check"),
|
|
236
|
+
import: selectDefaultHarness(inspections, "import"),
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function collectInitWarnings(
|
|
241
|
+
inspections: HarnessInspection[],
|
|
242
|
+
): InitWarnings {
|
|
243
|
+
const noPortableHarnessDetected = !inspections.some((inspection) => (
|
|
244
|
+
inspection.availability.available
|
|
245
|
+
));
|
|
246
|
+
|
|
247
|
+
return {
|
|
248
|
+
noPortableHarnessDetected,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function renderHarnessConfig(): Record<string, { command: string; args: string[] }> {
|
|
253
|
+
return {
|
|
254
|
+
codex: {
|
|
255
|
+
command: "codex",
|
|
256
|
+
args: [],
|
|
257
|
+
},
|
|
258
|
+
opencode: {
|
|
259
|
+
command: "opencode",
|
|
260
|
+
args: [],
|
|
261
|
+
},
|
|
262
|
+
"claude-code": {
|
|
263
|
+
command: "claude",
|
|
264
|
+
args: [],
|
|
265
|
+
},
|
|
266
|
+
gemini: {
|
|
267
|
+
command: "gemini",
|
|
268
|
+
args: [],
|
|
269
|
+
},
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function formatLocalSkillResult(result: SkillInstallResult, workspacePath: string): string {
|
|
274
|
+
const destination = toPortablePath(result.destinationPath, workspacePath);
|
|
275
|
+
if (result.state === "skipped" || result.state === "failed") {
|
|
276
|
+
return `- ${result.adapterId}: ${result.state} ${destination} (${result.reason ?? "unavailable"})`;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return `- ${result.adapterId}: ${result.state} ${destination}`;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function renderConfig(
|
|
283
|
+
selection: DefaultHarnessSelection,
|
|
284
|
+
enableAgentGuidance: boolean,
|
|
285
|
+
guidanceTargets: readonly string[] = ALL_GUIDANCE_TARGETS,
|
|
286
|
+
): string {
|
|
287
|
+
return `${Bun.YAML.stringify({
|
|
288
|
+
default_stage_profiles: {
|
|
289
|
+
generate: PROFILE_NAMES.generate,
|
|
290
|
+
implement: PROFILE_NAMES.implement,
|
|
291
|
+
check: PROFILE_NAMES.check,
|
|
292
|
+
import: PROFILE_NAMES.import,
|
|
293
|
+
},
|
|
294
|
+
profiles: {
|
|
295
|
+
planner: {
|
|
296
|
+
harness: selection.generate,
|
|
297
|
+
allow_edits: false,
|
|
298
|
+
},
|
|
299
|
+
implementer: {
|
|
300
|
+
harness: selection.implement,
|
|
301
|
+
allow_edits: true,
|
|
302
|
+
},
|
|
303
|
+
verifier: {
|
|
304
|
+
harness: selection.check,
|
|
305
|
+
allow_edits: false,
|
|
306
|
+
},
|
|
307
|
+
importer: {
|
|
308
|
+
harness: selection.import,
|
|
309
|
+
allow_edits: false,
|
|
310
|
+
},
|
|
311
|
+
},
|
|
312
|
+
harnesses: renderHarnessConfig(),
|
|
313
|
+
agent_guidance: {
|
|
314
|
+
enabled: enableAgentGuidance,
|
|
315
|
+
targets: guidanceTargets,
|
|
316
|
+
},
|
|
317
|
+
}).trimEnd()}
|
|
318
|
+
`;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function buildStarterFiles(specRoot: string): ScaffoldFile[] {
|
|
322
|
+
return [
|
|
323
|
+
...STARTER_FILES.map(([directory, filename]) => ({
|
|
324
|
+
path: join(specRoot, directory, filename),
|
|
325
|
+
content: "",
|
|
326
|
+
})),
|
|
327
|
+
{
|
|
328
|
+
path: join(specRoot, "project.md"),
|
|
329
|
+
content: PROJECT_MD_TEMPLATE,
|
|
330
|
+
},
|
|
331
|
+
];
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function writeScaffoldFiles(files: ScaffoldFile[]): void {
|
|
335
|
+
for (const file of files) {
|
|
336
|
+
mkdirSync(dirname(file.path), { recursive: true });
|
|
337
|
+
writeFileSync(file.path, file.content);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function ensureGitignoreEntry(workspacePath: string, entry: string): boolean {
|
|
342
|
+
const gitignorePath = join(workspacePath, ".gitignore");
|
|
343
|
+
const existing = existsSync(gitignorePath) ? readFileSync(gitignorePath, "utf8") : "";
|
|
344
|
+
const lines = existing.split("\n");
|
|
345
|
+
|
|
346
|
+
if (lines.some((line) => line.trim() === entry)) {
|
|
347
|
+
return false;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const separator = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
|
|
351
|
+
writeFileSync(gitignorePath, `${existing}${separator}${entry}\n`);
|
|
352
|
+
return true;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function hasExistingRepoSpecWorkspace(workspacePath: string): boolean {
|
|
356
|
+
const specRoot = join(workspacePath, ".specs");
|
|
357
|
+
if (!existsSync(specRoot)) {
|
|
358
|
+
return false;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if (existsSync(join(specRoot, "config.yaml"))) {
|
|
362
|
+
return true;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
for (const directory of ["backend", "frontend", "environments"]) {
|
|
366
|
+
const directoryPath = join(specRoot, directory);
|
|
367
|
+
if (!existsSync(directoryPath)) {
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
if (readdirSync(directoryPath).some((entry) => entry !== ".gitkeep")) {
|
|
372
|
+
return true;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
return false;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
export async function runSpecInitCommand(
|
|
380
|
+
args: string[],
|
|
381
|
+
cwd = process.cwd(),
|
|
382
|
+
io: InitCommandIO = createConsoleIO(),
|
|
383
|
+
deps: InitCommandDeps = {},
|
|
384
|
+
): Promise<number> {
|
|
385
|
+
try {
|
|
386
|
+
const options = parseInitArgs(args);
|
|
387
|
+
if (options.help) {
|
|
388
|
+
printInitHelp(io);
|
|
389
|
+
return 0;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const workspacePath = resolveBootstrapWorkspaceRoot(cwd);
|
|
393
|
+
if (options.external && hasExistingRepoSpecWorkspace(workspacePath)) {
|
|
394
|
+
io.error("`lisa spec init --external` refuses to hide an existing repo-local `.specs` workspace. Migrate or remove the repo-local Lisa workspace first.");
|
|
395
|
+
return 1;
|
|
396
|
+
}
|
|
397
|
+
const existingLayout = resolveWorkspaceLayoutFromRoot(workspacePath);
|
|
398
|
+
const globalPackRoots = options.globalPackRoots.length > 0
|
|
399
|
+
? options.globalPackRoots
|
|
400
|
+
: existingLayout.storageMode === "external"
|
|
401
|
+
? existingLayout.globalPackRoots
|
|
402
|
+
: [];
|
|
403
|
+
const layout = options.external
|
|
404
|
+
? initializeExternalWorkspace(workspacePath, globalPackRoots)
|
|
405
|
+
: existingLayout;
|
|
406
|
+
const configPath = layout.configPath;
|
|
407
|
+
const specRoot = dirname(configPath);
|
|
408
|
+
const shouldWriteConfig = options.force || !existsSync(configPath);
|
|
409
|
+
|
|
410
|
+
mkdirSync(join(specRoot, "backend"), { recursive: true });
|
|
411
|
+
mkdirSync(join(specRoot, "frontend"), { recursive: true });
|
|
412
|
+
mkdirSync(join(specRoot, "environments"), { recursive: true });
|
|
413
|
+
if (layout.worktreeSpecRoot) {
|
|
414
|
+
mkdirSync(layout.worktreeSpecRoot, { recursive: true });
|
|
415
|
+
}
|
|
416
|
+
mkdirSync(layout.runtimeRoot, { recursive: true });
|
|
417
|
+
mkdirSync(layout.snapshotRoot, { recursive: true });
|
|
418
|
+
|
|
419
|
+
const scaffoldFiles = buildStarterFiles(specRoot).filter((file) => !existsSync(file.path));
|
|
420
|
+
let defaultHarnesses: DefaultHarnessSelection | undefined;
|
|
421
|
+
let warnings: InitWarnings | undefined;
|
|
422
|
+
let ranLocalSetup = false;
|
|
423
|
+
const filesToWrite: ScaffoldFile[] = [...scaffoldFiles];
|
|
424
|
+
|
|
425
|
+
if (shouldWriteConfig) {
|
|
426
|
+
const inspect = deps.inspectHarnesses ?? inspectHarnesses;
|
|
427
|
+
const inspections = await inspect(workspacePath);
|
|
428
|
+
defaultHarnesses = selectDefaultHarnessesForWorkspace(
|
|
429
|
+
inspections,
|
|
430
|
+
);
|
|
431
|
+
warnings = collectInitWarnings(inspections);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
io.print("Lisa spec init");
|
|
435
|
+
io.print("");
|
|
436
|
+
io.print(`Workspace: ${workspacePath}`);
|
|
437
|
+
io.print(`Storage: ${layout.storageMode}`);
|
|
438
|
+
io.print(`Spec root: ${specRoot}`);
|
|
439
|
+
if (layout.worktreeSpecRoot) {
|
|
440
|
+
io.print(`Worktree spec root: ${layout.worktreeSpecRoot}`);
|
|
441
|
+
}
|
|
442
|
+
if (layout.globalPackRoots.length > 0) {
|
|
443
|
+
io.print(`Global pack roots: ${layout.globalPackRoots.join(", ")}`);
|
|
444
|
+
}
|
|
445
|
+
io.print(`Runtime root: ${layout.runtimeRoot}`);
|
|
446
|
+
io.print(`Snapshot root: ${layout.snapshotRoot}`);
|
|
447
|
+
|
|
448
|
+
if (shouldWriteConfig && io.ask && layout.storageMode !== "external") {
|
|
449
|
+
ranLocalSetup = await promptYesNo(
|
|
450
|
+
io,
|
|
451
|
+
"Install local Lisa skills now?",
|
|
452
|
+
false,
|
|
453
|
+
);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
if (shouldWriteConfig && defaultHarnesses) {
|
|
457
|
+
filesToWrite.unshift({
|
|
458
|
+
path: configPath,
|
|
459
|
+
content: renderConfig(defaultHarnesses, false, []),
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
if (filesToWrite.length === 0) {
|
|
464
|
+
io.print("Status: already initialized");
|
|
465
|
+
io.print(`Config: present (${toPortablePath(configPath, workspacePath)})`);
|
|
466
|
+
io.print("");
|
|
467
|
+
io.print("Nothing to write. Re-run with `lisa spec init --force` to replace the scaffolded config.");
|
|
468
|
+
return 0;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
writeScaffoldFiles(filesToWrite);
|
|
472
|
+
const addedGitignore = layout.storageMode === "external" ? false : ensureGitignoreEntry(workspacePath, ".specs/config.local.yaml");
|
|
473
|
+
|
|
474
|
+
let localSkillResults: SkillInstallResult[] = [];
|
|
475
|
+
if (ranLocalSetup) {
|
|
476
|
+
const installLocalSkills = deps.installLocalSkills ?? ((root: string) => installLisaSkills("local", root));
|
|
477
|
+
|
|
478
|
+
localSkillResults = await installLocalSkills(workspacePath);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
io.print(`Status: ${shouldWriteConfig ? (options.force ? "refreshed" : "initialized") : "updated"}`);
|
|
482
|
+
io.print("");
|
|
483
|
+
io.print("Wrote files:");
|
|
484
|
+
for (const file of filesToWrite) {
|
|
485
|
+
io.print(`- ${toPortablePath(file.path, workspacePath)}`);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
if (addedGitignore) {
|
|
489
|
+
io.print(`- .gitignore (added .specs/config.local.yaml)`);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
if (localSkillResults.length > 0) {
|
|
493
|
+
io.print("");
|
|
494
|
+
io.print("Local Lisa skill install:");
|
|
495
|
+
for (const result of localSkillResults) {
|
|
496
|
+
io.print(formatLocalSkillResult(result, workspacePath));
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
const localSkillInstallFailed = localSkillResults.some((result) => result.state === "failed");
|
|
501
|
+
|
|
502
|
+
if (!shouldWriteConfig) {
|
|
503
|
+
io.print("");
|
|
504
|
+
io.print(`Preserved existing config at ${toPortablePath(configPath, workspacePath)}.`);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
if (defaultHarnesses) {
|
|
508
|
+
io.print("");
|
|
509
|
+
io.print("Default stage profiles:");
|
|
510
|
+
io.print(`- generate -> ${PROFILE_NAMES.generate} (${defaultHarnesses.generate})`);
|
|
511
|
+
io.print(`- implement -> ${PROFILE_NAMES.implement} (${defaultHarnesses.implement})`);
|
|
512
|
+
io.print(`- check -> ${PROFILE_NAMES.check} (${defaultHarnesses.check})`);
|
|
513
|
+
io.print(`- import -> ${PROFILE_NAMES.import} (${defaultHarnesses.import})`);
|
|
514
|
+
|
|
515
|
+
if (warnings?.noPortableHarnessDetected) {
|
|
516
|
+
io.print("");
|
|
517
|
+
io.print("Warning: No portable harness CLI was detected for this repo.");
|
|
518
|
+
io.print("The scaffold uses standard command names, so install or reconfigure a harness before running spec stages.");
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
io.print("");
|
|
523
|
+
io.print("Next steps:");
|
|
524
|
+
io.print("- Run `lisa spec status` to confirm harness availability.");
|
|
525
|
+
if (layout.storageMode === "external") {
|
|
526
|
+
io.print("- Run `lisa spec guide backend <name>`, `lisa spec generate`, or `lisa spec import` to author specs in Lisa's external storage roots.");
|
|
527
|
+
io.print("- Run `lisa skill install --local` later only if you explicitly want repo-local harness guidance files.");
|
|
528
|
+
} else if (shouldWriteConfig && !ranLocalSetup) {
|
|
529
|
+
io.print("- Run `lisa skill install --local` later if you want Lisa to install repo-local harness guidance files.");
|
|
530
|
+
io.print("- If you want managed Lisa guidance updates later, point `agent_guidance.targets` at a non-root repo file instead of AGENTS.md/CLAUDE.md/GEMINI.md.");
|
|
531
|
+
io.print("- Run `lisa spec generate backend <name>` or `lisa spec import <path...>` to create your first specs.");
|
|
532
|
+
} else {
|
|
533
|
+
io.print("- Run `lisa spec generate backend <name>` or `lisa spec import <path...>` to create your first specs.");
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
if (localSkillInstallFailed) {
|
|
537
|
+
io.error("Local Lisa skill install did not complete for every detected harness.");
|
|
538
|
+
return 1;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
return 0;
|
|
542
|
+
} catch (error) {
|
|
543
|
+
io.error(error instanceof Error ? error.message : String(error));
|
|
544
|
+
return 1;
|
|
545
|
+
} finally {
|
|
546
|
+
io.close?.();
|
|
547
|
+
}
|
|
548
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { inspectHarnesses } from "../../harness/registry";
|
|
2
|
+
import type { HarnessInspection } from "../../harness/types";
|
|
3
|
+
import type { SpecWorkspaceSnapshot } from "../types";
|
|
4
|
+
import { inspectSpecWorkspace } from "../workspace";
|
|
5
|
+
|
|
6
|
+
export interface SpecStatusReport {
|
|
7
|
+
workspace: SpecWorkspaceSnapshot;
|
|
8
|
+
harnesses: HarnessInspection[];
|
|
9
|
+
availableHarnesses: HarnessInspection[];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function runSpecStatusWorkflow(cwd = process.cwd()): Promise<SpecStatusReport> {
|
|
13
|
+
const workspace = inspectSpecWorkspace(cwd);
|
|
14
|
+
const harnesses = await inspectHarnesses(workspace.workspacePath);
|
|
15
|
+
const availableHarnesses = harnesses.filter((inspection) => inspection.availability.available);
|
|
16
|
+
|
|
17
|
+
return {
|
|
18
|
+
workspace,
|
|
19
|
+
harnesses,
|
|
20
|
+
availableHarnesses,
|
|
21
|
+
};
|
|
22
|
+
}
|