@christang/keel 5.6.0 → 5.14.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.
@@ -2,6 +2,10 @@
2
2
 
3
3
  // Keel 4.1.0 one-way native projection contract.
4
4
 
5
+ const fs = require("fs");
6
+ const path = require("path");
7
+
8
+ const { readDelegationPolicy } = require("./config");
5
9
  const { resolveContext } = require("./context");
6
10
  const { loadTaskContract } = require("./task-contract");
7
11
  const { probeCapabilities } = require("./capabilities");
@@ -34,6 +38,39 @@ function blocked(target, event, reason, warnings = []) {
34
38
  };
35
39
  }
36
40
 
41
+ // Both refusals are decided before a delegate starts, never inferred from what
42
+ // it did. An absent manifest passes every write through silently, so a delegate
43
+ // that wrote successfully under one proves nothing about having been checked —
44
+ // there is no observable difference afterwards, which is why the condition has
45
+ // to be answered here.
46
+ function delegationRefusal(repo, delegation) {
47
+ // The policy is read directly rather than through the capsule, because the
48
+ // capsule cannot express the difference this refusal turns on. A tier outside
49
+ // the vocabulary fails closed at the config layer and reaches the capsule as
50
+ // no delegation at all — identical to a repository that declared nothing. One
51
+ // of those should proceed silently and the other must be reported, so the
52
+ // unresolved declaration has to be seen where it still exists.
53
+ const { unknown, accepted } = readDelegationPolicy(repo);
54
+ if (unknown.length > 0) {
55
+ return (
56
+ `Delegation declares tier "${unknown.join(", ")}", which this target `
57
+ + `does not provide. Accepted: ${accepted.join(", ")}. Keel refuses `
58
+ + "rather than substituting a tier, because work would otherwise run at "
59
+ + "a capability nobody declared while reporting success."
60
+ );
61
+ }
62
+ if (!delegation) return null;
63
+ if (!fs.existsSync(path.join(repo, "keel", "guard.json"))) {
64
+ return (
65
+ "Delegation requires an active write guard, and keel/guard.json is "
66
+ + "absent. Without it every write passes through unchecked and looks "
67
+ + "identical to a write the guard allowed. Run `keel gate task-start` "
68
+ + "for the selected task, then delegate."
69
+ );
70
+ }
71
+ return null;
72
+ }
73
+
37
74
  function capabilityKey(event) {
38
75
  if (event === "startup") return "continuity.start";
39
76
  if (["resume", "compaction"].includes(event)) return "continuity.reinject";
@@ -157,6 +194,24 @@ function projectRuntime(repo, options) {
157
194
  if (event === "subagent-stop") {
158
195
  projection.returnAuthority = "report-and-evidence-only";
159
196
  }
197
+ // Delegation extends the brief Keel already publishes rather than adding a
198
+ // carrier beside the host's own agent interface. The host spawns; this is the
199
+ // one-way view of OpenSpec it is handed.
200
+ if (event === "subagent-start") {
201
+ const refusal = delegationRefusal(repo, capsule.delegation);
202
+ if (refusal) return blocked(options.target, event, refusal, warnings);
203
+ }
204
+ if (event === "subagent-start" && capsule.delegation) {
205
+ projection.delegation = {
206
+ tier: capsule.delegation.tier,
207
+ source: capsule.delegation.source,
208
+ writeBoundary: capsule.touch,
209
+ note:
210
+ "Keel carries the declared tier and does not select a model; the "
211
+ + "target resolves it. Keel cannot observe which model executed, so "
212
+ + "the tier is what is recorded and never a claim about what ran.",
213
+ };
214
+ }
160
215
 
161
216
  return {
162
217
  schemaVersion: 1,
@@ -6,6 +6,7 @@ const path = require("path");
6
6
 
7
7
  const {
8
8
  CONFIG_RELATIVE_PATH,
9
+ readDelegationPolicy,
9
10
  readStandingAuthorization,
10
11
  } = require("./config");
11
12
 
@@ -866,6 +867,24 @@ function compileTaskContract(repo, change, task) {
866
867
  if (!autonomy.some((item) => /^Pre-authorized fallback:/i.test(item))) {
867
868
  autonomy.push("Pre-authorized fallback: none");
868
869
  }
870
+ // Who runs this task, resolved exactly as the autonomy boundary above is: the
871
+ // task keeps whatever it authored, the repository declaration supplies only
872
+ // what the task left silent, and the entry names its source. A declaration
873
+ // that could overwrite an authored tier would make the capsule unreadable on
874
+ // its own — you could not tell what this task decided from what the file did.
875
+ const authoredTier = normalizeText(field(task, "Delegation")).toLowerCase();
876
+ let delegation = { tier: null, source: null };
877
+ if (authoredTier) {
878
+ delegation = { tier: authoredTier, source: "task" };
879
+ } else {
880
+ const { tier } = readDelegationPolicy(repo);
881
+ if (tier) {
882
+ delegation = {
883
+ tier,
884
+ source: CONFIG_RELATIVE_PATH.split(path.sep).join("/"),
885
+ };
886
+ }
887
+ }
869
888
  // A question is unresolved authority when it is the subject of its Covers
870
889
  // entry. Scanning the whole field also matched a resolved question named as
871
890
  // supporting detail beside the fact that closed it, and the only fix
@@ -940,7 +959,18 @@ function compileTaskContract(repo, change, task) {
940
959
  couplingMode === "required" ? candidateBoundary : [],
941
960
  designContract: coupledContract,
942
961
  },
962
+ // A helper and a delegate are different roles. A helper is never a second
963
+ // writer and this stays true whatever the repository declares; delegation
964
+ // is a separate entry beside it, never a helper with the guard removed.
943
965
  helperAuthority: "read-only-evidence-only",
966
+ // Present only when a tier actually resolved. An unconditional field would
967
+ // change the compiled capsule for every task everywhere, moving every
968
+ // recorded anchor and drifting every live change in every consumer repo on
969
+ // upgrade — for repositories that declared nothing and asked for nothing.
970
+ // Omission keeps this release's invariant: no behavior change without a
971
+ // declaration. A repository that does declare gets a different capsule,
972
+ // which is honest, because its execution genuinely differs.
973
+ ...(delegation.tier ? { delegation } : {}),
944
974
  prohibitions: [
945
975
  "must not change Acceptance",
946
976
  // repo-action is the one mode whose authorized effect is the repository