@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.
Files changed (39) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +250 -0
  3. package/README.zh-CN.md +295 -0
  4. package/assets/bootstrap/AGENTS.md +9 -0
  5. package/assets/openspec/schemas/keel-spec-driven/schema.yaml +166 -0
  6. package/assets/openspec/schemas/keel-spec-driven/templates/design.md +52 -0
  7. package/assets/openspec/schemas/keel-spec-driven/templates/proposal.md +21 -0
  8. package/assets/openspec/schemas/keel-spec-driven/templates/spec.md +8 -0
  9. package/assets/openspec/schemas/keel-spec-driven/templates/tasks.md +68 -0
  10. package/bin/keel.js +1490 -0
  11. package/package.json +35 -0
  12. package/plugins/keel/.claude-plugin/plugin.json +17 -0
  13. package/plugins/keel/.codex-plugin/plugin.json +29 -0
  14. package/plugins/keel/agents/keel-single-task-goal-claude.md +16 -0
  15. package/plugins/keel/agents/keel-single-task-goal-codex.md +16 -0
  16. package/plugins/keel/hooks/hooks.json +30 -0
  17. package/plugins/keel/scripts/pretooluse-guard.js +156 -0
  18. package/plugins/keel/scripts/session-start.js +182 -0
  19. package/plugins/keel/skills/keel-align-expectations/SKILL.md +53 -0
  20. package/plugins/keel/skills/keel-align-expectations/references/hardware-dsl.md +21 -0
  21. package/plugins/keel/skills/keel-align-expectations/references/hardware.md +21 -0
  22. package/plugins/keel/skills/keel-align-expectations/references/web.md +21 -0
  23. package/plugins/keel/skills/keel-debug-failure/SKILL.md +41 -0
  24. package/plugins/keel/skills/keel-handoff/SKILL.md +45 -0
  25. package/plugins/keel/skills/keel-review-checklist/SKILL.md +73 -0
  26. package/plugins/keel/skills/keel-run-single-task-goal/SKILL.md +68 -0
  27. package/plugins/keel/skills/keel-tdd-or-test-first/SKILL.md +45 -0
  28. package/scripts/install_to_repo.py +1122 -0
  29. package/scripts/run_python.js +63 -0
  30. package/scripts/validate_plugin.py +9869 -0
  31. package/src/core/capabilities.js +291 -0
  32. package/src/core/context.js +514 -0
  33. package/src/core/gates.js +643 -0
  34. package/src/core/goal.js +230 -0
  35. package/src/core/guard.js +295 -0
  36. package/src/core/helper.js +319 -0
  37. package/src/core/projection.js +195 -0
  38. package/src/core/task-contract.js +736 -0
  39. package/src/core/tasksview.js +123 -0
@@ -0,0 +1,230 @@
1
+ "use strict";
2
+
3
+ // Keel 4.1.0 single-task native goal projection contract.
4
+ //
5
+ // Compiles one passing `keel-task-capsule/v1` into an ephemeral
6
+ // `keel-native-goal/v1` projection for a supported native runtime. The
7
+ // projection is never written as Keel-owned state; OpenSpec, Git, the capsule
8
+ // fingerprint, and deterministic gates remain durable authority.
9
+
10
+ const { resolveContext } = require("./context");
11
+ const { loadTaskContract } = require("./task-contract");
12
+ const { probeCapabilities } = require("./capabilities");
13
+
14
+ const GOAL_VERSION = "keel-native-goal/v1";
15
+ const CLAUDE_CONDITION_LIMIT = 4000;
16
+ const SUPPORTED_GOAL_TARGETS = new Set(["codex", "claude"]);
17
+ const TERMINAL_STATES = ["complete", "blocked", "paused"];
18
+ const EVIDENCE_PRESENTATION = [
19
+ "Surface every Command result and gate outcome in the transcript before any success claim.",
20
+ "Native evaluator success alone never marks or reports the OpenSpec task complete.",
21
+ "Achievement requires matching change/task/fingerprint, Acceptance demonstrated by the named Commands, passing Review, passing task-complete, and the durably checked task checkbox.",
22
+ ];
23
+
24
+ function blockedGoal(target, reason, warnings = [], extra = {}) {
25
+ return {
26
+ version: GOAL_VERSION,
27
+ status: "blocked",
28
+ target,
29
+ source: null,
30
+ capability: null,
31
+ goal: null,
32
+ reasons: [reason],
33
+ warnings,
34
+ ...extra,
35
+ };
36
+ }
37
+
38
+ function renderCondition(goal) {
39
+ const lines = [
40
+ `Goal: complete exactly one OpenSpec task — ${goal.owner} (fingerprint ${goal.fingerprint.value}).`,
41
+ `Objective: ${goal.objective}`,
42
+ "Acceptance (all must be demonstrated by the named Commands):",
43
+ ...goal.acceptance.map((item) => `- ${item}`),
44
+ "Commands:",
45
+ ...goal.commands.map((item) => `- ${item}`),
46
+ `Verification strategy: ${goal.verificationStrategy}.`,
47
+ `Write boundary (Touch): ${goal.touch.join(", ")}.`,
48
+ "Stop/Autonomy boundary:",
49
+ ...goal.stopBoundary.map((item) => `- ${item}`),
50
+ "Ownership: the current agent is the sole writer and owns Review, gate invocation, the task checkbox, and completion.",
51
+ "Done only when: task-complete passes and the current agent has durably checked the task; then stop and require a new explicit authorization before any next task.",
52
+ ];
53
+ return lines.join("\n");
54
+ }
55
+
56
+ function compileGoalProjection(repo, options) {
57
+ const target = options.target;
58
+ if (!SUPPORTED_GOAL_TARGETS.has(target)) {
59
+ return blockedGoal(
60
+ target,
61
+ `Native single-task goal execution supports codex and claude only; `
62
+ + `${target || "<missing>"} remains manual/compatibility-only.`
63
+ );
64
+ }
65
+ if (!options.change || !options.task) {
66
+ return blockedGoal(
67
+ target,
68
+ "Native goal activation requires one explicit --change and --task "
69
+ + "selection; ambiguous, multiple, or task-group activation is rejected."
70
+ );
71
+ }
72
+
73
+ const context = resolveContext(repo, {
74
+ change: options.change,
75
+ task: options.task,
76
+ });
77
+ if (context.status !== "ready" || !context.selection) {
78
+ return blockedGoal(
79
+ target,
80
+ context.reasons.join(" ") || "Current OpenSpec context is not ready.",
81
+ context.warnings
82
+ );
83
+ }
84
+ const change = context.selection.change;
85
+ const taskId = context.selection.task;
86
+ if (!taskId) {
87
+ return blockedGoal(
88
+ target,
89
+ "Native goal activation requires one selected executable task, not a "
90
+ + "change backlog or contiguous task group.",
91
+ context.warnings
92
+ );
93
+ }
94
+
95
+ const loaded = loadTaskContract(repo, change, taskId);
96
+ if (!loaded) {
97
+ return blockedGoal(
98
+ target,
99
+ `Selected durable task owner openspec/changes/${change}/tasks.md#${taskId} `
100
+ + "is missing.",
101
+ context.warnings
102
+ );
103
+ }
104
+ if (loaded.task.checked) {
105
+ return blockedGoal(
106
+ target,
107
+ "Selected task is already complete; a later task requires a new explicit "
108
+ + "user authorization and a new start fingerprint.",
109
+ context.warnings
110
+ );
111
+ }
112
+ if (loaded.contract.diagnostics.length > 0) {
113
+ return blockedGoal(
114
+ target,
115
+ loaded.contract.diagnostics.map((item) => item.message).join(" "),
116
+ context.warnings
117
+ );
118
+ }
119
+
120
+ const contract = loaded.contract;
121
+ const capsule = contract.capsule;
122
+ const owner = `openspec/changes/${change}/tasks.md#${taskId}`;
123
+
124
+ // Continuity is reconstructed from durable OpenSpec/Git authority, never a
125
+ // Keel cursor or cache: a resume passes the previously recorded owner and
126
+ // fingerprint, and any divergence hard-stops rather than silently rebinding.
127
+ if (options.expectedOwner && options.expectedOwner !== owner) {
128
+ return blockedGoal(
129
+ target,
130
+ `Recorded authorization owns ${options.expectedOwner}, but the current `
131
+ + `OpenSpec selection is ${owner}; checkout divergence requires new `
132
+ + "explicit authorization, not automatic rebinding.",
133
+ context.warnings
134
+ );
135
+ }
136
+ if (
137
+ options.expectedFingerprint
138
+ && options.expectedFingerprint !== contract.fingerprint.value
139
+ ) {
140
+ return blockedGoal(
141
+ target,
142
+ `Recompiled capsule fingerprint ${contract.fingerprint.value} does not `
143
+ + `match the recorded authorization ${options.expectedFingerprint}; `
144
+ + "fingerprint drift requires reauthorization before any product write.",
145
+ context.warnings
146
+ );
147
+ }
148
+
149
+ const capabilities = probeCapabilities(repo, target);
150
+ const capability = capabilities.capabilities["execution.goal"];
151
+
152
+ const goal = {
153
+ version: GOAL_VERSION,
154
+ target,
155
+ change,
156
+ task: taskId,
157
+ fingerprint: contract.fingerprint,
158
+ objective: capsule.task.title,
159
+ acceptance: capsule.acceptance,
160
+ commands: capsule.verification.commands.map(
161
+ (item) => `${item.label}: ${item.check}`
162
+ ),
163
+ verificationStrategy: capsule.verification.strategy,
164
+ touch: capsule.touch,
165
+ stopBoundary: [...capsule.boundaries.stop, ...capsule.boundaries.autonomy],
166
+ owner: capsule.owner,
167
+ ownership: "current-agent-sole-writer",
168
+ helperPolicy: capsule.helperAuthority,
169
+ terminalStates: TERMINAL_STATES,
170
+ evidencePresentation: EVIDENCE_PRESENTATION,
171
+ authorizationEvidence: {
172
+ field: "Automation authorization: single-task",
173
+ target,
174
+ change,
175
+ task: taskId,
176
+ fingerprint: contract.fingerprint.value,
177
+ },
178
+ prohibitions: capsule.prohibitions,
179
+ };
180
+ goal.owner = owner;
181
+ const conditionText = renderCondition(goal);
182
+ goal.condition = conditionText;
183
+ goal.conditionLength = conditionText.length;
184
+
185
+ if (target === "claude" && conditionText.length > CLAUDE_CONDITION_LIMIT) {
186
+ return blockedGoal(
187
+ target,
188
+ `Compiled goal condition is ${conditionText.length} characters, above the `
189
+ + `Claude ${CLAUDE_CONDITION_LIMIT}-character limit; refusing native `
190
+ + "activation rather than omitting Acceptance, fingerprint, or stop "
191
+ + "authority. Use the manual current-agent loop instead.",
192
+ context.warnings,
193
+ { conditionLength: conditionText.length }
194
+ );
195
+ }
196
+
197
+ return {
198
+ version: GOAL_VERSION,
199
+ status: "ready",
200
+ target,
201
+ source: { authority: "OpenSpec", owner, change, task: taskId },
202
+ capability,
203
+ goal,
204
+ reasons: [],
205
+ warnings: context.warnings,
206
+ };
207
+ }
208
+
209
+ function renderGoalProjection(result) {
210
+ const lines = [
211
+ `Keel native goal: ${result.status}`,
212
+ `Target: ${result.target}`,
213
+ ];
214
+ if (result.source) lines.push(`Owner: ${result.source.owner}`);
215
+ if (result.goal) {
216
+ lines.push(`Fingerprint: ${result.goal.fingerprint.value}`);
217
+ lines.push(`Condition length: ${result.goal.conditionLength}`);
218
+ }
219
+ for (const reason of result.reasons || []) lines.push(`Reason: ${reason}`);
220
+ for (const warning of result.warnings || []) lines.push(`Warning: ${warning}`);
221
+ return `${lines.join("\n")}\n`;
222
+ }
223
+
224
+ module.exports = {
225
+ GOAL_VERSION,
226
+ CLAUDE_CONDITION_LIMIT,
227
+ SUPPORTED_GOAL_TARGETS,
228
+ compileGoalProjection,
229
+ renderGoalProjection,
230
+ };
@@ -0,0 +1,295 @@
1
+ "use strict";
2
+
3
+ // Keel 4.x write-guard contract: an explicit, fingerprinted, disposable
4
+ // enforcement manifest for exactly one task. The manifest is never selection,
5
+ // continuity, or completion authority; only its presence authorizes the
6
+ // plugin PreToolUse hook to deny out-of-Touch file edits, and every broken
7
+ // state fails closed through guard status while absence changes nothing.
8
+
9
+ const crypto = require("crypto");
10
+ const fs = require("fs");
11
+ const path = require("path");
12
+ const { loadTaskContract } = require("./task-contract");
13
+
14
+ const MANIFEST_SCHEMA = "keel-write-guard/v1";
15
+
16
+ class GuardInputError extends Error {}
17
+
18
+ function manifestFile(repo) {
19
+ return path.join(repo, "keel", "guard.json");
20
+ }
21
+
22
+ function sha256(buffer) {
23
+ return crypto.createHash("sha256").update(buffer).digest("hex");
24
+ }
25
+
26
+ function guardResult(subcommand, status, extra = {}) {
27
+ return {
28
+ schemaVersion: 1,
29
+ command: "guard",
30
+ subcommand,
31
+ status,
32
+ manifestPath: "keel/guard.json",
33
+ problems: [],
34
+ warnings: [
35
+ "The guard manifest is a disposable enforcement pointer; OpenSpec and "
36
+ + "Git remain the only durable authority and selection never derives "
37
+ + "from it.",
38
+ ],
39
+ ...extra,
40
+ };
41
+ }
42
+
43
+ function authorityPaths(repo, change, contract) {
44
+ const paths = new Set([`openspec/changes/${change}/tasks.md`]);
45
+ for (const item of contract.capsule.authority) {
46
+ const source = String(item.source || "").split("#")[0].trim();
47
+ if (source && fs.existsSync(path.join(repo, source))) {
48
+ paths.add(source.replace(/\\/g, "/"));
49
+ }
50
+ }
51
+ return [...paths].sort();
52
+ }
53
+
54
+ function hashAuthority(repo, paths) {
55
+ return paths.map((relative) => ({
56
+ path: relative,
57
+ sha256: sha256(fs.readFileSync(path.join(repo, relative))),
58
+ }));
59
+ }
60
+
61
+ function readManifest(repo) {
62
+ const file = manifestFile(repo);
63
+ if (!fs.existsSync(file)) return { state: "absent" };
64
+ let manifest;
65
+ try {
66
+ manifest = JSON.parse(fs.readFileSync(file, "utf8"));
67
+ } catch {
68
+ return {
69
+ state: "invalid",
70
+ problems: [
71
+ {
72
+ code: "invalid-manifest",
73
+ message:
74
+ "keel/guard.json is unreadable or not JSON; run `keel guard "
75
+ + "clear` and reauthorize with `keel guard start`.",
76
+ },
77
+ ],
78
+ };
79
+ }
80
+ const shapeErrors = [];
81
+ if (manifest.schema !== MANIFEST_SCHEMA) {
82
+ shapeErrors.push(`schema must be ${MANIFEST_SCHEMA}`);
83
+ }
84
+ if (typeof manifest.change !== "string" || !manifest.change) {
85
+ shapeErrors.push("change must be a non-empty string");
86
+ }
87
+ if (typeof manifest.task !== "string" || !manifest.task) {
88
+ shapeErrors.push("task must be a non-empty string");
89
+ }
90
+ if (
91
+ !manifest.fingerprint
92
+ || manifest.fingerprint.algorithm !== "sha256"
93
+ || !/^[0-9a-f]{64}$/.test(String(manifest.fingerprint.value || ""))
94
+ ) {
95
+ shapeErrors.push("fingerprint must record a sha256 value");
96
+ }
97
+ if (
98
+ !Array.isArray(manifest.touch)
99
+ || manifest.touch.length === 0
100
+ || manifest.touch.some((item) => typeof item !== "string" || !item)
101
+ ) {
102
+ shapeErrors.push("touch must be a non-empty string list");
103
+ }
104
+ if (
105
+ !Array.isArray(manifest.authority)
106
+ || manifest.authority.length === 0
107
+ || manifest.authority.some(
108
+ (item) =>
109
+ !item
110
+ || typeof item.path !== "string"
111
+ || !/^[0-9a-f]{64}$/.test(String(item.sha256 || ""))
112
+ )
113
+ ) {
114
+ shapeErrors.push("authority must list hashed source files");
115
+ }
116
+ if (shapeErrors.length > 0) {
117
+ return {
118
+ state: "invalid",
119
+ problems: shapeErrors.map((message) => ({
120
+ code: "invalid-manifest",
121
+ message:
122
+ `keel/guard.json is invalid (${message}); run \`keel guard clear\` `
123
+ + "and reauthorize with `keel guard start`.",
124
+ })),
125
+ };
126
+ }
127
+ return { state: "ok", manifest };
128
+ }
129
+
130
+ function startGuard(repo, options) {
131
+ if (!options.change || !options.task) {
132
+ throw new GuardInputError("guard start requires --change and --task");
133
+ }
134
+ const loaded = loadTaskContract(repo, options.change, options.task);
135
+ if (!loaded) {
136
+ throw new GuardInputError(
137
+ `task ${options.change}#${options.task} does not exist`
138
+ );
139
+ }
140
+ const problems = [];
141
+ if (loaded.task.checked) {
142
+ problems.push({
143
+ code: "task-completed",
144
+ message:
145
+ `Task ${options.change}#${options.task} is already checked complete; `
146
+ + "a completed task cannot be guarded. Run `keel guard clear` and "
147
+ + "authorize a new task explicitly.",
148
+ });
149
+ }
150
+ problems.push(...loaded.contract.diagnostics);
151
+
152
+ const existing = readManifest(repo);
153
+ if (
154
+ problems.length === 0
155
+ && existing.state === "ok"
156
+ && (
157
+ existing.manifest.change !== options.change
158
+ || existing.manifest.task !== options.task
159
+ )
160
+ && !options.force
161
+ ) {
162
+ problems.push({
163
+ code: "guard-active",
164
+ message:
165
+ `An active guard already covers ${existing.manifest.change}#`
166
+ + `${existing.manifest.task}; run \`keel guard clear\` first or pass `
167
+ + "--force to replace it.",
168
+ });
169
+ }
170
+ if (problems.length > 0) {
171
+ const refused = guardResult("start", "refused");
172
+ refused.problems = problems;
173
+ return refused;
174
+ }
175
+
176
+ const paths = authorityPaths(repo, options.change, loaded.contract);
177
+ const manifest = {
178
+ schema: MANIFEST_SCHEMA,
179
+ change: options.change,
180
+ task: options.task,
181
+ fingerprint: loaded.contract.fingerprint,
182
+ touch: loaded.contract.capsule.touch,
183
+ authority: hashAuthority(repo, paths),
184
+ };
185
+ fs.mkdirSync(path.join(repo, "keel"), { recursive: true });
186
+ fs.writeFileSync(
187
+ manifestFile(repo),
188
+ `${JSON.stringify(manifest, null, 2)}\n`,
189
+ "utf8"
190
+ );
191
+ return guardResult("start", "started", { manifest });
192
+ }
193
+
194
+ function guardStatus(repo) {
195
+ const existing = readManifest(repo);
196
+ if (existing.state === "absent") {
197
+ return guardResult("status", "absent");
198
+ }
199
+ if (existing.state === "invalid") {
200
+ const invalid = guardResult("status", "invalid");
201
+ invalid.problems = existing.problems;
202
+ return invalid;
203
+ }
204
+ const manifest = existing.manifest;
205
+ const problems = [];
206
+ const loaded = loadTaskContract(repo, manifest.change, manifest.task);
207
+ if (!loaded) {
208
+ problems.push({
209
+ code: "authority-drift",
210
+ message:
211
+ `Guarded task ${manifest.change}#${manifest.task} no longer resolves; `
212
+ + "reauthorize through `keel gate task-start` and `keel guard start`.",
213
+ });
214
+ const drifted = guardResult("status", "drifted", { manifest });
215
+ drifted.problems = problems;
216
+ return drifted;
217
+ }
218
+ if (loaded.task.checked) {
219
+ const completed = guardResult("status", "completed", { manifest });
220
+ completed.problems = [
221
+ {
222
+ code: "task-completed",
223
+ message:
224
+ `Guarded task ${manifest.change}#${manifest.task} is checked `
225
+ + "complete; run `keel guard clear` before authorizing new work.",
226
+ },
227
+ ];
228
+ return completed;
229
+ }
230
+ if (loaded.contract.diagnostics.length > 0) {
231
+ problems.push(...loaded.contract.diagnostics);
232
+ } else if (
233
+ loaded.contract.fingerprint.value !== manifest.fingerprint.value
234
+ ) {
235
+ problems.push({
236
+ code: "fingerprint-drift",
237
+ message:
238
+ "The recompiled capsule fingerprint no longer matches the guard; "
239
+ + "reauthorize through `keel gate task-start` and `keel guard start`.",
240
+ });
241
+ }
242
+ for (const entry of manifest.authority) {
243
+ const file = path.join(repo, entry.path);
244
+ if (!fs.existsSync(file) || sha256(fs.readFileSync(file)) !== entry.sha256) {
245
+ problems.push({
246
+ code: "authority-drift",
247
+ message:
248
+ `Recorded authority hash for ${entry.path} no longer matches; `
249
+ + "reauthorize through `keel gate task-start` and `keel guard start`.",
250
+ });
251
+ }
252
+ }
253
+ if (problems.length > 0) {
254
+ const drifted = guardResult("status", "drifted", { manifest });
255
+ drifted.problems = problems;
256
+ return drifted;
257
+ }
258
+ return guardResult("status", "active", { manifest });
259
+ }
260
+
261
+ function clearGuard(repo) {
262
+ const file = manifestFile(repo);
263
+ if (!fs.existsSync(file)) {
264
+ return guardResult("clear", "absent");
265
+ }
266
+ fs.rmSync(file, { force: true });
267
+ return guardResult("clear", "cleared");
268
+ }
269
+
270
+ function renderGuard(result) {
271
+ const lines = [
272
+ `Keel guard: ${result.subcommand}`,
273
+ `Status: ${result.status}`,
274
+ ];
275
+ if (result.manifest) {
276
+ lines.push(
277
+ `Selection: ${result.manifest.change}#${result.manifest.task}`,
278
+ `Fingerprint: ${result.manifest.fingerprint.algorithm}:`
279
+ + result.manifest.fingerprint.value
280
+ );
281
+ }
282
+ for (const item of result.problems) lines.push(`Problem: ${item.message}`);
283
+ for (const warning of result.warnings) lines.push(`Warning: ${warning}`);
284
+ return `${lines.join("\n")}\n`;
285
+ }
286
+
287
+ module.exports = {
288
+ GuardInputError,
289
+ MANIFEST_SCHEMA,
290
+ clearGuard,
291
+ guardStatus,
292
+ readManifest,
293
+ renderGuard,
294
+ startGuard,
295
+ };