@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,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
|
+
}
|
package/src/spec/cli.ts
ADDED
|
@@ -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";
|