@christang/keel 5.1.1
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 +250 -0
- package/README.zh-CN.md +295 -0
- package/assets/bootstrap/AGENTS.md +9 -0
- package/assets/openspec/schemas/keel-spec-driven/schema.yaml +166 -0
- package/assets/openspec/schemas/keel-spec-driven/templates/design.md +52 -0
- package/assets/openspec/schemas/keel-spec-driven/templates/proposal.md +21 -0
- package/assets/openspec/schemas/keel-spec-driven/templates/spec.md +8 -0
- package/assets/openspec/schemas/keel-spec-driven/templates/tasks.md +68 -0
- package/bin/keel.js +1490 -0
- package/package.json +35 -0
- package/plugins/keel/.claude-plugin/plugin.json +17 -0
- package/plugins/keel/.codex-plugin/plugin.json +29 -0
- package/plugins/keel/agents/keel-single-task-goal-claude.md +16 -0
- package/plugins/keel/agents/keel-single-task-goal-codex.md +16 -0
- package/plugins/keel/hooks/hooks.json +30 -0
- package/plugins/keel/scripts/pretooluse-guard.js +156 -0
- package/plugins/keel/scripts/session-start.js +182 -0
- package/plugins/keel/skills/keel-align-expectations/SKILL.md +53 -0
- package/plugins/keel/skills/keel-align-expectations/references/hardware-dsl.md +21 -0
- package/plugins/keel/skills/keel-align-expectations/references/hardware.md +21 -0
- package/plugins/keel/skills/keel-align-expectations/references/web.md +21 -0
- package/plugins/keel/skills/keel-debug-failure/SKILL.md +41 -0
- package/plugins/keel/skills/keel-handoff/SKILL.md +45 -0
- package/plugins/keel/skills/keel-review-checklist/SKILL.md +73 -0
- package/plugins/keel/skills/keel-run-single-task-goal/SKILL.md +68 -0
- package/plugins/keel/skills/keel-tdd-or-test-first/SKILL.md +45 -0
- package/scripts/install_to_repo.py +1122 -0
- package/scripts/run_python.js +63 -0
- package/scripts/validate_plugin.py +9869 -0
- package/src/core/capabilities.js +291 -0
- package/src/core/context.js +514 -0
- package/src/core/gates.js +643 -0
- package/src/core/goal.js +230 -0
- package/src/core/guard.js +295 -0
- package/src/core/helper.js +319 -0
- package/src/core/projection.js +195 -0
- package/src/core/task-contract.js +736 -0
- package/src/core/tasksview.js +123 -0
package/bin/keel.js
ADDED
|
@@ -0,0 +1,1490 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
const fs = require("fs");
|
|
5
|
+
const os = require("os");
|
|
6
|
+
const path = require("path");
|
|
7
|
+
const { spawnSync } = require("child_process");
|
|
8
|
+
const {
|
|
9
|
+
renderContext,
|
|
10
|
+
resolveContext,
|
|
11
|
+
} = require("../src/core/context");
|
|
12
|
+
const {
|
|
13
|
+
GateInputError,
|
|
14
|
+
renderGate,
|
|
15
|
+
runGate,
|
|
16
|
+
} = require("../src/core/gates");
|
|
17
|
+
const {
|
|
18
|
+
probeCapabilities,
|
|
19
|
+
renderCapabilities,
|
|
20
|
+
} = require("../src/core/capabilities");
|
|
21
|
+
const {
|
|
22
|
+
projectRuntime,
|
|
23
|
+
renderProjection,
|
|
24
|
+
} = require("../src/core/projection");
|
|
25
|
+
const {
|
|
26
|
+
compileGoalProjection,
|
|
27
|
+
renderGoalProjection,
|
|
28
|
+
} = require("../src/core/goal");
|
|
29
|
+
const {
|
|
30
|
+
compileHelperBrief,
|
|
31
|
+
captureHelperBaseline,
|
|
32
|
+
verifyHelperEvidence,
|
|
33
|
+
renderHelper,
|
|
34
|
+
} = require("../src/core/helper");
|
|
35
|
+
const {
|
|
36
|
+
compileTasksView,
|
|
37
|
+
renderTasksView,
|
|
38
|
+
} = require("../src/core/tasksview");
|
|
39
|
+
const {
|
|
40
|
+
GuardInputError,
|
|
41
|
+
clearGuard,
|
|
42
|
+
guardStatus,
|
|
43
|
+
renderGuard,
|
|
44
|
+
startGuard,
|
|
45
|
+
} = require("../src/core/guard");
|
|
46
|
+
|
|
47
|
+
const PACKAGE_ROOT = path.resolve(__dirname, "..");
|
|
48
|
+
const PACKAGE_JSON = require(path.join(PACKAGE_ROOT, "package.json"));
|
|
49
|
+
const INSTALL_SCRIPT = path.join(PACKAGE_ROOT, "scripts", "install_to_repo.py");
|
|
50
|
+
const DEFAULT_UPDATE_SOURCE = "github:TanglmChris/keel";
|
|
51
|
+
const VALID_TARGETS = new Set(["claude", "codex", "opencode", "both"]);
|
|
52
|
+
const KEEL_SKILLS = [
|
|
53
|
+
"keel-align-expectations",
|
|
54
|
+
"keel-debug-failure",
|
|
55
|
+
"keel-handoff",
|
|
56
|
+
"keel-review-checklist",
|
|
57
|
+
"keel-tdd-or-test-first",
|
|
58
|
+
];
|
|
59
|
+
const ALIGNMENT_REFERENCES = [
|
|
60
|
+
"references/web.md",
|
|
61
|
+
"references/hardware.md",
|
|
62
|
+
"references/hardware-dsl.md",
|
|
63
|
+
];
|
|
64
|
+
const OPENSPEC_COMMAND_IDS = ["propose", "explore", "apply", "sync", "archive"];
|
|
65
|
+
const OPENSPEC_SKILLS = [
|
|
66
|
+
"openspec-propose",
|
|
67
|
+
"openspec-explore",
|
|
68
|
+
"openspec-apply-change",
|
|
69
|
+
"openspec-sync-specs",
|
|
70
|
+
"openspec-archive-change",
|
|
71
|
+
];
|
|
72
|
+
const OPENSPEC_OVERLAY_ACTIONS = ["propose", "apply", "archive"];
|
|
73
|
+
const OPENSPEC_SURFACE_OVERLAY_START =
|
|
74
|
+
`<!-- keel:openspec-surface-overlay version=${PACKAGE_JSON.version} -->`;
|
|
75
|
+
const OPENSPEC_SURFACE_OVERLAY_END =
|
|
76
|
+
"<!-- keel:openspec-surface-overlay:end -->";
|
|
77
|
+
const OPENSPEC_SURFACE_OVERLAY_RE =
|
|
78
|
+
/<!--\s*keel:openspec-surface-overlay(?:\s+[^>]*)?\s*-->[\s\S]*?<!--\s*keel:openspec-surface-overlay:end\s*-->/;
|
|
79
|
+
|
|
80
|
+
const HELP = `keel ${PACKAGE_JSON.version}
|
|
81
|
+
|
|
82
|
+
Usage:
|
|
83
|
+
keel context [repo] [--change name] [--task id] [--json] [--clear-handoff]
|
|
84
|
+
keel capabilities [repo] [--target claude|codex|opencode] [--json]
|
|
85
|
+
keel project [repo] --target claude|codex|opencode --event startup|resume|compaction|goal|task-view|worktree|subagent-start|subagent-stop [--authorize goal|task-view|subagent] [--expected-owner owner] [--native-complete] [--change name] [--task id] [--json]
|
|
86
|
+
keel project tasks [repo] --target claude [--change name] [--json]
|
|
87
|
+
keel gate task-start|task-complete|change-close [repo] [--change name] [--task id] [--action sync|archive] [--base git-ref] [--no-guard] [--record] [--json]
|
|
88
|
+
keel guard start|status|clear [repo] [--change name] [--task id] [--force] [--json]
|
|
89
|
+
keel --init [repo] [--target claude|codex|opencode] [--dry-run] [--force-template-update]
|
|
90
|
+
keel --install [repo] [--target claude|codex|opencode] [--dry-run] [--force-template-update]
|
|
91
|
+
keel --clear [repo] [--target claude|codex|opencode] [--dry-run]
|
|
92
|
+
keel --uninstall [repo] [--target claude|codex|opencode] [--dry-run]
|
|
93
|
+
keel --update [--dry-run] [--source npm-package-or-git-spec]
|
|
94
|
+
keel --check [repo] [--target claude|codex|opencode]
|
|
95
|
+
keel --doctor [repo] [--target claude|codex|opencode]
|
|
96
|
+
keel --version
|
|
97
|
+
keel --help
|
|
98
|
+
|
|
99
|
+
Defaults:
|
|
100
|
+
repo defaults to the current working directory.
|
|
101
|
+
target defaults to claude.
|
|
102
|
+
--update refreshes the global keel CLI, not project protocol files.
|
|
103
|
+
update source defaults to ${DEFAULT_UPDATE_SOURCE}.
|
|
104
|
+
|
|
105
|
+
Project layout:
|
|
106
|
+
continuity is recomputed from OpenSpec on every invocation.
|
|
107
|
+
keel/HANDOFF.md is an optional keel-handoff/v1 pointer override.
|
|
108
|
+
Claude skills are installed under .claude/skills/keel-*.
|
|
109
|
+
Codex skills are installed under .agents/skills/keel-*.
|
|
110
|
+
OpenCode skills are installed under .opencode/skills/keel-*.
|
|
111
|
+
repeat keel --install to refresh project protocol files.
|
|
112
|
+
|
|
113
|
+
Examples:
|
|
114
|
+
keel context
|
|
115
|
+
keel context --json
|
|
116
|
+
keel context --change my-change --task 1.1
|
|
117
|
+
keel context --clear-handoff
|
|
118
|
+
keel gate task-start --change my-change --task 1.1 --json
|
|
119
|
+
keel gate task-complete --change my-change --task 1.1 --json
|
|
120
|
+
keel gate change-close --change my-change --action archive --json
|
|
121
|
+
keel guard start --change my-change --task 1.1 --json
|
|
122
|
+
keel guard status --json
|
|
123
|
+
keel guard clear --json
|
|
124
|
+
keel --init
|
|
125
|
+
keel --install
|
|
126
|
+
keel --install --target codex
|
|
127
|
+
keel --install --target opencode
|
|
128
|
+
keel --install --dry-run
|
|
129
|
+
keel --check
|
|
130
|
+
keel --doctor
|
|
131
|
+
keel --install --force-template-update
|
|
132
|
+
keel --update
|
|
133
|
+
keel --update --dry-run
|
|
134
|
+
keel --clear --dry-run
|
|
135
|
+
keel --uninstall --dry-run
|
|
136
|
+
`;
|
|
137
|
+
|
|
138
|
+
function printHelp() {
|
|
139
|
+
process.stdout.write(HELP);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function printVersion() {
|
|
143
|
+
process.stdout.write(`keel ${PACKAGE_JSON.version}\n`);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function fail(message, exitCode = 2) {
|
|
147
|
+
process.stderr.write(`keel: ${message}\n`);
|
|
148
|
+
process.stderr.write("Run keel --help for usage.\n");
|
|
149
|
+
process.exit(exitCode);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function parseArgs(argv) {
|
|
153
|
+
const parsed = {
|
|
154
|
+
action: null,
|
|
155
|
+
repo: null,
|
|
156
|
+
target: "claude",
|
|
157
|
+
dryRun: false,
|
|
158
|
+
forceTemplateUpdate: false,
|
|
159
|
+
updateSource: null,
|
|
160
|
+
help: false,
|
|
161
|
+
version: false,
|
|
162
|
+
change: null,
|
|
163
|
+
task: null,
|
|
164
|
+
json: false,
|
|
165
|
+
clearHandoff: false,
|
|
166
|
+
gateStage: null,
|
|
167
|
+
closeAction: null,
|
|
168
|
+
base: null,
|
|
169
|
+
noGuard: false,
|
|
170
|
+
record: false,
|
|
171
|
+
guardSubcommand: null,
|
|
172
|
+
force: false,
|
|
173
|
+
projectionEvent: null,
|
|
174
|
+
authorizations: [],
|
|
175
|
+
expectedOwner: null,
|
|
176
|
+
nativeComplete: false,
|
|
177
|
+
projectSubcommand: null,
|
|
178
|
+
expectedFingerprint: null,
|
|
179
|
+
helperQuestion: null,
|
|
180
|
+
helperCommand: null,
|
|
181
|
+
helperReads: [],
|
|
182
|
+
helperExternal: [],
|
|
183
|
+
helperVerify: false,
|
|
184
|
+
helperCaptureBaseline: false,
|
|
185
|
+
helperBaseline: null,
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
189
|
+
const arg = argv[index];
|
|
190
|
+
if (arg === "--help" || arg === "-h") {
|
|
191
|
+
parsed.help = true;
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
if (arg === "--version" || arg === "-v") {
|
|
195
|
+
parsed.version = true;
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
if (arg === "context" && parsed.action === null && parsed.repo === null) {
|
|
199
|
+
parsed.action = "context";
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
if (arg === "gate" && parsed.action === null && parsed.repo === null) {
|
|
203
|
+
parsed.action = "gate";
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
if (arg === "guard" && parsed.action === null && parsed.repo === null) {
|
|
207
|
+
parsed.action = "guard";
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
if (arg === "--force") {
|
|
211
|
+
parsed.force = true;
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
if (
|
|
215
|
+
arg === "capabilities"
|
|
216
|
+
&& parsed.action === null
|
|
217
|
+
&& parsed.repo === null
|
|
218
|
+
) {
|
|
219
|
+
parsed.action = "capabilities";
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
if (arg === "project" && parsed.action === null && parsed.repo === null) {
|
|
223
|
+
parsed.action = "project";
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
if (
|
|
227
|
+
["--init", "--install", "--clear", "--uninstall", "--update", "--check", "--doctor"].includes(arg)
|
|
228
|
+
) {
|
|
229
|
+
if (parsed.action !== null) {
|
|
230
|
+
fail(`choose only one action; already saw ${parsed.action}`);
|
|
231
|
+
}
|
|
232
|
+
parsed.action = arg.slice(2);
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
if (arg === "--dry-run") {
|
|
236
|
+
parsed.dryRun = true;
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
if (arg === "--json") {
|
|
240
|
+
parsed.json = true;
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
if (arg === "--clear-handoff") {
|
|
244
|
+
parsed.clearHandoff = true;
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
if (arg === "--no-guard") {
|
|
248
|
+
parsed.noGuard = true;
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
if (arg === "--record") {
|
|
252
|
+
parsed.record = true;
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
if (arg === "--change" || arg === "--task") {
|
|
256
|
+
index += 1;
|
|
257
|
+
if (index >= argv.length) {
|
|
258
|
+
fail(`${arg} requires a value`);
|
|
259
|
+
}
|
|
260
|
+
const key = arg === "--change" ? "change" : "task";
|
|
261
|
+
if (parsed[key] !== null) {
|
|
262
|
+
fail(`${arg} was provided more than once`);
|
|
263
|
+
}
|
|
264
|
+
parsed[key] = argv[index];
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
if (arg === "--action" || arg === "--base") {
|
|
268
|
+
index += 1;
|
|
269
|
+
if (index >= argv.length) {
|
|
270
|
+
fail(`${arg} requires a value`);
|
|
271
|
+
}
|
|
272
|
+
const key = arg === "--action" ? "closeAction" : "base";
|
|
273
|
+
if (parsed[key] !== null) {
|
|
274
|
+
fail(`${arg} was provided more than once`);
|
|
275
|
+
}
|
|
276
|
+
parsed[key] = argv[index];
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
if (arg === "--event" || arg === "--authorize" || arg === "--expected-owner") {
|
|
280
|
+
index += 1;
|
|
281
|
+
if (index >= argv.length) {
|
|
282
|
+
fail(`${arg} requires a value`);
|
|
283
|
+
}
|
|
284
|
+
if (arg === "--authorize") {
|
|
285
|
+
if (!parsed.authorizations.includes(argv[index])) {
|
|
286
|
+
parsed.authorizations.push(argv[index]);
|
|
287
|
+
}
|
|
288
|
+
} else {
|
|
289
|
+
const key = arg === "--event" ? "projectionEvent" : "expectedOwner";
|
|
290
|
+
if (parsed[key] !== null) fail(`${arg} was provided more than once`);
|
|
291
|
+
parsed[key] = argv[index];
|
|
292
|
+
}
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
if (arg === "--native-complete") {
|
|
296
|
+
parsed.nativeComplete = true;
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
if (arg === "--expected-fingerprint") {
|
|
300
|
+
index += 1;
|
|
301
|
+
if (index >= argv.length) {
|
|
302
|
+
fail("--expected-fingerprint requires a value");
|
|
303
|
+
}
|
|
304
|
+
if (parsed.expectedFingerprint !== null) {
|
|
305
|
+
fail("--expected-fingerprint was provided more than once");
|
|
306
|
+
}
|
|
307
|
+
parsed.expectedFingerprint = argv[index];
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
if (arg === "--brief" || arg === "--command" || arg === "--baseline") {
|
|
311
|
+
index += 1;
|
|
312
|
+
if (index >= argv.length) {
|
|
313
|
+
fail(`${arg} requires a value`);
|
|
314
|
+
}
|
|
315
|
+
const key =
|
|
316
|
+
arg === "--brief"
|
|
317
|
+
? "helperQuestion"
|
|
318
|
+
: arg === "--command"
|
|
319
|
+
? "helperCommand"
|
|
320
|
+
: "helperBaseline";
|
|
321
|
+
if (parsed[key] !== null) {
|
|
322
|
+
fail(`${arg} was provided more than once`);
|
|
323
|
+
}
|
|
324
|
+
parsed[key] = argv[index];
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
if (arg === "--read" || arg === "--external") {
|
|
328
|
+
index += 1;
|
|
329
|
+
if (index >= argv.length) {
|
|
330
|
+
fail(`${arg} requires a value`);
|
|
331
|
+
}
|
|
332
|
+
const key = arg === "--read" ? "helperReads" : "helperExternal";
|
|
333
|
+
parsed[key].push(argv[index]);
|
|
334
|
+
continue;
|
|
335
|
+
}
|
|
336
|
+
if (arg === "--verify") {
|
|
337
|
+
parsed.helperVerify = true;
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
if (arg === "--capture-baseline") {
|
|
341
|
+
parsed.helperCaptureBaseline = true;
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
if (arg.startsWith("--change=") || arg.startsWith("--task=")) {
|
|
345
|
+
const key = arg.startsWith("--change=") ? "change" : "task";
|
|
346
|
+
if (parsed[key] !== null) {
|
|
347
|
+
fail(`--${key} was provided more than once`);
|
|
348
|
+
}
|
|
349
|
+
parsed[key] = arg.slice(key.length + 3);
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
352
|
+
if (arg === "--force-template-update") {
|
|
353
|
+
parsed.forceTemplateUpdate = true;
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
if (arg === "--target") {
|
|
357
|
+
index += 1;
|
|
358
|
+
if (index >= argv.length) {
|
|
359
|
+
fail("--target requires claude, codex, or opencode");
|
|
360
|
+
}
|
|
361
|
+
parsed.target = argv[index];
|
|
362
|
+
continue;
|
|
363
|
+
}
|
|
364
|
+
if (arg.startsWith("--target=")) {
|
|
365
|
+
parsed.target = arg.slice("--target=".length);
|
|
366
|
+
continue;
|
|
367
|
+
}
|
|
368
|
+
if (arg === "--profile" || arg.startsWith("--profile=")) {
|
|
369
|
+
fail(
|
|
370
|
+
"--profile is no longer supported: web, hardware, and hardware-dsl "
|
|
371
|
+
+ "guidance is bundled with the keel-align-expectations skill as "
|
|
372
|
+
+ "on-demand references"
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
if (arg === "--repo") {
|
|
376
|
+
index += 1;
|
|
377
|
+
if (index >= argv.length) {
|
|
378
|
+
fail("--repo requires a path");
|
|
379
|
+
}
|
|
380
|
+
if (parsed.repo !== null) {
|
|
381
|
+
fail("repo path was provided more than once");
|
|
382
|
+
}
|
|
383
|
+
parsed.repo = argv[index];
|
|
384
|
+
continue;
|
|
385
|
+
}
|
|
386
|
+
if (arg.startsWith("--repo=")) {
|
|
387
|
+
if (parsed.repo !== null) {
|
|
388
|
+
fail("repo path was provided more than once");
|
|
389
|
+
}
|
|
390
|
+
parsed.repo = arg.slice("--repo=".length);
|
|
391
|
+
continue;
|
|
392
|
+
}
|
|
393
|
+
if (arg === "--source") {
|
|
394
|
+
index += 1;
|
|
395
|
+
if (index >= argv.length) {
|
|
396
|
+
fail("--source requires an npm package or git spec");
|
|
397
|
+
}
|
|
398
|
+
if (parsed.updateSource !== null) {
|
|
399
|
+
fail("update source was provided more than once");
|
|
400
|
+
}
|
|
401
|
+
parsed.updateSource = argv[index];
|
|
402
|
+
continue;
|
|
403
|
+
}
|
|
404
|
+
if (arg.startsWith("--source=")) {
|
|
405
|
+
if (parsed.updateSource !== null) {
|
|
406
|
+
fail("update source was provided more than once");
|
|
407
|
+
}
|
|
408
|
+
parsed.updateSource = arg.slice("--source=".length);
|
|
409
|
+
continue;
|
|
410
|
+
}
|
|
411
|
+
if (arg.startsWith("-")) {
|
|
412
|
+
fail(`unknown option: ${arg}`);
|
|
413
|
+
}
|
|
414
|
+
if (parsed.action === "gate" && parsed.gateStage === null) {
|
|
415
|
+
parsed.gateStage = arg;
|
|
416
|
+
continue;
|
|
417
|
+
}
|
|
418
|
+
if (parsed.action === "guard" && parsed.guardSubcommand === null) {
|
|
419
|
+
parsed.guardSubcommand = arg;
|
|
420
|
+
continue;
|
|
421
|
+
}
|
|
422
|
+
if (parsed.action === "project" && parsed.projectSubcommand === null) {
|
|
423
|
+
parsed.projectSubcommand = arg;
|
|
424
|
+
continue;
|
|
425
|
+
}
|
|
426
|
+
if (parsed.repo !== null) {
|
|
427
|
+
fail("repo path was provided more than once");
|
|
428
|
+
}
|
|
429
|
+
parsed.repo = arg;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
if (parsed.target === "both") {
|
|
433
|
+
parsed.target = "claude";
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
if (!VALID_TARGETS.has(parsed.target)) {
|
|
437
|
+
fail(`invalid target: ${parsed.target}`);
|
|
438
|
+
}
|
|
439
|
+
if (
|
|
440
|
+
!["context", "gate", "capabilities", "project", "guard"].includes(
|
|
441
|
+
parsed.action
|
|
442
|
+
)
|
|
443
|
+
&& (
|
|
444
|
+
parsed.change !== null
|
|
445
|
+
|| parsed.task !== null
|
|
446
|
+
|| parsed.json
|
|
447
|
+
|| parsed.clearHandoff
|
|
448
|
+
|| parsed.closeAction !== null
|
|
449
|
+
|| parsed.base !== null
|
|
450
|
+
|| parsed.projectionEvent !== null
|
|
451
|
+
|| parsed.authorizations.length > 0
|
|
452
|
+
|| parsed.expectedOwner !== null
|
|
453
|
+
|| parsed.nativeComplete
|
|
454
|
+
)
|
|
455
|
+
) {
|
|
456
|
+
fail("selection and JSON options apply only to keel context or keel gate");
|
|
457
|
+
}
|
|
458
|
+
if (parsed.action !== "context" && parsed.clearHandoff) {
|
|
459
|
+
fail("--clear-handoff applies only to keel context");
|
|
460
|
+
}
|
|
461
|
+
if (
|
|
462
|
+
parsed.action === "capabilities"
|
|
463
|
+
&& (
|
|
464
|
+
parsed.change !== null
|
|
465
|
+
|| parsed.task !== null
|
|
466
|
+
|| parsed.closeAction !== null
|
|
467
|
+
|| parsed.base !== null
|
|
468
|
+
)
|
|
469
|
+
) {
|
|
470
|
+
fail("capabilities accepts only repo, --target, and --json");
|
|
471
|
+
}
|
|
472
|
+
if (
|
|
473
|
+
parsed.action !== "project"
|
|
474
|
+
&& (
|
|
475
|
+
parsed.projectionEvent !== null
|
|
476
|
+
|| parsed.authorizations.length > 0
|
|
477
|
+
|| parsed.expectedOwner !== null
|
|
478
|
+
|| parsed.nativeComplete
|
|
479
|
+
)
|
|
480
|
+
) {
|
|
481
|
+
fail("projection options apply only to keel project");
|
|
482
|
+
}
|
|
483
|
+
if (parsed.action !== "project" && parsed.projectSubcommand !== null) {
|
|
484
|
+
fail("project subcommands apply only to keel project");
|
|
485
|
+
}
|
|
486
|
+
if (parsed.action !== "guard" && parsed.guardSubcommand !== null) {
|
|
487
|
+
fail("guard subcommands apply only to keel guard");
|
|
488
|
+
}
|
|
489
|
+
if (parsed.force && parsed.action !== "guard") {
|
|
490
|
+
fail("--force applies only to keel guard start");
|
|
491
|
+
}
|
|
492
|
+
if (parsed.noGuard && parsed.action !== "gate") {
|
|
493
|
+
fail("--no-guard applies only to keel gate task-start");
|
|
494
|
+
}
|
|
495
|
+
if (parsed.record && parsed.action !== "gate") {
|
|
496
|
+
fail("--record applies only to keel gate task-start");
|
|
497
|
+
}
|
|
498
|
+
if (parsed.projectSubcommand !== "goal" && parsed.expectedFingerprint !== null) {
|
|
499
|
+
fail("--expected-fingerprint applies only to keel project goal");
|
|
500
|
+
}
|
|
501
|
+
if (parsed.action === "project" && parsed.projectSubcommand !== null) {
|
|
502
|
+
if (!["goal", "helper", "tasks"].includes(parsed.projectSubcommand)) {
|
|
503
|
+
fail(
|
|
504
|
+
`unknown project subcommand: ${parsed.projectSubcommand}; `
|
|
505
|
+
+ "only keel project goal, keel project helper, and keel project "
|
|
506
|
+
+ "tasks are supported"
|
|
507
|
+
);
|
|
508
|
+
}
|
|
509
|
+
if (
|
|
510
|
+
parsed.projectionEvent !== null
|
|
511
|
+
|| parsed.authorizations.length > 0
|
|
512
|
+
|| parsed.nativeComplete
|
|
513
|
+
) {
|
|
514
|
+
fail(
|
|
515
|
+
"keel project goal/helper/tasks do not take --event, --authorize, or "
|
|
516
|
+
+ "--native-complete; those apply only to keel project without a "
|
|
517
|
+
+ "subcommand"
|
|
518
|
+
);
|
|
519
|
+
}
|
|
520
|
+
if (parsed.projectSubcommand === "tasks" && parsed.task !== null) {
|
|
521
|
+
fail(
|
|
522
|
+
"keel project tasks projects a whole change checklist; "
|
|
523
|
+
+ "--task is not supported"
|
|
524
|
+
);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
const helperFlagsUsed =
|
|
528
|
+
parsed.helperQuestion !== null
|
|
529
|
+
|| parsed.helperCommand !== null
|
|
530
|
+
|| parsed.helperReads.length > 0
|
|
531
|
+
|| parsed.helperExternal.length > 0
|
|
532
|
+
|| parsed.helperVerify
|
|
533
|
+
|| parsed.helperCaptureBaseline
|
|
534
|
+
|| parsed.helperBaseline !== null;
|
|
535
|
+
if (helperFlagsUsed && parsed.projectSubcommand !== "helper") {
|
|
536
|
+
fail(
|
|
537
|
+
"--brief, --command, --read, --external, --verify, --capture-baseline, "
|
|
538
|
+
+ "and --baseline apply only to keel project helper"
|
|
539
|
+
);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
return parsed;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
function pythonCandidates() {
|
|
546
|
+
if (process.env.KEEL_PYTHON) {
|
|
547
|
+
return [{ command: process.env.KEEL_PYTHON, prefixArgs: [] }];
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
const candidates = [];
|
|
551
|
+
if (process.platform === "win32") {
|
|
552
|
+
candidates.push({ command: "py", prefixArgs: ["-3"] });
|
|
553
|
+
candidates.push({ command: "python", prefixArgs: [] });
|
|
554
|
+
candidates.push({ command: "python3", prefixArgs: [] });
|
|
555
|
+
} else {
|
|
556
|
+
candidates.push({ command: "python3", prefixArgs: [] });
|
|
557
|
+
candidates.push({ command: "python", prefixArgs: [] });
|
|
558
|
+
}
|
|
559
|
+
return candidates;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
function commandExists(candidate) {
|
|
563
|
+
const result = spawnSync(
|
|
564
|
+
candidate.command,
|
|
565
|
+
[...candidate.prefixArgs, "--version"],
|
|
566
|
+
{ encoding: "utf8" }
|
|
567
|
+
);
|
|
568
|
+
return !result.error && result.status === 0;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
function npmCommand() {
|
|
572
|
+
return process.platform === "win32" ? "npm.cmd" : "npm";
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
function openspecCandidates() {
|
|
576
|
+
const localBin = path.join(
|
|
577
|
+
PACKAGE_ROOT,
|
|
578
|
+
"node_modules",
|
|
579
|
+
".bin",
|
|
580
|
+
process.platform === "win32" ? "openspec.cmd" : "openspec"
|
|
581
|
+
);
|
|
582
|
+
const candidates = [];
|
|
583
|
+
if (fs.existsSync(localBin)) {
|
|
584
|
+
candidates.push(localBin);
|
|
585
|
+
}
|
|
586
|
+
candidates.push(process.platform === "win32" ? "openspec.cmd" : "openspec");
|
|
587
|
+
candidates.push("openspec");
|
|
588
|
+
return [...new Set(candidates)];
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
function runCommand(command, args, options = {}) {
|
|
592
|
+
if (options.dryRun) {
|
|
593
|
+
process.stdout.write(
|
|
594
|
+
`keel: would run ${formatCommand(command, args)}`
|
|
595
|
+
+ (options.cwd ? ` in ${options.cwd}` : "")
|
|
596
|
+
+ "\n"
|
|
597
|
+
);
|
|
598
|
+
return 0;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
const result = spawnSync(command, args, {
|
|
602
|
+
cwd: options.cwd || process.cwd(),
|
|
603
|
+
stdio: options.stdio || "inherit",
|
|
604
|
+
encoding: options.encoding || "utf8",
|
|
605
|
+
shell: process.platform === "win32",
|
|
606
|
+
});
|
|
607
|
+
if (result.error) {
|
|
608
|
+
if (options.silentNotFound && result.error.code === "ENOENT") {
|
|
609
|
+
return 127;
|
|
610
|
+
}
|
|
611
|
+
process.stderr.write(
|
|
612
|
+
`keel: failed to run ${command}: ${result.error.message}\n`
|
|
613
|
+
);
|
|
614
|
+
return 1;
|
|
615
|
+
}
|
|
616
|
+
return typeof result.status === "number" ? result.status : 1;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
function findOpenSpecCommand() {
|
|
620
|
+
for (const command of openspecCandidates()) {
|
|
621
|
+
const status = runCommand(command, ["--version"], {
|
|
622
|
+
stdio: "ignore",
|
|
623
|
+
silentNotFound: true,
|
|
624
|
+
});
|
|
625
|
+
if (status === 0) {
|
|
626
|
+
return command;
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
return null;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
function formatCommand(command, args) {
|
|
633
|
+
const quote = (value) => {
|
|
634
|
+
if (/^[A-Za-z0-9_./:@+-]+$/.test(value)) {
|
|
635
|
+
return value;
|
|
636
|
+
}
|
|
637
|
+
return JSON.stringify(value);
|
|
638
|
+
};
|
|
639
|
+
return [command, ...args].map(quote).join(" ");
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
function findPackedTarball(packStdout, tempDir) {
|
|
643
|
+
try {
|
|
644
|
+
const parsed = JSON.parse(packStdout);
|
|
645
|
+
const packEntries = Array.isArray(parsed) ? parsed : [parsed];
|
|
646
|
+
for (const entry of packEntries) {
|
|
647
|
+
if (entry && typeof entry.filename === "string") {
|
|
648
|
+
const candidate = path.join(tempDir, entry.filename);
|
|
649
|
+
if (fs.existsSync(candidate)) {
|
|
650
|
+
return candidate;
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
} catch {
|
|
655
|
+
// Fall back to scanning the pack destination below.
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
const tarballs = fs
|
|
659
|
+
.readdirSync(tempDir)
|
|
660
|
+
.filter((entry) => entry.endsWith(".tgz"))
|
|
661
|
+
.map((entry) => path.join(tempDir, entry));
|
|
662
|
+
|
|
663
|
+
if (tarballs.length === 1) {
|
|
664
|
+
return tarballs[0];
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
throw new Error(
|
|
668
|
+
`expected one packed tarball in ${tempDir}, found ${tarballs.length}`
|
|
669
|
+
);
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
function runGlobalUpdate(options) {
|
|
673
|
+
if (options.repo !== null) {
|
|
674
|
+
fail("--update refreshes the global keel CLI and does not accept a repo path");
|
|
675
|
+
}
|
|
676
|
+
if (options.target !== "claude") {
|
|
677
|
+
fail("--update refreshes the global keel CLI and does not accept --target");
|
|
678
|
+
}
|
|
679
|
+
if (options.forceTemplateUpdate) {
|
|
680
|
+
fail("--update does not accept --force-template-update; use keel --install --force-template-update for project files");
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
const source =
|
|
684
|
+
options.updateSource || process.env.KEEL_UPDATE_SOURCE || DEFAULT_UPDATE_SOURCE;
|
|
685
|
+
const npm = npmCommand();
|
|
686
|
+
|
|
687
|
+
if (options.dryRun) {
|
|
688
|
+
process.stdout.write(
|
|
689
|
+
`keel: would run ${formatCommand(npm, [
|
|
690
|
+
"pack",
|
|
691
|
+
source,
|
|
692
|
+
"--pack-destination",
|
|
693
|
+
"<temporary-directory>",
|
|
694
|
+
"--json",
|
|
695
|
+
])}\n`
|
|
696
|
+
);
|
|
697
|
+
process.stdout.write(
|
|
698
|
+
`keel: would run ${formatCommand(npm, [
|
|
699
|
+
"install",
|
|
700
|
+
"-g",
|
|
701
|
+
"<packed-tarball>",
|
|
702
|
+
])}\n`
|
|
703
|
+
);
|
|
704
|
+
return 0;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "keel-update-"));
|
|
708
|
+
try {
|
|
709
|
+
const packArgs = [
|
|
710
|
+
"pack",
|
|
711
|
+
source,
|
|
712
|
+
"--pack-destination",
|
|
713
|
+
tempDir,
|
|
714
|
+
"--json",
|
|
715
|
+
];
|
|
716
|
+
const packResult = spawnSync(npm, packArgs, {
|
|
717
|
+
encoding: "utf8",
|
|
718
|
+
stdio: ["inherit", "pipe", "inherit"],
|
|
719
|
+
shell: process.platform === "win32",
|
|
720
|
+
});
|
|
721
|
+
if (packResult.error) {
|
|
722
|
+
process.stderr.write(
|
|
723
|
+
`keel: failed to run npm pack: ${packResult.error.message}\n`
|
|
724
|
+
);
|
|
725
|
+
return 1;
|
|
726
|
+
}
|
|
727
|
+
if (packResult.status !== 0) {
|
|
728
|
+
return typeof packResult.status === "number" ? packResult.status : 1;
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
const tarball = findPackedTarball(packResult.stdout, tempDir);
|
|
732
|
+
const installResult = spawnSync(npm, ["install", "-g", tarball], {
|
|
733
|
+
stdio: "inherit",
|
|
734
|
+
shell: process.platform === "win32",
|
|
735
|
+
});
|
|
736
|
+
if (installResult.error) {
|
|
737
|
+
process.stderr.write(
|
|
738
|
+
`keel: failed to run npm install: ${installResult.error.message}\n`
|
|
739
|
+
);
|
|
740
|
+
return 1;
|
|
741
|
+
}
|
|
742
|
+
return typeof installResult.status === "number" ? installResult.status : 1;
|
|
743
|
+
} catch (error) {
|
|
744
|
+
process.stderr.write(`keel: update failed: ${error.message}\n`);
|
|
745
|
+
return 1;
|
|
746
|
+
} finally {
|
|
747
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
function runPython(script, args) {
|
|
752
|
+
if (!fs.existsSync(script)) {
|
|
753
|
+
process.stderr.write(`keel: missing packaged script: ${script}\n`);
|
|
754
|
+
return 1;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
for (const candidate of pythonCandidates()) {
|
|
758
|
+
if (!commandExists(candidate)) {
|
|
759
|
+
continue;
|
|
760
|
+
}
|
|
761
|
+
const result = spawnSync(
|
|
762
|
+
candidate.command,
|
|
763
|
+
[...candidate.prefixArgs, script, ...args],
|
|
764
|
+
{ stdio: "inherit" }
|
|
765
|
+
);
|
|
766
|
+
return typeof result.status === "number" ? result.status : 1;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
process.stderr.write(
|
|
770
|
+
"keel: Python 3 is required. Install python3/python, or set KEEL_PYTHON.\n"
|
|
771
|
+
);
|
|
772
|
+
return 1;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
function installerArgs(options, extra = []) {
|
|
776
|
+
const repo = path.resolve(options.repo || process.cwd());
|
|
777
|
+
const args = [repo, "--target", options.target, ...extra];
|
|
778
|
+
if (options.dryRun) {
|
|
779
|
+
args.push("--dry-run");
|
|
780
|
+
}
|
|
781
|
+
if (options.forceTemplateUpdate) {
|
|
782
|
+
args.push("--force-template-update");
|
|
783
|
+
}
|
|
784
|
+
return args;
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
function openspecToolsForTarget(target) {
|
|
788
|
+
return target;
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
function runOpenSpec(args, repo, options) {
|
|
792
|
+
const command = options.dryRun ? "openspec" : findOpenSpecCommand();
|
|
793
|
+
if (!command) {
|
|
794
|
+
process.stderr.write(
|
|
795
|
+
"keel: OpenSpec CLI is provided by Keel's npm dependencies but was not found. Reinstall keel so npm installs its dependencies.\n"
|
|
796
|
+
);
|
|
797
|
+
return 1;
|
|
798
|
+
}
|
|
799
|
+
return runCommand(command, args, {
|
|
800
|
+
cwd: repo,
|
|
801
|
+
dryRun: options.dryRun,
|
|
802
|
+
});
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
function runProjectInit(options) {
|
|
806
|
+
if (options.updateSource !== null) {
|
|
807
|
+
fail("--source only applies to --update");
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
const repo = path.resolve(options.repo || process.cwd());
|
|
811
|
+
if (!options.dryRun) {
|
|
812
|
+
fs.mkdirSync(repo, { recursive: true });
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
const openspecTools = openspecToolsForTarget(options.target);
|
|
816
|
+
if (openspecTools) {
|
|
817
|
+
const initStatus = runOpenSpec(
|
|
818
|
+
["init", "--tools", openspecTools, "--force"],
|
|
819
|
+
repo,
|
|
820
|
+
options
|
|
821
|
+
);
|
|
822
|
+
if (initStatus !== 0) {
|
|
823
|
+
return initStatus;
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
const installStatus = runPython(INSTALL_SCRIPT, installerArgs(options));
|
|
828
|
+
if (installStatus !== 0) {
|
|
829
|
+
return installStatus;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
if (openspecTools) {
|
|
833
|
+
const updateStatus = runOpenSpec(["update", "--force"], repo, options);
|
|
834
|
+
if (updateStatus !== 0) {
|
|
835
|
+
return updateStatus;
|
|
836
|
+
}
|
|
837
|
+
return refreshOpenSpecSurfaceOverlay(repo, options.target, {
|
|
838
|
+
dryRun: options.dryRun,
|
|
839
|
+
}).status;
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
return 0;
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
function printDoctorLine(name, status, detail = "") {
|
|
846
|
+
process.stdout.write(`${name}: ${status}${detail ? ` - ${detail}` : ""}\n`);
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
function codexHome() {
|
|
850
|
+
const configured = (process.env.CODEX_HOME || "").trim();
|
|
851
|
+
return path.resolve(configured || path.join(os.homedir(), ".codex"));
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
function countExisting(paths) {
|
|
855
|
+
const existing = paths.filter((candidate) => fs.existsSync(candidate));
|
|
856
|
+
return { existing: existing.length, total: paths.length };
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
function formatCount(counts, location) {
|
|
860
|
+
return `${counts.existing}/${counts.total} under ${location}`;
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
function surfaceStatus(counts) {
|
|
864
|
+
return counts.existing === counts.total ? "ok" : "missing";
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
function skillRootForTarget(target) {
|
|
868
|
+
if (target === "claude") return path.join(".claude", "skills");
|
|
869
|
+
if (target === "codex") return path.join(".agents", "skills");
|
|
870
|
+
return path.join(".opencode", "skills");
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
function openspecSkillRootForTarget(target) {
|
|
874
|
+
if (target === "claude") return path.join(".claude", "skills");
|
|
875
|
+
if (target === "codex") return path.join(".codex", "skills");
|
|
876
|
+
return path.join(".opencode", "skills");
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
function commandSurfaceForTarget(target, repo) {
|
|
880
|
+
if (target === "claude") {
|
|
881
|
+
const location = path.join(".claude", "commands", "opsx");
|
|
882
|
+
return {
|
|
883
|
+
location,
|
|
884
|
+
paths: OPENSPEC_COMMAND_IDS.map((id) =>
|
|
885
|
+
path.join(repo, location, `${id}.md`)
|
|
886
|
+
),
|
|
887
|
+
remediation: "run keel --init --target claude or openspec update --force",
|
|
888
|
+
};
|
|
889
|
+
}
|
|
890
|
+
if (target === "codex") {
|
|
891
|
+
const home = codexHome();
|
|
892
|
+
const location = path.join(home, "prompts");
|
|
893
|
+
return {
|
|
894
|
+
location,
|
|
895
|
+
paths: OPENSPEC_COMMAND_IDS.map((id) =>
|
|
896
|
+
path.join(location, `opsx-${id}.md`)
|
|
897
|
+
),
|
|
898
|
+
remediation: "run keel --init --target codex or openspec update --force",
|
|
899
|
+
};
|
|
900
|
+
}
|
|
901
|
+
const location = path.join(".opencode", "commands");
|
|
902
|
+
return {
|
|
903
|
+
location,
|
|
904
|
+
paths: OPENSPEC_COMMAND_IDS.map((id) =>
|
|
905
|
+
path.join(repo, location, `opsx-${id}.md`)
|
|
906
|
+
),
|
|
907
|
+
remediation: "run keel --init --target opencode or openspec update --force",
|
|
908
|
+
};
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
function commandPathForAction(target, repo, action) {
|
|
912
|
+
if (target === "claude") {
|
|
913
|
+
return path.join(repo, ".claude", "commands", "opsx", `${action}.md`);
|
|
914
|
+
}
|
|
915
|
+
if (target === "codex") {
|
|
916
|
+
return path.join(codexHome(), "prompts", `opsx-${action}.md`);
|
|
917
|
+
}
|
|
918
|
+
return path.join(repo, ".opencode", "commands", `opsx-${action}.md`);
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
function openspecOverlaySurfacesForTarget(target, repo) {
|
|
922
|
+
const skillRoot = openspecSkillRootForTarget(target);
|
|
923
|
+
return OPENSPEC_OVERLAY_ACTIONS.flatMap((action) => {
|
|
924
|
+
if (action === "propose" && target === "opencode") {
|
|
925
|
+
return [];
|
|
926
|
+
}
|
|
927
|
+
const skillName =
|
|
928
|
+
action === "propose"
|
|
929
|
+
? "openspec-propose"
|
|
930
|
+
: action === "apply"
|
|
931
|
+
? "openspec-apply-change"
|
|
932
|
+
: "openspec-archive-change";
|
|
933
|
+
return [
|
|
934
|
+
{
|
|
935
|
+
action,
|
|
936
|
+
path: path.join(repo, skillRoot, skillName, "SKILL.md"),
|
|
937
|
+
},
|
|
938
|
+
{
|
|
939
|
+
action,
|
|
940
|
+
path: commandPathForAction(target, repo, action),
|
|
941
|
+
},
|
|
942
|
+
];
|
|
943
|
+
});
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
function overlayTitleForAction(action) {
|
|
947
|
+
if (action === "propose") return "Keel Authoring Overlay";
|
|
948
|
+
return action === "apply"
|
|
949
|
+
? "Keel Apply Overlay"
|
|
950
|
+
: "Keel Archive Overlay";
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
function keelOpenSpecOverlay(action) {
|
|
954
|
+
if (action === "propose") {
|
|
955
|
+
const lines = [
|
|
956
|
+
OPENSPEC_SURFACE_OVERLAY_START,
|
|
957
|
+
"## Keel Authoring Overlay",
|
|
958
|
+
"",
|
|
959
|
+
"Keel rules below take precedence over conflicting generic OpenSpec instructions in this file.",
|
|
960
|
+
"",
|
|
961
|
+
"### Expectation alignment before specs and tasks finalize",
|
|
962
|
+
"",
|
|
963
|
+
"- Before specs and executable tasks are finalized, run `keel-align-expectations`: quick path for complete low-risk requests, deep path when a material choice can change user-visible behavior, an external interface, acceptance, security/privacy/permission boundaries, data migration, protocol/state/timing/reset semantics, generated equivalence, irreversible cost, or a dependency commitment.",
|
|
964
|
+
"- Inspect repository code, tests, docs, and existing OpenSpec authority before asking the user a question those sources can answer; record verified facts as F<n> and escalate only user-owned product choices.",
|
|
965
|
+
"- Label inferred expectations as candidates; user silence never authorizes a material product decision, and unaccepted material candidates stay Q<n> or are explicitly discarded.",
|
|
966
|
+
"- In deep mode ask one material decision at a time, explain why it matters, and provide a recommended answer; stop once executable authority is clear.",
|
|
967
|
+
"- Route accepted outcomes to their durable owners (proposal, design, specs, tasks); create no separate alignment ledger and keep HANDOFF pointer-only.",
|
|
968
|
+
"- A proposal may start as a concise hypothesis, but no affected task becomes executable while a material expectation is unaccepted, unverified, unowned, and undiscarded.",
|
|
969
|
+
OPENSPEC_SURFACE_OVERLAY_END,
|
|
970
|
+
"",
|
|
971
|
+
];
|
|
972
|
+
return lines.join("\n");
|
|
973
|
+
}
|
|
974
|
+
const actionBody =
|
|
975
|
+
action === "apply"
|
|
976
|
+
? [
|
|
977
|
+
"- The current agent remains the Keel task owner and selects one unchecked task or a small contiguous task group from `tasks.md`.",
|
|
978
|
+
"- Run the Task Authoring Gate: each relevant critical expectation must be covered by a slice, deferred to a durable owner, or explicitly discarded.",
|
|
979
|
+
"- Run the Slice Start Gate: selected current slices must name source expectations and include Read, Touch, Acceptance, Commands, and Stop/Autonomy boundaries before implementation.",
|
|
980
|
+
"- Rough future slices may remain drafts, but cannot be selected for implementation or marked complete.",
|
|
981
|
+
"- Obey the selected task contract: Read is required starting context, Touch is the write boundary, Commands prove Acceptance, and the Autonomy boundary controls fallback decisions.",
|
|
982
|
+
"- Target-native subagents return report/evidence only; they cannot mark tasks complete, update OpenSpec state, commit, sync, archive, or change Acceptance.",
|
|
983
|
+
"- The current agent reviews all subagent output, command evidence, and diffs before marking any task complete.",
|
|
984
|
+
"- When implementation exposes a material expectation, acceptance boundary, or user-owned decision absent from durable authority, stop before implementing that choice, rerun `keel-align-expectations`, and reauthor the affected proposal/design/spec/task authority first.",
|
|
985
|
+
"- A discovered repository fact that does not change accepted behavior or scope may be recorded and execution continues inside the existing task boundary without a product interview.",
|
|
986
|
+
]
|
|
987
|
+
: [
|
|
988
|
+
"- The current agent owns final sync/archive decisions and must verify task evidence, follow-up ownership, and completion gates before proceeding.",
|
|
989
|
+
"- Before final sync/archive, each related critical expectation must have behavior evidence, a durable follow-up owner, or an explicit discard reason.",
|
|
990
|
+
"- Target-native subagents may help with bounded assessment or evidence production only; they cannot archive, sync, change acceptance, or bypass completion gates.",
|
|
991
|
+
"- The current agent reviews any subagent report before running `openspec-sync-specs`, `/opsx:sync`, or `/opsx:archive`.",
|
|
992
|
+
"- Do not treat generic OpenSpec archive delegation language as authority to transfer Keel ownership.",
|
|
993
|
+
];
|
|
994
|
+
|
|
995
|
+
const lines = [
|
|
996
|
+
OPENSPEC_SURFACE_OVERLAY_START,
|
|
997
|
+
`## ${overlayTitleForAction(action)}`,
|
|
998
|
+
"",
|
|
999
|
+
"Keel rules below take precedence over conflicting generic OpenSpec instructions in this file.",
|
|
1000
|
+
"",
|
|
1001
|
+
"### Target-native subagent gate",
|
|
1002
|
+
"",
|
|
1003
|
+
"- The current agent remains responsible for Keel ownership, task/archive decisions, scope control, and final reporting.",
|
|
1004
|
+
"- Use a target-native subagent only when the current agent decides it is useful for a bounded helper step.",
|
|
1005
|
+
"- Target-native subagents return report/evidence only; the current agent reviews the output before acting.",
|
|
1006
|
+
"- The subagent brief must name the selected change/task, required read context, allowed write boundary or read-only diagnostic scope, expected commands/evidence, and prohibited actions.",
|
|
1007
|
+
"- Prohibited actions include scope expansion, Acceptance changes, completion marking, sync/archive decisions, commits, handoff changes, and cross-runtime delegation unless the selected task or user explicitly authorizes them.",
|
|
1008
|
+
...actionBody,
|
|
1009
|
+
OPENSPEC_SURFACE_OVERLAY_END,
|
|
1010
|
+
"",
|
|
1011
|
+
];
|
|
1012
|
+
return lines.join("\n");
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
function mergeOpenSpecSurfaceOverlay(content, action) {
|
|
1016
|
+
const overlay = keelOpenSpecOverlay(action);
|
|
1017
|
+
if (OPENSPEC_SURFACE_OVERLAY_RE.test(content)) {
|
|
1018
|
+
return content.replace(OPENSPEC_SURFACE_OVERLAY_RE, overlay.trimEnd());
|
|
1019
|
+
}
|
|
1020
|
+
const separator = content.endsWith("\n") ? "\n" : "\n\n";
|
|
1021
|
+
return `${content}${separator}${overlay}`;
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
function refreshOpenSpecSurfaceOverlay(repo, target, options = {}) {
|
|
1025
|
+
const surfaces = openspecOverlaySurfacesForTarget(target, repo);
|
|
1026
|
+
const counts = {
|
|
1027
|
+
refreshed: 0,
|
|
1028
|
+
current: 0,
|
|
1029
|
+
missing: 0,
|
|
1030
|
+
};
|
|
1031
|
+
|
|
1032
|
+
if (options.dryRun) {
|
|
1033
|
+
for (const surface of surfaces) {
|
|
1034
|
+
process.stdout.write(
|
|
1035
|
+
`keel: would refresh OpenSpec ${surface.action} overlay in ${surface.path}\n`
|
|
1036
|
+
);
|
|
1037
|
+
}
|
|
1038
|
+
return { status: 0, ...counts };
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
for (const surface of surfaces) {
|
|
1042
|
+
if (!fs.existsSync(surface.path)) {
|
|
1043
|
+
counts.missing += 1;
|
|
1044
|
+
continue;
|
|
1045
|
+
}
|
|
1046
|
+
const content = fs.readFileSync(surface.path, "utf8");
|
|
1047
|
+
const next = mergeOpenSpecSurfaceOverlay(content, surface.action);
|
|
1048
|
+
if (next === content) {
|
|
1049
|
+
counts.current += 1;
|
|
1050
|
+
continue;
|
|
1051
|
+
}
|
|
1052
|
+
fs.writeFileSync(surface.path, next, "utf8");
|
|
1053
|
+
counts.refreshed += 1;
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
if (counts.refreshed > 0 || counts.current > 0) {
|
|
1057
|
+
process.stdout.write(
|
|
1058
|
+
"keel: OpenSpec apply/archive overlay "
|
|
1059
|
+
+ `refreshed=${counts.refreshed} current=${counts.current} `
|
|
1060
|
+
+ `missing=${counts.missing}\n`
|
|
1061
|
+
);
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
return { status: 0, ...counts };
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
function hasCurrentOpenSpecOverlay(filePath) {
|
|
1068
|
+
if (!fs.existsSync(filePath)) {
|
|
1069
|
+
return false;
|
|
1070
|
+
}
|
|
1071
|
+
return fs
|
|
1072
|
+
.readFileSync(filePath, "utf8")
|
|
1073
|
+
.includes(OPENSPEC_SURFACE_OVERLAY_START);
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
function countOpenSpecOverlays(paths) {
|
|
1077
|
+
const current = paths.filter(hasCurrentOpenSpecOverlay);
|
|
1078
|
+
return { existing: current.length, total: paths.length };
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
function overlayRemediation(target) {
|
|
1082
|
+
return `run keel --init --target ${target} or keel --install --target ${target}`;
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
function printTargetSurface(repo, target) {
|
|
1086
|
+
process.stdout.write("\nTarget surface:\n");
|
|
1087
|
+
|
|
1088
|
+
const agentsPath = path.join(repo, "AGENTS.md");
|
|
1089
|
+
const hasBootstrap =
|
|
1090
|
+
fs.existsSync(agentsPath)
|
|
1091
|
+
&& /<!--\s*keel:start(?:\s+[^>]*)?\s*-->/.test(
|
|
1092
|
+
fs.readFileSync(agentsPath, "utf8")
|
|
1093
|
+
);
|
|
1094
|
+
printDoctorLine(
|
|
1095
|
+
"bootstrap",
|
|
1096
|
+
hasBootstrap ? "ok" : "missing",
|
|
1097
|
+
hasBootstrap
|
|
1098
|
+
? "AGENTS.md carries the Keel managed bootstrap block"
|
|
1099
|
+
: `AGENTS.md bootstrap missing; run keel --install --target ${target}`
|
|
1100
|
+
);
|
|
1101
|
+
if (target === "claude") {
|
|
1102
|
+
const claudePath = path.join(repo, "CLAUDE.md");
|
|
1103
|
+
const hasImport =
|
|
1104
|
+
fs.existsSync(claudePath)
|
|
1105
|
+
&& fs.readFileSync(claudePath, "utf8").includes("@AGENTS.md");
|
|
1106
|
+
printDoctorLine(
|
|
1107
|
+
"CLAUDE import",
|
|
1108
|
+
hasImport ? "ok" : "missing",
|
|
1109
|
+
hasImport
|
|
1110
|
+
? "CLAUDE.md imports @AGENTS.md"
|
|
1111
|
+
: "CLAUDE.md lacks the @AGENTS.md import; run keel --install --target claude"
|
|
1112
|
+
);
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
if (target === "opencode") {
|
|
1116
|
+
printDoctorLine(
|
|
1117
|
+
"native plugin",
|
|
1118
|
+
"manual",
|
|
1119
|
+
"OpenCode has no v4 native plugin surface; existing artifacts are "
|
|
1120
|
+
+ "compatibility-only outside v4 support"
|
|
1121
|
+
);
|
|
1122
|
+
} else {
|
|
1123
|
+
const manifestRelative = path.join(
|
|
1124
|
+
"plugins",
|
|
1125
|
+
"keel",
|
|
1126
|
+
target === "codex" ? ".codex-plugin" : ".claude-plugin",
|
|
1127
|
+
"plugin.json"
|
|
1128
|
+
);
|
|
1129
|
+
const manifestPath = path.join(repo, manifestRelative);
|
|
1130
|
+
let sourceStatus = "missing";
|
|
1131
|
+
let sourceDetail = `plugin source absent at ${manifestRelative}`;
|
|
1132
|
+
if (fs.existsSync(manifestPath)) {
|
|
1133
|
+
try {
|
|
1134
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
|
|
1135
|
+
if (manifest.name === "keel" && manifest.version) {
|
|
1136
|
+
sourceStatus = "ok";
|
|
1137
|
+
sourceDetail = `plugin source valid (keel ${manifest.version}) at ${manifestRelative}`;
|
|
1138
|
+
if (manifest.version.split("+")[0] !== PACKAGE_JSON.version) {
|
|
1139
|
+
sourceStatus = "warning";
|
|
1140
|
+
sourceDetail =
|
|
1141
|
+
`plugin source version ${manifest.version} differs from CLI `
|
|
1142
|
+
+ `${PACKAGE_JSON.version}; align both before release`;
|
|
1143
|
+
}
|
|
1144
|
+
} else {
|
|
1145
|
+
sourceStatus = "warning";
|
|
1146
|
+
sourceDetail = `plugin source invalid at ${manifestRelative}`;
|
|
1147
|
+
}
|
|
1148
|
+
} catch {
|
|
1149
|
+
sourceStatus = "warning";
|
|
1150
|
+
sourceDetail = `plugin source unreadable at ${manifestRelative}`;
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
printDoctorLine("native plugin source", sourceStatus, sourceDetail);
|
|
1154
|
+
printDoctorLine(
|
|
1155
|
+
"native plugin runtime",
|
|
1156
|
+
"manual",
|
|
1157
|
+
"marketplace install, enablement, hook trust/activation, and behavior "
|
|
1158
|
+
+ `need runtime evidence; install via ${
|
|
1159
|
+
target === "codex"
|
|
1160
|
+
? "codex plugin add keel@<marketplace>"
|
|
1161
|
+
: "claude plugin install keel@<marketplace>"
|
|
1162
|
+
} and verify in a fresh session`
|
|
1163
|
+
);
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
const openspecSkillRoot = openspecSkillRootForTarget(target);
|
|
1167
|
+
const openspecSkillPaths = OPENSPEC_SKILLS.map((skill) =>
|
|
1168
|
+
path.join(repo, openspecSkillRoot, skill, "SKILL.md")
|
|
1169
|
+
);
|
|
1170
|
+
const openspecSkillCounts = countExisting(openspecSkillPaths);
|
|
1171
|
+
printDoctorLine(
|
|
1172
|
+
"OpenSpec action skills",
|
|
1173
|
+
surfaceStatus(openspecSkillCounts),
|
|
1174
|
+
formatCount(openspecSkillCounts, openspecSkillRoot)
|
|
1175
|
+
);
|
|
1176
|
+
|
|
1177
|
+
const commands = commandSurfaceForTarget(target, repo);
|
|
1178
|
+
const commandCounts = countExisting(commands.paths);
|
|
1179
|
+
const commandDetail =
|
|
1180
|
+
surfaceStatus(commandCounts) === "ok"
|
|
1181
|
+
? formatCount(commandCounts, commands.location)
|
|
1182
|
+
: `${formatCount(commandCounts, commands.location)}; ${commands.remediation}`;
|
|
1183
|
+
printDoctorLine(
|
|
1184
|
+
"OpenSpec commands",
|
|
1185
|
+
surfaceStatus(commandCounts),
|
|
1186
|
+
commandDetail
|
|
1187
|
+
);
|
|
1188
|
+
|
|
1189
|
+
const overlayPaths = openspecOverlaySurfacesForTarget(target, repo).map(
|
|
1190
|
+
(surface) => surface.path
|
|
1191
|
+
);
|
|
1192
|
+
const overlayCounts = countOpenSpecOverlays(overlayPaths);
|
|
1193
|
+
const overlayDetail =
|
|
1194
|
+
surfaceStatus(overlayCounts) === "ok"
|
|
1195
|
+
? formatCount(overlayCounts, "apply/archive skills and commands")
|
|
1196
|
+
: `${formatCount(
|
|
1197
|
+
overlayCounts,
|
|
1198
|
+
"apply/archive skills and commands"
|
|
1199
|
+
)}; ${overlayRemediation(target)}`;
|
|
1200
|
+
printDoctorLine(
|
|
1201
|
+
"Keel apply/archive overlay",
|
|
1202
|
+
surfaceStatus(overlayCounts),
|
|
1203
|
+
overlayDetail
|
|
1204
|
+
);
|
|
1205
|
+
|
|
1206
|
+
process.stdout.write("\n");
|
|
1207
|
+
process.stdout.write(renderCapabilities(probeCapabilities(repo, target)));
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
function runDoctor(options) {
|
|
1211
|
+
if (options.updateSource !== null) {
|
|
1212
|
+
fail("--source only applies to --update");
|
|
1213
|
+
}
|
|
1214
|
+
if (options.dryRun || options.forceTemplateUpdate) {
|
|
1215
|
+
fail("--doctor does not accept --dry-run or --force-template-update");
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
const repo = path.resolve(options.repo || process.cwd());
|
|
1219
|
+
process.stdout.write(`keel doctor for ${repo}\n`);
|
|
1220
|
+
|
|
1221
|
+
const python = pythonCandidates().find(commandExists);
|
|
1222
|
+
printDoctorLine(
|
|
1223
|
+
"python3",
|
|
1224
|
+
python ? "ok" : "missing",
|
|
1225
|
+
python ? formatCommand(python.command, python.prefixArgs) : "set KEEL_PYTHON or install Python 3"
|
|
1226
|
+
);
|
|
1227
|
+
|
|
1228
|
+
const openspec = findOpenSpecCommand();
|
|
1229
|
+
printDoctorLine(
|
|
1230
|
+
"openspec",
|
|
1231
|
+
openspec ? "ok" : "missing",
|
|
1232
|
+
openspec || "reinstall keel so npm installs its OpenSpec dependency"
|
|
1233
|
+
);
|
|
1234
|
+
|
|
1235
|
+
process.stdout.write("\nProject status:\n");
|
|
1236
|
+
const checkStatus = runPython(
|
|
1237
|
+
INSTALL_SCRIPT,
|
|
1238
|
+
installerArgs(options, ["--check"])
|
|
1239
|
+
);
|
|
1240
|
+
|
|
1241
|
+
if (openspec && fs.existsSync(path.join(repo, "openspec", "schemas", "keel-spec-driven"))) {
|
|
1242
|
+
const schemaStatus = runCommand(
|
|
1243
|
+
openspec,
|
|
1244
|
+
["schema", "validate", "keel-spec-driven"],
|
|
1245
|
+
{ cwd: repo }
|
|
1246
|
+
);
|
|
1247
|
+
printDoctorLine(
|
|
1248
|
+
"keel-spec-driven schema",
|
|
1249
|
+
schemaStatus === 0 ? "ok" : "failed"
|
|
1250
|
+
);
|
|
1251
|
+
} else {
|
|
1252
|
+
printDoctorLine(
|
|
1253
|
+
"keel-spec-driven schema",
|
|
1254
|
+
"not checked",
|
|
1255
|
+
"run keel --init or keel --install first"
|
|
1256
|
+
);
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
printTargetSurface(repo, options.target);
|
|
1260
|
+
|
|
1261
|
+
return checkStatus;
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
function runAction(options) {
|
|
1265
|
+
if (options.updateSource !== null && options.action !== "update") {
|
|
1266
|
+
fail("--source only applies to --update");
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
if (options.action === "context") {
|
|
1270
|
+
if (options.dryRun || options.forceTemplateUpdate || options.updateSource) {
|
|
1271
|
+
fail("context does not accept install or update options");
|
|
1272
|
+
}
|
|
1273
|
+
if (options.task && !options.change) {
|
|
1274
|
+
fail("context requires --change when --task is provided");
|
|
1275
|
+
}
|
|
1276
|
+
const repo = path.resolve(options.repo || process.cwd());
|
|
1277
|
+
let result;
|
|
1278
|
+
try {
|
|
1279
|
+
if (options.clearHandoff) {
|
|
1280
|
+
fs.rmSync(path.join(repo, "keel", "HANDOFF.md"), { force: true });
|
|
1281
|
+
}
|
|
1282
|
+
result = resolveContext(repo, options);
|
|
1283
|
+
} catch (error) {
|
|
1284
|
+
process.stderr.write(`keel: context input error: ${error.message}\n`);
|
|
1285
|
+
return 1;
|
|
1286
|
+
}
|
|
1287
|
+
process.stdout.write(
|
|
1288
|
+
options.json ? `${JSON.stringify(result, null, 2)}\n` : renderContext(result)
|
|
1289
|
+
);
|
|
1290
|
+
return 0;
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
if (options.action === "gate") {
|
|
1294
|
+
if (!options.gateStage) {
|
|
1295
|
+
fail("gate requires task-start, task-complete, or change-close");
|
|
1296
|
+
}
|
|
1297
|
+
if (options.dryRun || options.forceTemplateUpdate || options.updateSource) {
|
|
1298
|
+
fail("gate does not accept install or update options");
|
|
1299
|
+
}
|
|
1300
|
+
if (options.task && !options.change) {
|
|
1301
|
+
fail("gate requires --change when --task is provided");
|
|
1302
|
+
}
|
|
1303
|
+
const repo = path.resolve(options.repo || process.cwd());
|
|
1304
|
+
let result;
|
|
1305
|
+
try {
|
|
1306
|
+
result = runGate(repo, options.gateStage, options);
|
|
1307
|
+
} catch (error) {
|
|
1308
|
+
if (error instanceof GateInputError) {
|
|
1309
|
+
process.stderr.write(`keel: gate input error: ${error.message}\n`);
|
|
1310
|
+
return 1;
|
|
1311
|
+
}
|
|
1312
|
+
throw error;
|
|
1313
|
+
}
|
|
1314
|
+
process.stdout.write(
|
|
1315
|
+
options.json ? `${JSON.stringify(result, null, 2)}\n` : renderGate(result)
|
|
1316
|
+
);
|
|
1317
|
+
if (result.status === "pass") return 0;
|
|
1318
|
+
if (result.status === "needs-review") return 4;
|
|
1319
|
+
return 3;
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
if (options.action === "guard") {
|
|
1323
|
+
if (!["start", "status", "clear"].includes(options.guardSubcommand || "")) {
|
|
1324
|
+
fail("guard requires start, status, or clear");
|
|
1325
|
+
}
|
|
1326
|
+
if (options.dryRun || options.forceTemplateUpdate || options.updateSource) {
|
|
1327
|
+
fail("guard does not accept install or update options");
|
|
1328
|
+
}
|
|
1329
|
+
const repo = path.resolve(options.repo || process.cwd());
|
|
1330
|
+
let result;
|
|
1331
|
+
try {
|
|
1332
|
+
if (options.guardSubcommand === "start") {
|
|
1333
|
+
result = startGuard(repo, options);
|
|
1334
|
+
} else if (options.guardSubcommand === "status") {
|
|
1335
|
+
result = guardStatus(repo);
|
|
1336
|
+
} else {
|
|
1337
|
+
result = clearGuard(repo);
|
|
1338
|
+
}
|
|
1339
|
+
} catch (error) {
|
|
1340
|
+
if (error instanceof GuardInputError) {
|
|
1341
|
+
process.stderr.write(`keel: guard input error: ${error.message}\n`);
|
|
1342
|
+
return 1;
|
|
1343
|
+
}
|
|
1344
|
+
throw error;
|
|
1345
|
+
}
|
|
1346
|
+
process.stdout.write(
|
|
1347
|
+
options.json ? `${JSON.stringify(result, null, 2)}\n` : renderGuard(result)
|
|
1348
|
+
);
|
|
1349
|
+
return ["started", "active", "absent", "cleared"].includes(result.status)
|
|
1350
|
+
? 0
|
|
1351
|
+
: 3;
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
if (options.action === "capabilities") {
|
|
1355
|
+
if (options.dryRun || options.forceTemplateUpdate || options.updateSource) {
|
|
1356
|
+
fail("capabilities does not accept install or update options");
|
|
1357
|
+
}
|
|
1358
|
+
const repo = path.resolve(options.repo || process.cwd());
|
|
1359
|
+
const result = probeCapabilities(repo, options.target);
|
|
1360
|
+
process.stdout.write(
|
|
1361
|
+
options.json
|
|
1362
|
+
? `${JSON.stringify(result, null, 2)}\n`
|
|
1363
|
+
: renderCapabilities(result)
|
|
1364
|
+
);
|
|
1365
|
+
return 0;
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
if (options.action === "project") {
|
|
1369
|
+
if (options.dryRun || options.forceTemplateUpdate || options.updateSource) {
|
|
1370
|
+
fail("project does not accept install or update options");
|
|
1371
|
+
}
|
|
1372
|
+
if (options.task && !options.change) {
|
|
1373
|
+
fail("project requires --change when --task is provided");
|
|
1374
|
+
}
|
|
1375
|
+
const repo = path.resolve(options.repo || process.cwd());
|
|
1376
|
+
if (options.projectSubcommand === "goal") {
|
|
1377
|
+
const result = compileGoalProjection(repo, options);
|
|
1378
|
+
process.stdout.write(
|
|
1379
|
+
options.json
|
|
1380
|
+
? `${JSON.stringify(result, null, 2)}\n`
|
|
1381
|
+
: renderGoalProjection(result)
|
|
1382
|
+
);
|
|
1383
|
+
return result.status === "ready" ? 0 : 3;
|
|
1384
|
+
}
|
|
1385
|
+
if (options.projectSubcommand === "tasks") {
|
|
1386
|
+
const result = compileTasksView(repo, options);
|
|
1387
|
+
process.stdout.write(
|
|
1388
|
+
options.json
|
|
1389
|
+
? `${JSON.stringify(result, null, 2)}\n`
|
|
1390
|
+
: renderTasksView(result)
|
|
1391
|
+
);
|
|
1392
|
+
return result.status === "ready" ? 0 : 3;
|
|
1393
|
+
}
|
|
1394
|
+
if (options.projectSubcommand === "helper") {
|
|
1395
|
+
let result;
|
|
1396
|
+
try {
|
|
1397
|
+
if (options.helperCaptureBaseline) {
|
|
1398
|
+
result = captureHelperBaseline(repo, options);
|
|
1399
|
+
} else if (options.helperVerify) {
|
|
1400
|
+
result = verifyHelperEvidence(repo, options);
|
|
1401
|
+
} else {
|
|
1402
|
+
result = compileHelperBrief(repo, options);
|
|
1403
|
+
}
|
|
1404
|
+
} catch (error) {
|
|
1405
|
+
process.stderr.write(`keel: helper input error: ${error.message}\n`);
|
|
1406
|
+
return 1;
|
|
1407
|
+
}
|
|
1408
|
+
process.stdout.write(
|
|
1409
|
+
options.json
|
|
1410
|
+
? `${JSON.stringify(result, null, 2)}\n`
|
|
1411
|
+
: renderHelper(result)
|
|
1412
|
+
);
|
|
1413
|
+
const okStates = ["ready", "verified", "captured"];
|
|
1414
|
+
return okStates.includes(result.status) ? 0 : 3;
|
|
1415
|
+
}
|
|
1416
|
+
let result;
|
|
1417
|
+
try {
|
|
1418
|
+
result = projectRuntime(repo, options);
|
|
1419
|
+
} catch (error) {
|
|
1420
|
+
process.stderr.write(`keel: projection input error: ${error.message}\n`);
|
|
1421
|
+
return 1;
|
|
1422
|
+
}
|
|
1423
|
+
process.stdout.write(
|
|
1424
|
+
options.json
|
|
1425
|
+
? `${JSON.stringify(result, null, 2)}\n`
|
|
1426
|
+
: renderProjection(result)
|
|
1427
|
+
);
|
|
1428
|
+
return 0;
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
if (options.action === "init") {
|
|
1432
|
+
return runProjectInit(options);
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
if (options.action === "install") {
|
|
1436
|
+
const installStatus = runPython(INSTALL_SCRIPT, installerArgs(options));
|
|
1437
|
+
if (installStatus !== 0) {
|
|
1438
|
+
return installStatus;
|
|
1439
|
+
}
|
|
1440
|
+
return refreshOpenSpecSurfaceOverlay(
|
|
1441
|
+
path.resolve(options.repo || process.cwd()),
|
|
1442
|
+
options.target,
|
|
1443
|
+
{ dryRun: options.dryRun }
|
|
1444
|
+
).status;
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
if (options.action === "update") {
|
|
1448
|
+
return runGlobalUpdate(options);
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
if (options.action === "clear" || options.action === "uninstall") {
|
|
1452
|
+
return runPython(INSTALL_SCRIPT, installerArgs(options, ["--uninstall"]));
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
if (options.action === "check") {
|
|
1456
|
+
if (options.dryRun || options.forceTemplateUpdate) {
|
|
1457
|
+
fail("--check does not accept --dry-run or --force-template-update");
|
|
1458
|
+
}
|
|
1459
|
+
const checkStatus = runPython(
|
|
1460
|
+
INSTALL_SCRIPT,
|
|
1461
|
+
installerArgs(options, ["--check"])
|
|
1462
|
+
);
|
|
1463
|
+
if (checkStatus !== 0) {
|
|
1464
|
+
return checkStatus;
|
|
1465
|
+
}
|
|
1466
|
+
process.stdout.write("\nDry-run install plan:\n");
|
|
1467
|
+
return runPython(INSTALL_SCRIPT, installerArgs({ ...options, dryRun: true }));
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
if (options.action === "doctor") {
|
|
1471
|
+
return runDoctor(options);
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
fail("missing action");
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
function main() {
|
|
1478
|
+
const options = parseArgs(process.argv.slice(2));
|
|
1479
|
+
if (options.help || (!options.action && !options.version)) {
|
|
1480
|
+
printHelp();
|
|
1481
|
+
return 0;
|
|
1482
|
+
}
|
|
1483
|
+
if (options.version) {
|
|
1484
|
+
printVersion();
|
|
1485
|
+
return 0;
|
|
1486
|
+
}
|
|
1487
|
+
return runAction(options);
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
process.exit(main());
|