@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.
Files changed (69) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +407 -0
  3. package/bin/lisa-runtime.js +8797 -0
  4. package/bin/lisa.js +21 -0
  5. package/completion.ts +58 -0
  6. package/install.ps1 +51 -0
  7. package/install.sh +93 -0
  8. package/lisa.ts +6 -0
  9. package/package.json +66 -0
  10. package/skills/README.md +28 -0
  11. package/skills/claude-code/CLAUDE.md +151 -0
  12. package/skills/codex/AGENTS.md +151 -0
  13. package/skills/gemini/GEMINI.md +151 -0
  14. package/skills/opencode/AGENTS.md +152 -0
  15. package/src/cli.ts +85 -0
  16. package/src/harness/base-adapter.ts +47 -0
  17. package/src/harness/claude-code.ts +106 -0
  18. package/src/harness/codex.ts +80 -0
  19. package/src/harness/command.ts +173 -0
  20. package/src/harness/gemini.ts +74 -0
  21. package/src/harness/opencode.ts +84 -0
  22. package/src/harness/registry.ts +29 -0
  23. package/src/harness/runner.ts +19 -0
  24. package/src/harness/types.ts +73 -0
  25. package/src/output-mode.ts +32 -0
  26. package/src/skill/artifacts.ts +174 -0
  27. package/src/skill/cli.ts +29 -0
  28. package/src/skill/install.ts +317 -0
  29. package/src/spec/agent-guidance.ts +466 -0
  30. package/src/spec/cli.ts +151 -0
  31. package/src/spec/commands/check.ts +1 -0
  32. package/src/spec/commands/config.ts +146 -0
  33. package/src/spec/commands/diff.ts +1 -0
  34. package/src/spec/commands/generate.ts +1 -0
  35. package/src/spec/commands/guide.ts +1 -0
  36. package/src/spec/commands/harness-list.ts +36 -0
  37. package/src/spec/commands/implement.ts +1 -0
  38. package/src/spec/commands/import.ts +1 -0
  39. package/src/spec/commands/init.ts +1 -0
  40. package/src/spec/commands/status.ts +87 -0
  41. package/src/spec/config.ts +63 -0
  42. package/src/spec/diff.ts +791 -0
  43. package/src/spec/extensions/benchmark.ts +347 -0
  44. package/src/spec/extensions/registry.ts +59 -0
  45. package/src/spec/extensions/types.ts +56 -0
  46. package/src/spec/grammar/index.ts +14 -0
  47. package/src/spec/grammar/parser.ts +443 -0
  48. package/src/spec/grammar/types.ts +70 -0
  49. package/src/spec/grammar/validator.ts +104 -0
  50. package/src/spec/loader.ts +174 -0
  51. package/src/spec/local-config.ts +59 -0
  52. package/src/spec/parser.ts +226 -0
  53. package/src/spec/path-utils.ts +73 -0
  54. package/src/spec/planner.ts +299 -0
  55. package/src/spec/prompt-renderer.ts +318 -0
  56. package/src/spec/skill-content.ts +119 -0
  57. package/src/spec/types.ts +239 -0
  58. package/src/spec/validator.ts +443 -0
  59. package/src/spec/workflows/check.ts +1534 -0
  60. package/src/spec/workflows/diff.ts +209 -0
  61. package/src/spec/workflows/generate.ts +1270 -0
  62. package/src/spec/workflows/guide.ts +190 -0
  63. package/src/spec/workflows/implement.ts +797 -0
  64. package/src/spec/workflows/import.ts +986 -0
  65. package/src/spec/workflows/init.ts +548 -0
  66. package/src/spec/workflows/status.ts +22 -0
  67. package/src/spec/workspace.ts +541 -0
  68. package/uninstall.ps1 +21 -0
  69. package/uninstall.sh +22 -0
@@ -0,0 +1,466 @@
1
+ import { existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from "fs";
2
+ import { basename, dirname, join, relative, resolve } from "path";
3
+ import { fileURLToPath } from "url";
4
+
5
+ import { getHarnessAdapters } from "../harness/registry";
6
+ import { renderOpenCodeSkill } from "../skill/artifacts";
7
+ import { normalizeRelativePath } from "./path-utils";
8
+ import { resolveConfiguredHarnessId } from "./config";
9
+ import { loadSpecWorkspace } from "./loader";
10
+ import { LISA_ROOT_GUIDANCE } from "./skill-content";
11
+ import type { LoadedSpecWorkspace, ParsedSpecConfig, SpecArea } from "./types";
12
+ import { assertSafeLisaStorageWritePath, resolveWorkspaceLayout, toLogicalPath } from "./workspace";
13
+
14
+ const MANAGED_GUIDANCE_START = "<!-- LISA_AGENT_GUIDANCE_START -->";
15
+ const MANAGED_GUIDANCE_END = "<!-- LISA_AGENT_GUIDANCE_END -->";
16
+
17
+ const WORKSPACE_SKILL_ARTIFACT_PATHS: Record<string, string> = {
18
+ codex: "skills/codex/AGENTS.md",
19
+ opencode: ".opencode/skills/lisa/SKILL.md",
20
+ "claude-code": "skills/claude-code/CLAUDE.md",
21
+ gemini: "skills/gemini/GEMINI.md",
22
+ };
23
+
24
+ const BUNDLED_SKILL_ARTIFACT_PATHS: Record<string, string> = {
25
+ codex: "skills/codex/AGENTS.md",
26
+ opencode: "skills/opencode/AGENTS.md",
27
+ "claude-code": "skills/claude-code/CLAUDE.md",
28
+ gemini: "skills/gemini/GEMINI.md",
29
+ };
30
+
31
+ function isRootGuidanceTarget(target: string): boolean {
32
+ return ["AGENTS.md", "CLAUDE.md", "GEMINI.md"].includes(target);
33
+ }
34
+
35
+ function containsLisaSubstring(content: string): boolean {
36
+ return content.toLowerCase().includes("lisa");
37
+ }
38
+
39
+ function resolveRootGuidanceHarnessId(target: string): string | undefined {
40
+ return target === "AGENTS.md"
41
+ ? "codex"
42
+ : target === "CLAUDE.md"
43
+ ? "claude-code"
44
+ : target === "GEMINI.md"
45
+ ? "gemini"
46
+ : undefined;
47
+ }
48
+
49
+ function matchesBundledRootGuidance(target: string, content: string): boolean {
50
+ const harnessId = resolveRootGuidanceHarnessId(target);
51
+ const bundledPath = harnessId ? getBundledSkillArtifactPathForHarness(harnessId) : undefined;
52
+ return Boolean(bundledPath && readFileSync(bundledPath, "utf8") === content);
53
+ }
54
+
55
+ function isSafeRepoRelativePath(path: string): boolean {
56
+ const raw = path.replace(/\\/g, "/").trim();
57
+ if (!raw || raw.startsWith("/") || /^[A-Za-z]:\//.test(raw)) {
58
+ return false;
59
+ }
60
+
61
+ const normalized = normalizeRelativePath(raw).trim();
62
+
63
+ return !normalized.split("/").some((segment) => segment.length === 0 || segment === "..");
64
+ }
65
+
66
+ function isReservedLisaPath(path: string): boolean {
67
+ const normalized = normalizeRelativePath(path).trim();
68
+ return normalized === ".specs"
69
+ || normalized.startsWith(".specs/")
70
+ || normalized === ".lisa"
71
+ || normalized.startsWith(".lisa/");
72
+ }
73
+
74
+ function getWorkspaceSkillArtifactPathForHarness(harnessId: string): string | undefined {
75
+ return WORKSPACE_SKILL_ARTIFACT_PATHS[harnessId];
76
+ }
77
+
78
+ function getBundledSkillArtifactPathForHarness(harnessId: string): string | undefined {
79
+ const bundledRelativePath = BUNDLED_SKILL_ARTIFACT_PATHS[harnessId] ?? getWorkspaceSkillArtifactPathForHarness(harnessId);
80
+ if (!bundledRelativePath) {
81
+ return undefined;
82
+ }
83
+
84
+ const candidates = [
85
+ fileURLToPath(new URL(`../../${bundledRelativePath}`, import.meta.url)),
86
+ fileURLToPath(new URL(`../${bundledRelativePath}`, import.meta.url)),
87
+ ];
88
+
89
+ return candidates.find((candidatePath) => existsSync(candidatePath));
90
+ }
91
+
92
+ function resolveMaterializedSkillArtifactPath(layout: ReturnType<typeof resolveWorkspaceLayout>, harnessId: string): string | undefined {
93
+ const fileName = harnessId === "opencode"
94
+ ? "SKILL.md"
95
+ : basename(getWorkspaceSkillArtifactPathForHarness(harnessId) ?? "");
96
+ if (!fileName) {
97
+ return undefined;
98
+ }
99
+
100
+ return join(layout.runtimeRoot, "skills", harnessId, fileName);
101
+ }
102
+
103
+ function formatSkillArtifactPath(layout: ReturnType<typeof resolveWorkspaceLayout>, artifactPath: string): string {
104
+ return toLogicalPath(layout, artifactPath);
105
+ }
106
+
107
+ export function ensureGuidanceSkillArtifact(workspaceRoot: string, harnessId: string): string {
108
+ const layout = resolveWorkspaceLayout(workspaceRoot);
109
+ const workspaceRelativePath = getWorkspaceSkillArtifactPathForHarness(harnessId);
110
+ if (workspaceRelativePath && existsSync(join(workspaceRoot, workspaceRelativePath))) {
111
+ return workspaceRelativePath;
112
+ }
113
+
114
+ const bundledPath = getBundledSkillArtifactPathForHarness(harnessId);
115
+ const materializedPath = resolveMaterializedSkillArtifactPath(layout, harnessId);
116
+ if (!bundledPath || !materializedPath || !existsSync(bundledPath)) {
117
+ throw new Error(`No Lisa skill artifact is available for harness \`${harnessId}\`.`);
118
+ }
119
+
120
+ if (layout.storageMode === "repo") {
121
+ assertSafeRepoWritePath(workspaceRoot, materializedPath);
122
+ } else {
123
+ assertSafeLisaStorageWritePath(layout.runtimeRoot, materializedPath, "Lisa guidance skill artifact");
124
+ }
125
+ mkdirSync(dirname(materializedPath), { recursive: true });
126
+ const bundledContent = readFileSync(bundledPath, "utf8");
127
+ writeFileSync(materializedPath, harnessId === "opencode" ? renderOpenCodeSkill(bundledContent) : bundledContent);
128
+ if (layout.storageMode === "external") {
129
+ return materializedPath;
130
+ }
131
+
132
+ return formatSkillArtifactPath(layout, materializedPath);
133
+ }
134
+
135
+ export function resolveGuidanceHarnessId(config: ParsedSpecConfig | undefined, override?: string): string {
136
+ if (override) {
137
+ return config ? resolveConfiguredHarnessId(config, override) : override;
138
+ }
139
+
140
+ const profileName = config?.defaultStageProfiles.generate;
141
+ const harnessId = profileName ? config?.profiles[profileName]?.harness : undefined;
142
+ return config && harnessId ? resolveConfiguredHarnessId(config, harnessId) : harnessId ?? "opencode";
143
+ }
144
+
145
+ export function renderSeedSpec(area: SpecArea, name: string): string {
146
+ return `---
147
+ id: ${area}.${name}
148
+ status: draft
149
+ code_paths:
150
+ - "TODO: replace with repo-relative code paths"
151
+ test_paths:
152
+ - "TODO: replace with mapped test paths or remove this key if you only use test_commands"
153
+ test_commands:
154
+ - "TODO: replace with deterministic test commands"
155
+ ---
156
+
157
+ # Summary
158
+ TODO: Replace this seed summary with the intended behavior.
159
+
160
+ ## Use Cases
161
+ - TODO: Describe the primary user or system flow.
162
+
163
+ ## Invariants
164
+ - TODO: Capture the behavior that must always remain true.
165
+
166
+ ## Failure Modes
167
+ - TODO: Record important edge cases, failure handling, or safety constraints.
168
+
169
+ ## Acceptance Criteria
170
+ - TODO: Add specific observable outcomes that prove the behavior works.
171
+
172
+ ## Out of Scope
173
+ - TODO: List behavior this spec does not intend to cover.
174
+ `;
175
+ }
176
+
177
+ function normalizeFrontmatterStringList(value: unknown): string[] {
178
+ if (!Array.isArray(value)) {
179
+ return [];
180
+ }
181
+
182
+ return value
183
+ .filter((entry): entry is string => typeof entry === "string")
184
+ .map((entry) => entry.trim())
185
+ .filter((entry) => entry.length > 0);
186
+ }
187
+
188
+ function resolveAdvertisedSkillArtifactPath(workspaceRoot: string, harnessId: string): string | undefined {
189
+ const layout = resolveWorkspaceLayout(workspaceRoot);
190
+ const localInstallPath = harnessId === "codex"
191
+ ? "AGENTS.md"
192
+ : harnessId === "claude-code"
193
+ ? "CLAUDE.md"
194
+ : harnessId === "gemini"
195
+ ? "GEMINI.md"
196
+ : harnessId === "opencode"
197
+ ? ".opencode/skills/lisa/SKILL.md"
198
+ : undefined;
199
+ if (
200
+ localInstallPath
201
+ && existsSync(join(workspaceRoot, localInstallPath))
202
+ && (
203
+ !isRootGuidanceTarget(localInstallPath)
204
+ || (() => {
205
+ const content = readFileSync(join(workspaceRoot, localInstallPath), "utf8");
206
+ return content.includes(MANAGED_GUIDANCE_START) || matchesBundledRootGuidance(localInstallPath, content);
207
+ })()
208
+ )
209
+ ) {
210
+ return localInstallPath;
211
+ }
212
+
213
+ const resolvedMaterializedPath = resolveMaterializedSkillArtifactPath(layout, harnessId);
214
+ if (resolvedMaterializedPath && existsSync(resolvedMaterializedPath)) {
215
+ return formatSkillArtifactPath(layout, resolvedMaterializedPath);
216
+ }
217
+
218
+ const workspacePath = getWorkspaceSkillArtifactPathForHarness(harnessId);
219
+ if (workspacePath && existsSync(join(workspaceRoot, workspacePath))) {
220
+ return workspacePath;
221
+ }
222
+
223
+ const bundledPath = getBundledSkillArtifactPathForHarness(harnessId);
224
+ if (bundledPath) {
225
+ const relativeBundledPath = normalizeRelativePath(relative(workspaceRoot, bundledPath));
226
+ if (relativeBundledPath && relativeBundledPath !== ".." && !relativeBundledPath.startsWith("../")) {
227
+ return relativeBundledPath;
228
+ }
229
+ }
230
+
231
+ if (resolvedMaterializedPath) {
232
+ return formatSkillArtifactPath(layout, resolvedMaterializedPath);
233
+ }
234
+
235
+ return undefined;
236
+ }
237
+
238
+ function renderManagedGuidanceContent(workspace: LoadedSpecWorkspace): string {
239
+ const commandLines = [
240
+ "- `lisa skill install --global|--local`",
241
+ "- `lisa spec init`",
242
+ "- `lisa spec config [options]`",
243
+ "- `lisa spec status`",
244
+ "- `lisa spec harness list`",
245
+ "- `lisa spec guide [backend|frontend] [name] [options]`",
246
+ "- `lisa spec generate [backend|frontend] [name] [options]`",
247
+ "- `lisa spec diff [options]`",
248
+ "- `lisa spec implement [options]`",
249
+ "- `lisa spec check --changed|--all [options]`",
250
+ "- `lisa spec import <path...> [options]`",
251
+ ];
252
+ const harnessLines = getHarnessAdapters(workspace.workspacePath)
253
+ .map((adapter) => {
254
+ const artifactPath = resolveAdvertisedSkillArtifactPath(workspace.workspacePath, adapter.id);
255
+ return artifactPath ? `- \`${adapter.id}\` -> \`${artifactPath}\`` : `- \`${adapter.id}\``;
256
+ });
257
+ const activeSpecLines = workspace.documents
258
+ .filter((document) => document.kind === "base" && document.status === "active" && document.id)
259
+ .map((document) => {
260
+ const codePaths = normalizeFrontmatterStringList(document.frontmatter.code_paths);
261
+ const testPaths = normalizeFrontmatterStringList(document.frontmatter.test_paths);
262
+ const testCommands = normalizeFrontmatterStringList(document.frontmatter.test_commands);
263
+ const codePointer = codePaths.length > 0 ? codePaths.join(", ") : "none declared";
264
+ const testPointerParts = [
265
+ testPaths.length > 0 ? testPaths.join(", ") : "",
266
+ testCommands.length > 0 ? testCommands.join(", ") : "",
267
+ ].filter((entry) => entry.length > 0);
268
+ const testPointer = testPointerParts.length > 0 ? testPointerParts.join("; ") : "none declared";
269
+ return `- \`${document.id}\`: code ${codePointer}; tests ${testPointer}`;
270
+ });
271
+
272
+ const lines = [
273
+ MANAGED_GUIDANCE_START,
274
+ "## Lisa",
275
+ "",
276
+ "This block is managed by Lisa. Edit outside these markers.",
277
+ "",
278
+ LISA_ROOT_GUIDANCE,
279
+ "",
280
+ "### Current CLI surface",
281
+ ...commandLines,
282
+ "",
283
+ "### Harness skill artifacts",
284
+ ...harnessLines,
285
+ "",
286
+ "### Active spec pointers",
287
+ ...(activeSpecLines.length > 0 ? activeSpecLines : ["- No active base specs yet."]),
288
+ MANAGED_GUIDANCE_END,
289
+ ];
290
+
291
+ return `${lines.join("\n").trimEnd()}\n`;
292
+ }
293
+
294
+ function upsertManagedGuidance(existingContent: string, blockContent: string): string {
295
+ const trimmedExisting = existingContent.trimEnd();
296
+ const startIndex = existingContent.indexOf(MANAGED_GUIDANCE_START);
297
+ const endIndex = existingContent.indexOf(MANAGED_GUIDANCE_END);
298
+
299
+ if (startIndex >= 0 || endIndex >= 0) {
300
+ if (startIndex < 0 || endIndex < 0 || endIndex < startIndex) {
301
+ throw new Error("Agent guidance target contains malformed Lisa-managed markers.");
302
+ }
303
+
304
+ const suffixIndex = endIndex + MANAGED_GUIDANCE_END.length;
305
+ return `${existingContent.slice(0, startIndex)}${blockContent}${existingContent.slice(suffixIndex).replace(/^\n*/, "")}`;
306
+ }
307
+
308
+ if (!trimmedExisting) {
309
+ return blockContent;
310
+ }
311
+
312
+ return `${trimmedExisting}\n\n${blockContent}`;
313
+ }
314
+
315
+ export function preserveManagedGuidance(existingContent: string, replacementContent: string): string {
316
+ const startIndex = existingContent.indexOf(MANAGED_GUIDANCE_START);
317
+ const endIndex = existingContent.indexOf(MANAGED_GUIDANCE_END);
318
+ if (startIndex < 0 || endIndex < 0 || endIndex < startIndex) {
319
+ return replacementContent;
320
+ }
321
+
322
+ const block = existingContent.slice(startIndex, endIndex + MANAGED_GUIDANCE_END.length);
323
+ return upsertManagedGuidance(replacementContent, `${block}\n`);
324
+ }
325
+
326
+ function resolveGuidanceTargets(config: LoadedSpecWorkspace["config"]): string[] {
327
+ const guidance = config?.agentGuidance;
328
+ if (!guidance) {
329
+ return [];
330
+ }
331
+
332
+ const enabled = guidance.enabled ?? (Boolean(guidance.target) || Boolean(guidance.targets?.length));
333
+ if (!enabled) {
334
+ return [];
335
+ }
336
+
337
+ const single = guidance.target?.trim();
338
+ const plural = guidance.targets?.map((t) => t.trim()).filter((t) => t.length > 0) ?? [];
339
+ return Array.from(new Set([single, ...plural].filter((t): t is string => Boolean(t))));
340
+ }
341
+
342
+ function writeGuidanceTarget(workspaceRoot: string, target: string, blockContent: string): string {
343
+ if (!isSafeRepoRelativePath(target)) {
344
+ throw new Error(`Spec config agent_guidance target \`${target}\` must stay inside the repository using a safe relative path.`);
345
+ }
346
+
347
+ if (isReservedLisaPath(target)) {
348
+ throw new Error(`Spec config agent_guidance target \`${target}\` must not point inside \`.specs/\` or \`.lisa/\`.`);
349
+ }
350
+
351
+ const targetPath = resolve(workspaceRoot, target);
352
+ const relativeTargetPath = normalizeRelativePath(relative(workspaceRoot, targetPath));
353
+ if (relativeTargetPath === ".." || relativeTargetPath.startsWith("../")) {
354
+ throw new Error(`Spec config agent_guidance target \`${target}\` must stay inside the repository using a safe relative path.`);
355
+ }
356
+
357
+ assertSafeRepoWritePath(workspaceRoot, targetPath);
358
+ const existingContent = existsSync(targetPath) ? readFileSync(targetPath, "utf8") : "";
359
+ if (
360
+ existingContent
361
+ && isRootGuidanceTarget(relativeTargetPath || target)
362
+ && containsLisaSubstring(existingContent)
363
+ && !existingContent.includes(MANAGED_GUIDANCE_START)
364
+ && !matchesBundledRootGuidance(relativeTargetPath || target, existingContent)
365
+ ) {
366
+ throw new Error(`Refusing to update ${relativeTargetPath || target} because it already contains lisa.`);
367
+ }
368
+ const nextContent = upsertManagedGuidance(existingContent, blockContent);
369
+ mkdirSync(dirname(targetPath), { recursive: true });
370
+ writeFileSync(targetPath, nextContent);
371
+ return relativeTargetPath || target;
372
+ }
373
+
374
+ export function updateAgentGuidance(workspaceRoot: string): string | undefined {
375
+ const workspace = loadSpecWorkspace(workspaceRoot);
376
+ const targets = resolveGuidanceTargets(workspace.config);
377
+ if (targets.length === 0) {
378
+ return undefined;
379
+ }
380
+
381
+ const blockContent = renderManagedGuidanceContent(workspace);
382
+ const written: string[] = [];
383
+ for (const target of targets) {
384
+ written.push(writeGuidanceTarget(workspaceRoot, target, blockContent));
385
+ }
386
+
387
+ return written.join(", ");
388
+ }
389
+
390
+ export function assertSafeRepoWritePath(workspaceRoot: string, targetPath: string): void {
391
+ const workspaceRealPath = realpathSync(workspaceRoot);
392
+ let existingPath = targetPath;
393
+
394
+ while (!existsSync(existingPath)) {
395
+ const parentPath = dirname(existingPath);
396
+ if (parentPath === existingPath) {
397
+ throw new Error(`Could not resolve a safe parent directory for ${targetPath}`);
398
+ }
399
+ existingPath = parentPath;
400
+ }
401
+
402
+ let cursor = existingPath;
403
+ while (true) {
404
+ if (existsSync(cursor) && lstatSync(cursor).isSymbolicLink()) {
405
+ throw new Error(`Refusing to write agent guidance through symlinked path: ${cursor}`);
406
+ }
407
+
408
+ if (cursor === workspaceRoot) {
409
+ break;
410
+ }
411
+
412
+ const parentPath = dirname(cursor);
413
+ if (parentPath === cursor) {
414
+ break;
415
+ }
416
+ cursor = parentPath;
417
+ }
418
+
419
+ const realExistingPath = realpathSync(existingPath);
420
+ if (realExistingPath !== workspaceRealPath && !realExistingPath.startsWith(`${workspaceRealPath}/`)) {
421
+ throw new Error(`Refusing to write agent guidance outside the repository: ${targetPath}`);
422
+ }
423
+
424
+ if (existsSync(targetPath)) {
425
+ const realTargetPath = realpathSync(targetPath);
426
+ if (realTargetPath !== workspaceRealPath && !realTargetPath.startsWith(`${workspaceRealPath}/`)) {
427
+ throw new Error(`Refusing to overwrite agent guidance outside the repository: ${targetPath}`);
428
+ }
429
+ }
430
+ }
431
+
432
+ export function updateAgentGuidanceSafely(
433
+ workspaceRoot: string,
434
+ options: { onlyExistingTargets?: boolean } = {},
435
+ ): { path?: string; warning?: string } {
436
+ try {
437
+ const workspace = loadSpecWorkspace(workspaceRoot);
438
+ const targets = resolveGuidanceTargets(workspace.config)
439
+ .filter((target) => !options.onlyExistingTargets || existsSync(resolve(workspaceRoot, target)));
440
+ if (targets.length === 0) {
441
+ return {};
442
+ }
443
+
444
+ const blockContent = renderManagedGuidanceContent(workspace);
445
+ const written: string[] = [];
446
+ const warnings: string[] = [];
447
+
448
+ for (const target of targets) {
449
+ try {
450
+ written.push(writeGuidanceTarget(workspaceRoot, target, blockContent));
451
+ } catch (error) {
452
+ const message = error instanceof Error ? error.message : String(error);
453
+ warnings.push(`${target}: ${message}`);
454
+ }
455
+ }
456
+
457
+ return {
458
+ path: written.length > 0 ? written.join(", ") : undefined,
459
+ warning: warnings.length > 0 ? warnings.join("; ") : undefined,
460
+ };
461
+ } catch (error) {
462
+ return {
463
+ warning: error instanceof Error ? error.message : String(error),
464
+ };
465
+ }
466
+ }
@@ -0,0 +1,151 @@
1
+ import { resolveOutputMode } from "../output-mode";
2
+ import { runSpecCheckCommand } from "./commands/check";
3
+ import { runSpecConfigCommand } from "./commands/config";
4
+ import { runSpecDiffCommand } from "./commands/diff";
5
+ import { runSpecGenerateCommand } from "./commands/generate";
6
+ import { runSpecGuideCommand } from "./commands/guide";
7
+ import { runSpecHarnessListCommand } from "./commands/harness-list";
8
+ import { runSpecImportCommand } from "./commands/import";
9
+ import { runSpecImplementCommand } from "./commands/implement";
10
+ import { runSpecInitCommand } from "./commands/init";
11
+ import { runSpecStatusCommand } from "./commands/status";
12
+
13
+ function printSpecHelp(): void {
14
+ const outputMode = resolveOutputMode();
15
+ const interactive = outputMode === "interactive";
16
+ const intro = interactive
17
+ ? `Specs in .specs/ define intended behavior. Lisa diffs those specs, plans the work, runs one writer for implementation, and then verifies the result.
18
+
19
+ Use Lisa spec commands to scaffold the spec workspace, author or import specs, inspect spec deltas, run implementation, and verify conformance.
20
+
21
+ Workflow:
22
+ init Scaffold .specs/ for the repo
23
+ guide Create or reuse a seed spec and open the harness skill
24
+ generate/import Draft new specs from prompts or existing code
25
+ diff Review changed specs and the derived plan
26
+ implement Run the single-writer implementation harness
27
+ check Verify changed or all specs with tests and reports
28
+
29
+ Override output mode with LISA_OUTPUT_MODE=interactive or LISA_OUTPUT_MODE=concise.
30
+
31
+ `
32
+ : "";
33
+
34
+ console.log(`Lisa spec commands
35
+
36
+ ${intro}Usage:
37
+ lisa spec init
38
+ lisa spec status
39
+ lisa spec config [options]
40
+ lisa spec harness list
41
+ lisa spec guide [backend|frontend] [name] [options]
42
+ lisa spec generate [backend|frontend] [name] [options]
43
+ lisa spec implement [options]
44
+ lisa spec check --changed|--all [options]
45
+ lisa spec import <path...> [options]
46
+ lisa spec diff [options]
47
+
48
+ Available today:
49
+ init Scaffold a .specs workspace for the current repo
50
+ status Show spec workspace and harness status
51
+ config View or set local harness and model overrides
52
+ harness list List registered harness adapters and capabilities
53
+ guide Create or reuse a seed spec and point to the right harness skill
54
+ generate Draft a spec with preview and approval gates
55
+ diff Show changed spec deltas and the derived implementation plan
56
+ implement Run a single-writer implementation harness from spec deltas
57
+ check Verify changed or all specs with reports and drift warnings
58
+ import Draft reviewable specs from existing code paths
59
+
60
+ Environment:
61
+ LISA_OUTPUT_MODE=interactive|concise Override help copy mode
62
+
63
+ ${interactive ? `Typical flow:
64
+ 1. lisa spec init
65
+ Set up .specs/ and the default harness config.
66
+ 2. lisa spec guide backend <name>
67
+ Start a seed spec and jump to the matching harness skill.
68
+ 3. lisa spec generate backend <name>
69
+ Write a new active spec.
70
+ or lisa spec import <path...>
71
+ Draft specs from existing code.
72
+ 4. lisa spec diff
73
+ Preview the spec delta and derived implementation plan.
74
+ 5. lisa spec implement
75
+ Turn changed specs into code updates with one writer.
76
+ 6. lisa spec check --changed
77
+ Re-run mapped tests and verifier audits before review.
78
+ ` : ""}
79
+ `);
80
+ }
81
+
82
+ function printHarnessHelp(): void {
83
+ console.log(`Lisa spec harness commands
84
+
85
+ Usage:
86
+ lisa spec harness list
87
+ `);
88
+ }
89
+
90
+ export async function runSpecCli(args: string[], cwd = process.cwd()): Promise<number> {
91
+ const [command, subcommand] = args;
92
+
93
+ if (!command || command === "help" || command === "--help" || command === "-h") {
94
+ printSpecHelp();
95
+ return 0;
96
+ }
97
+
98
+ if (command === "status") {
99
+ return runSpecStatusCommand(args.slice(1), cwd);
100
+ }
101
+
102
+ if (command === "init") {
103
+ return runSpecInitCommand(args.slice(1), cwd);
104
+ }
105
+
106
+ if (command === "config") {
107
+ return runSpecConfigCommand(args.slice(1), cwd);
108
+ }
109
+
110
+ if (command === "generate") {
111
+ return runSpecGenerateCommand(args.slice(1), cwd);
112
+ }
113
+
114
+ if (command === "guide") {
115
+ return runSpecGuideCommand(args.slice(1), cwd);
116
+ }
117
+
118
+ if (command === "diff") {
119
+ return runSpecDiffCommand(args.slice(1), cwd);
120
+ }
121
+
122
+ if (command === "implement") {
123
+ return runSpecImplementCommand(args.slice(1), cwd);
124
+ }
125
+
126
+ if (command === "check") {
127
+ return runSpecCheckCommand(args.slice(1), cwd);
128
+ }
129
+
130
+ if (command === "import") {
131
+ return runSpecImportCommand(args.slice(1), cwd);
132
+ }
133
+
134
+ if (command === "harness") {
135
+ if (!subcommand || subcommand === "help" || subcommand === "--help" || subcommand === "-h") {
136
+ printHarnessHelp();
137
+ return 0;
138
+ }
139
+
140
+ if (subcommand === "list") {
141
+ return runSpecHarnessListCommand(cwd);
142
+ }
143
+
144
+ console.error(`Unknown lisa spec harness command: ${subcommand}`);
145
+ return 1;
146
+ }
147
+
148
+ console.error(`Unknown lisa spec command: ${command}`);
149
+ console.error("Run `lisa spec --help` for available commands.");
150
+ return 1;
151
+ }
@@ -0,0 +1 @@
1
+ export { runSpecCheckCommand } from "../workflows/check";