@christang/keel 5.14.0 → 5.20.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/assets/bootstrap/AGENTS.md +1 -1
- package/bin/keel.js +39 -17
- package/package.json +1 -1
- package/plugins/keel/.claude-plugin/plugin.json +1 -1
- package/plugins/keel/.codex-plugin/plugin.json +1 -1
- package/plugins/keel/skills/keel-review-checklist/SKILL.md +1 -1
- package/scripts/install_to_repo.py +12 -2
- package/scripts/validate_plugin.py +956 -2
- package/src/core/gates.js +111 -47
- package/src/core/guard.js +53 -0
- package/src/core/task-contract.js +18 -4
package/bin/keel.js
CHANGED
|
@@ -699,21 +699,35 @@ function openspecReportedVersion(command) {
|
|
|
699
699
|
return match ? match[0] : null;
|
|
700
700
|
}
|
|
701
701
|
|
|
702
|
-
|
|
702
|
+
// The root is the repository under diagnosis, never PACKAGE_ROOT. Rooting this
|
|
703
|
+
// at Keel's own install location made the line a statement about a repository
|
|
704
|
+
// the reader was never shown: a consumer pinning 9.9.9 was told the version
|
|
705
|
+
// Keel's checkout pins, and a global install — which ships no lockfile — was
|
|
706
|
+
// told `unreadable` forever, which is exactly the drift this check was added
|
|
707
|
+
// to expose. There is no fallback to PACKAGE_ROOT on purpose; falling back
|
|
708
|
+
// would reinstate the misattribution and leave the reader unable to tell which
|
|
709
|
+
// case they were in.
|
|
710
|
+
//
|
|
711
|
+
// Three outcomes, not two. A repository with no lockfile and one whose lockfile
|
|
712
|
+
// names no OpenSpec both *declare* nothing, which is the ordinary case for any
|
|
713
|
+
// project that does not depend on OpenSpec directly. A lockfile that exists and
|
|
714
|
+
// cannot be parsed is a read failure. Collapsing them would either warn at
|
|
715
|
+
// everyone or hide a real failure.
|
|
716
|
+
function declaredOpenSpecVersion(repo) {
|
|
717
|
+
const lockPath = path.join(repo, "package-lock.json");
|
|
718
|
+
if (!fs.existsSync(lockPath)) return { state: "none", version: null };
|
|
719
|
+
let lock;
|
|
703
720
|
try {
|
|
704
|
-
|
|
705
|
-
fs.readFileSync(path.join(PACKAGE_ROOT, "package-lock.json"), "utf8")
|
|
706
|
-
);
|
|
707
|
-
for (const [name, entry] of Object.entries(lock.packages || {})) {
|
|
708
|
-
if (name.endsWith("@fission-ai/openspec") && entry && entry.version) {
|
|
709
|
-
return entry.version;
|
|
710
|
-
}
|
|
711
|
-
}
|
|
721
|
+
lock = JSON.parse(fs.readFileSync(lockPath, "utf8"));
|
|
712
722
|
} catch {
|
|
713
|
-
|
|
714
|
-
// with, which is not the same as agreement and is reported as such.
|
|
723
|
+
return { state: "unreadable", version: null };
|
|
715
724
|
}
|
|
716
|
-
|
|
725
|
+
for (const [name, entry] of Object.entries(lock.packages || {})) {
|
|
726
|
+
if (name.endsWith("@fission-ai/openspec") && entry && entry.version) {
|
|
727
|
+
return { state: "declared", version: entry.version };
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
return { state: "none", version: null };
|
|
717
731
|
}
|
|
718
732
|
|
|
719
733
|
function findOpenSpecCommand() {
|
|
@@ -1426,16 +1440,24 @@ function runDoctor(options) {
|
|
|
1426
1440
|
silentNotFound: true,
|
|
1427
1441
|
}) === 0;
|
|
1428
1442
|
const resolvedVersion = openspecReportedVersion(openspec);
|
|
1429
|
-
const
|
|
1443
|
+
const declared = declaredOpenSpecVersion(repo);
|
|
1430
1444
|
const mismatched = Boolean(
|
|
1431
|
-
resolvedVersion
|
|
1445
|
+
resolvedVersion
|
|
1446
|
+
&& declared.state === "declared"
|
|
1447
|
+
&& resolvedVersion !== declared.version
|
|
1432
1448
|
);
|
|
1433
1449
|
const where = bareOpenSpecOnPath
|
|
1434
1450
|
? openspec
|
|
1435
1451
|
: `${openspec} is keel-resolvable but bare \`openspec\` is not on PATH — use \`keel openspec\``;
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1452
|
+
// Two versions on one line need two owners. `repo` is the repository named
|
|
1453
|
+
// on doctor's first line; the answering build is attributed by the path
|
|
1454
|
+
// already printed beside it.
|
|
1455
|
+
const declaredText = {
|
|
1456
|
+
declared: `repo pins ${declared.version}`,
|
|
1457
|
+
none: "repo declares no OpenSpec version",
|
|
1458
|
+
unreadable: "repo package-lock.json unreadable",
|
|
1459
|
+
}[declared.state];
|
|
1460
|
+
const versions = `${resolvedVersion || "version unreadable"}, ${declaredText}`;
|
|
1439
1461
|
printDoctorLine(
|
|
1440
1462
|
"openspec",
|
|
1441
1463
|
mismatched || !bareOpenSpecOnPath ? "warning" : "ok",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "keel",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.20.0",
|
|
4
4
|
"description": "Keel OpenSpec execution discipline: stateless continuity, task capsules, deterministic gates, and expectation alignment for Codex and Claude Code.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "TanglmChris",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "keel",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.20.0",
|
|
4
4
|
"description": "Keel OpenSpec execution discipline: stateless continuity, task capsules, deterministic gates, and expectation alignment for Codex and Claude Code.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "TanglmChris",
|
|
@@ -23,7 +23,7 @@ Read the selected OpenSpec proposal, design, specs, tasks, diff, and command evi
|
|
|
23
23
|
- For a red-green strategy (`vertical-tdd`, `regression-first`), confirm concrete per-label `.red` and `.green` Evidence exists for the same check; evidence-first tasks instead name their observable proof.
|
|
24
24
|
- A failure message must **name the actual cause** of what it reports. Watch for one condition guarding **two distinct failures** — `if result is None or result["status"] != expected` reports the first failure's message when the second one happened, sending the reader to a place with no problem in it. Split the condition. No gate can judge this: deciding whether a sentence misleads needs a model, so it stays here.
|
|
25
25
|
- When two tasks in the change declared the same Touch set under a red-green strategy — `keel gate task-start` warns about this — ask whether they turned out to be **one behavior** split in half. The tell is that the first task's minimal implementation was wrong in the field, or that the second had no honest red left because the first already made its checks pass. The gate can only see the shape; by completion you can see the outcome, which is the only point at which this is answerable.
|
|
26
|
-
-
|
|
26
|
+
- Scope attribution is now the gate's job by default: `keel gate task-complete` compares the worktree against the dirty set `task-start` recorded and refuses a path outside Touch, so a `pass` is evidence about scope rather than silence about it. Two cases still need you. When the gate reports paths as *unattributed* — no recorded set, because the manifest predates the field or the guard was cleared — nothing was checked and the review is the only scope evidence. And a path already dirty when the task started is never attributed even if the task changed it again, so read the gate's silence about such a path as absence of a check rather than absence of a write.
|
|
27
27
|
- For `Coupling: required`, confirm one complete candidate reached its completion gate and generated artifacts are aligned.
|
|
28
28
|
|
|
29
29
|
## Semantic Review
|
|
@@ -120,9 +120,19 @@ TASKS_COMMIT_STATUS_PATTERNS = (
|
|
|
120
120
|
"remove branch merge state from tasks.md; git log is the source of truth",
|
|
121
121
|
),
|
|
122
122
|
)
|
|
123
|
+
# A commit identifier is hexadecimal, and `a`-`f` are the only reason those
|
|
124
|
+
# letters appear in one. A run of decimal digits alone is a phone number, a
|
|
125
|
+
# timestamp, an order number, or a port — evidence prose, not state git owns.
|
|
126
|
+
# Matching one as a recorded identifier (#58) asks the author to reword
|
|
127
|
+
# something that was true, and a check that refuses correct work is one people
|
|
128
|
+
# learn to route around. The length and word-boundary conditions are kept
|
|
129
|
+
# verbatim inside the lookahead, so the token that matches is the token that
|
|
130
|
+
# always matched, minus the all-decimal ones.
|
|
131
|
+
_HASH_SHAPED_TOKEN = r"\b(?=[0-9a-f]{7,40}\b)[0-9a-f]*[a-f][0-9a-f]*\b"
|
|
132
|
+
_HASH_CONTEXT_WORD = r"(?:commit|提交|合入|master|main|HEAD|hash|哈希)"
|
|
123
133
|
TASKS_CONTEXTUAL_HASH_RE = re.compile(
|
|
124
|
-
|
|
125
|
-
|
|
134
|
+
rf"(?i){_HASH_CONTEXT_WORD}.*{_HASH_SHAPED_TOKEN}|"
|
|
135
|
+
rf"{_HASH_SHAPED_TOKEN}.*{_HASH_CONTEXT_WORD}"
|
|
126
136
|
)
|
|
127
137
|
|
|
128
138
|
|