@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
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// Keel 4.1.0 bounded read-only helper contract.
|
|
4
|
+
//
|
|
5
|
+
// A helper is never a second writer. `keel project helper` compiles one
|
|
6
|
+
// bounded read-only question or one exact repository-byte-stable verification
|
|
7
|
+
// command into a `keel-helper-brief/v1` evidence contract, and verifies a
|
|
8
|
+
// helper's return only after proving the repository bytes are unchanged. The
|
|
9
|
+
// current agent stays the sole writer, owner of Acceptance, Review, gates, and
|
|
10
|
+
// completion; helper absence never disables current-agent goal execution.
|
|
11
|
+
|
|
12
|
+
const fs = require("fs");
|
|
13
|
+
const os = require("os");
|
|
14
|
+
const path = require("path");
|
|
15
|
+
const crypto = require("crypto");
|
|
16
|
+
|
|
17
|
+
const BRIEF_VERSION = "keel-helper-brief/v1";
|
|
18
|
+
const BASELINE_VERSION = "keel-helper-baseline/v1";
|
|
19
|
+
const VERIFY_VERSION = "keel-helper-verification/v1";
|
|
20
|
+
const SUPPORTED_HELPER_TARGETS = new Set(["codex", "claude"]);
|
|
21
|
+
|
|
22
|
+
// Any mutation, delegation, or ownership request disqualifies a read-only brief.
|
|
23
|
+
const FORBIDDEN_INTENT = new RegExp(
|
|
24
|
+
"\\b(implement|write|edit|modify|create|refactor|delete|remove|fix|apply|"
|
|
25
|
+
+ "generate|install|commit|push|sync|archive|mark|check off|rewrite|"
|
|
26
|
+
+ "update the task|change the acceptance|use the fallback|use a fallback)\\b",
|
|
27
|
+
"i"
|
|
28
|
+
);
|
|
29
|
+
const DELEGATION_INTENT = new RegExp(
|
|
30
|
+
"\\b(subagent|sub-agent|delegate|delegation|spawn|agent team|another agent|"
|
|
31
|
+
+ "helper)\\b",
|
|
32
|
+
"i"
|
|
33
|
+
);
|
|
34
|
+
// Shell tokens that would create or move repository artifacts.
|
|
35
|
+
const ARTIFACT_TOKENS = [
|
|
36
|
+
">", ">>", "|&", "tee ", "git commit", "git add", "git push", "git checkout",
|
|
37
|
+
"git reset", "git restore", "git rm", "git mv", "npm install", "npm i ",
|
|
38
|
+
"npm ci", "mkdir", "touch ", "rm ", "mv ", "cp ", "rmdir",
|
|
39
|
+
];
|
|
40
|
+
const REPORT_SCHEMA = [
|
|
41
|
+
"question-or-command",
|
|
42
|
+
"reads-performed",
|
|
43
|
+
"observed-evidence",
|
|
44
|
+
"byte-stability: verified|rejected|unverifiable",
|
|
45
|
+
"no-writes-no-delegation-no-completion-authority",
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
function blockedBrief(target, reason, extra = {}) {
|
|
49
|
+
return {
|
|
50
|
+
version: BRIEF_VERSION,
|
|
51
|
+
status: "blocked",
|
|
52
|
+
target,
|
|
53
|
+
brief: null,
|
|
54
|
+
reasons: [reason],
|
|
55
|
+
...extra,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function isExternal(repo, candidate) {
|
|
60
|
+
const rel = path.relative(repo, path.resolve(candidate));
|
|
61
|
+
return (
|
|
62
|
+
rel === ".."
|
|
63
|
+
|| rel.startsWith(`..${path.sep}`)
|
|
64
|
+
|| path.isAbsolute(rel)
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function compileHelperBrief(repo, options) {
|
|
69
|
+
const target = options.target;
|
|
70
|
+
if (!SUPPORTED_HELPER_TARGETS.has(target)) {
|
|
71
|
+
return blockedBrief(
|
|
72
|
+
target,
|
|
73
|
+
`Bounded read-only helpers support codex and claude only; `
|
|
74
|
+
+ `${target || "missing"} remains manual/compatibility-only.`
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const question = (options.helperQuestion || "").trim();
|
|
79
|
+
const command = (options.helperCommand || "").trim();
|
|
80
|
+
if ((question && command) || (!question && !command)) {
|
|
81
|
+
return blockedBrief(
|
|
82
|
+
target,
|
|
83
|
+
"A helper brief needs exactly one bounded read-only question or one exact "
|
|
84
|
+
+ "repository-byte-stable verification command, not both and not neither."
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const external = options.helperExternal || [];
|
|
89
|
+
for (const declared of external) {
|
|
90
|
+
if (!isExternal(repo, declared)) {
|
|
91
|
+
return blockedBrief(
|
|
92
|
+
target,
|
|
93
|
+
`Declared helper output path is inside the repository: ${declared}; `
|
|
94
|
+
+ "helper temporaries must live outside the worktree."
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (question) {
|
|
100
|
+
if (/\?[\s\S]*\?/.test(question)) {
|
|
101
|
+
return blockedBrief(
|
|
102
|
+
target,
|
|
103
|
+
"A helper brief carries one bounded question; multiple unrelated "
|
|
104
|
+
+ "questions must be split into separate read-only briefs."
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
if (DELEGATION_INTENT.test(question)) {
|
|
108
|
+
return blockedBrief(
|
|
109
|
+
target,
|
|
110
|
+
"A helper cannot delegate or spawn nested helpers; the brief must be a "
|
|
111
|
+
+ "single read-only question the helper answers itself."
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
if (FORBIDDEN_INTENT.test(question)) {
|
|
115
|
+
return blockedBrief(
|
|
116
|
+
target,
|
|
117
|
+
"A helper is read-only and owns no completion authority; it cannot be "
|
|
118
|
+
+ "asked to implement, write, mark, sync, archive, commit, push, or "
|
|
119
|
+
+ "change Acceptance."
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
} else {
|
|
123
|
+
if (DELEGATION_INTENT.test(command)) {
|
|
124
|
+
return blockedBrief(
|
|
125
|
+
target,
|
|
126
|
+
"A helper verification command cannot delegate or nest further helpers."
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
const lowered = command.toLowerCase();
|
|
130
|
+
const artifact = ARTIFACT_TOKENS.find((token) => lowered.includes(token));
|
|
131
|
+
if (artifact) {
|
|
132
|
+
return blockedBrief(
|
|
133
|
+
target,
|
|
134
|
+
`Helper command would generate or move repository artifacts (${artifact.trim()}); `
|
|
135
|
+
+ "only repository-byte-stable verification commands are allowed."
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
if (FORBIDDEN_INTENT.test(command)) {
|
|
139
|
+
return blockedBrief(
|
|
140
|
+
target,
|
|
141
|
+
"A helper verification command cannot mark, sync, archive, commit, push, "
|
|
142
|
+
+ "or otherwise assume completion authority."
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const brief = {
|
|
148
|
+
version: BRIEF_VERSION,
|
|
149
|
+
target,
|
|
150
|
+
mode: question ? "question" : "verification-command",
|
|
151
|
+
request: question || command,
|
|
152
|
+
reads: options.helperReads || [],
|
|
153
|
+
externalPaths: external,
|
|
154
|
+
authority: "read-only-evidence-only",
|
|
155
|
+
writesProducts: false,
|
|
156
|
+
delegates: false,
|
|
157
|
+
completionAuthority: false,
|
|
158
|
+
reportSchema: REPORT_SCHEMA,
|
|
159
|
+
};
|
|
160
|
+
return {
|
|
161
|
+
version: BRIEF_VERSION,
|
|
162
|
+
status: "ready",
|
|
163
|
+
target,
|
|
164
|
+
brief,
|
|
165
|
+
reasons: [],
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function snapshotRepo(repo) {
|
|
170
|
+
const snapshot = {};
|
|
171
|
+
const walk = (dir) => {
|
|
172
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
173
|
+
if (entry.name === ".git") continue;
|
|
174
|
+
const full = path.join(dir, entry.name);
|
|
175
|
+
if (entry.isDirectory()) {
|
|
176
|
+
walk(full);
|
|
177
|
+
} else if (entry.isFile()) {
|
|
178
|
+
const rel = path.relative(repo, full).replace(/\\/g, "/");
|
|
179
|
+
const bytes = fs.readFileSync(full);
|
|
180
|
+
const hash = crypto.createHash("sha256").update(bytes).digest("hex");
|
|
181
|
+
const mode = fs.statSync(full).mode & 0o777;
|
|
182
|
+
snapshot[rel] = `${hash}:${mode.toString(8)}`;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
walk(repo);
|
|
187
|
+
return snapshot;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function captureHelperBaseline(repo, options) {
|
|
191
|
+
const outPath = options.helperBaseline;
|
|
192
|
+
if (!outPath) {
|
|
193
|
+
throw new Error("helper baseline capture requires --baseline");
|
|
194
|
+
}
|
|
195
|
+
if (!isExternal(repo, outPath)) {
|
|
196
|
+
throw new Error(
|
|
197
|
+
"helper baseline must be written outside the repository worktree"
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
const snapshot = snapshotRepo(repo);
|
|
201
|
+
fs.mkdirSync(path.dirname(path.resolve(outPath)), { recursive: true });
|
|
202
|
+
fs.writeFileSync(
|
|
203
|
+
path.resolve(outPath),
|
|
204
|
+
`${JSON.stringify({ version: BASELINE_VERSION, snapshot }, null, 2)}\n`,
|
|
205
|
+
"utf8"
|
|
206
|
+
);
|
|
207
|
+
return {
|
|
208
|
+
version: BASELINE_VERSION,
|
|
209
|
+
status: "captured",
|
|
210
|
+
path: path.resolve(outPath),
|
|
211
|
+
count: Object.keys(snapshot).length,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function classifyChanges(before, after) {
|
|
216
|
+
const changes = [];
|
|
217
|
+
for (const [rel, value] of Object.entries(after)) {
|
|
218
|
+
if (!(rel in before)) {
|
|
219
|
+
changes.push({ path: rel, kind: "added" });
|
|
220
|
+
} else if (before[rel] !== after[rel]) {
|
|
221
|
+
const [beforeHash, beforeMode] = before[rel].split(":");
|
|
222
|
+
const [afterHash] = value.split(":");
|
|
223
|
+
changes.push({
|
|
224
|
+
path: rel,
|
|
225
|
+
kind: beforeHash === afterHash ? "permission-changed" : "modified",
|
|
226
|
+
});
|
|
227
|
+
void beforeMode;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
for (const rel of Object.keys(before)) {
|
|
231
|
+
if (!(rel in after)) {
|
|
232
|
+
changes.push({ path: rel, kind: "removed" });
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return changes.sort((a, b) => a.path.localeCompare(b.path));
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function verifyHelperEvidence(repo, options) {
|
|
239
|
+
const baselinePath = options.helperBaseline;
|
|
240
|
+
if (!baselinePath || !fs.existsSync(path.resolve(baselinePath))) {
|
|
241
|
+
return {
|
|
242
|
+
version: VERIFY_VERSION,
|
|
243
|
+
status: "unverifiable",
|
|
244
|
+
target: options.target,
|
|
245
|
+
changes: [],
|
|
246
|
+
cleanup: "none",
|
|
247
|
+
reasons: [
|
|
248
|
+
"Byte stability cannot be established without a recorded baseline; the "
|
|
249
|
+
+ "verification remains current-agent work.",
|
|
250
|
+
],
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
let baseline;
|
|
254
|
+
try {
|
|
255
|
+
baseline = JSON.parse(fs.readFileSync(path.resolve(baselinePath), "utf8"));
|
|
256
|
+
} catch {
|
|
257
|
+
return {
|
|
258
|
+
version: VERIFY_VERSION,
|
|
259
|
+
status: "unverifiable",
|
|
260
|
+
target: options.target,
|
|
261
|
+
changes: [],
|
|
262
|
+
cleanup: "none",
|
|
263
|
+
reasons: [
|
|
264
|
+
"Recorded helper baseline is unreadable; byte stability cannot be "
|
|
265
|
+
+ "established and the verification remains current-agent work.",
|
|
266
|
+
],
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
const before = baseline.snapshot || {};
|
|
270
|
+
const after = snapshotRepo(repo);
|
|
271
|
+
const changes = classifyChanges(before, after);
|
|
272
|
+
if (changes.length === 0) {
|
|
273
|
+
return {
|
|
274
|
+
version: VERIFY_VERSION,
|
|
275
|
+
status: "verified",
|
|
276
|
+
target: options.target,
|
|
277
|
+
changes: [],
|
|
278
|
+
cleanup: "none",
|
|
279
|
+
reasons: [],
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
return {
|
|
283
|
+
version: VERIFY_VERSION,
|
|
284
|
+
status: "rejected",
|
|
285
|
+
target: options.target,
|
|
286
|
+
changes,
|
|
287
|
+
// The helper never restores, deletes, or attributes; it reports exact paths.
|
|
288
|
+
cleanup: "none",
|
|
289
|
+
reasons: [
|
|
290
|
+
"Helper evidence rejected: repository bytes changed at "
|
|
291
|
+
+ changes.map((item) => `${item.path} (${item.kind})`).join(", ")
|
|
292
|
+
+ "; the current agent owns these paths and no cleanup is performed.",
|
|
293
|
+
],
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function renderHelper(result) {
|
|
298
|
+
const lines = [
|
|
299
|
+
`Keel helper: ${result.status}`,
|
|
300
|
+
`Version: ${result.version}`,
|
|
301
|
+
];
|
|
302
|
+
if (result.target) lines.push(`Target: ${result.target}`);
|
|
303
|
+
for (const change of result.changes || []) {
|
|
304
|
+
lines.push(`Changed: ${change.path} (${change.kind})`);
|
|
305
|
+
}
|
|
306
|
+
for (const reason of result.reasons || []) lines.push(`Reason: ${reason}`);
|
|
307
|
+
return `${lines.join("\n")}\n`;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
module.exports = {
|
|
311
|
+
BRIEF_VERSION,
|
|
312
|
+
BASELINE_VERSION,
|
|
313
|
+
VERIFY_VERSION,
|
|
314
|
+
SUPPORTED_HELPER_TARGETS,
|
|
315
|
+
compileHelperBrief,
|
|
316
|
+
captureHelperBaseline,
|
|
317
|
+
verifyHelperEvidence,
|
|
318
|
+
renderHelper,
|
|
319
|
+
};
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// Keel 4.1.0 one-way native projection contract.
|
|
4
|
+
|
|
5
|
+
const { resolveContext } = require("./context");
|
|
6
|
+
const { loadTaskContract } = require("./task-contract");
|
|
7
|
+
const { probeCapabilities } = require("./capabilities");
|
|
8
|
+
|
|
9
|
+
const EVENTS = new Set([
|
|
10
|
+
"startup",
|
|
11
|
+
"resume",
|
|
12
|
+
"compaction",
|
|
13
|
+
"goal",
|
|
14
|
+
"task-view",
|
|
15
|
+
"worktree",
|
|
16
|
+
"subagent-start",
|
|
17
|
+
"subagent-stop",
|
|
18
|
+
]);
|
|
19
|
+
|
|
20
|
+
function blocked(target, event, reason, warnings = []) {
|
|
21
|
+
return {
|
|
22
|
+
schemaVersion: 1,
|
|
23
|
+
status: "blocked",
|
|
24
|
+
target,
|
|
25
|
+
event,
|
|
26
|
+
source: null,
|
|
27
|
+
capability: {
|
|
28
|
+
level: "manual",
|
|
29
|
+
command: "keel context --json",
|
|
30
|
+
},
|
|
31
|
+
projection: null,
|
|
32
|
+
reasons: [reason],
|
|
33
|
+
warnings,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function capabilityKey(event) {
|
|
38
|
+
if (event === "startup") return "continuity.start";
|
|
39
|
+
if (["resume", "compaction"].includes(event)) return "continuity.reinject";
|
|
40
|
+
if (event === "goal") return "execution.goal";
|
|
41
|
+
if (event === "task-view") return "execution.task-view";
|
|
42
|
+
if (event === "worktree") return "execution.worktree";
|
|
43
|
+
if (event === "subagent-start") return "delegation.context";
|
|
44
|
+
return "delegation.return";
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function projectRuntime(repo, options) {
|
|
48
|
+
const event = options.projectionEvent;
|
|
49
|
+
if (!EVENTS.has(event)) {
|
|
50
|
+
throw new Error(`unsupported projection event: ${event || "<missing>"}`);
|
|
51
|
+
}
|
|
52
|
+
if (!["claude", "codex", "opencode"].includes(options.target)) {
|
|
53
|
+
throw new Error(`unsupported target: ${options.target}`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const context = resolveContext(repo, {
|
|
57
|
+
change: options.change,
|
|
58
|
+
task: options.task,
|
|
59
|
+
});
|
|
60
|
+
if (context.status !== "ready" || !context.selection) {
|
|
61
|
+
return blocked(
|
|
62
|
+
options.target,
|
|
63
|
+
event,
|
|
64
|
+
context.reasons.join(" ") || "Current OpenSpec context is not ready.",
|
|
65
|
+
context.warnings
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
const change = context.selection.change;
|
|
69
|
+
const taskId = context.selection.task;
|
|
70
|
+
if (!taskId) {
|
|
71
|
+
return blocked(
|
|
72
|
+
options.target,
|
|
73
|
+
event,
|
|
74
|
+
"Projection requires one selected executable task.",
|
|
75
|
+
context.warnings
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
const loaded = loadTaskContract(repo, change, taskId);
|
|
79
|
+
if (!loaded || loaded.task.checked) {
|
|
80
|
+
return blocked(
|
|
81
|
+
options.target,
|
|
82
|
+
event,
|
|
83
|
+
"Selected durable task owner is missing or already complete.",
|
|
84
|
+
context.warnings
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
if (loaded.contract.diagnostics.length > 0) {
|
|
88
|
+
return blocked(
|
|
89
|
+
options.target,
|
|
90
|
+
event,
|
|
91
|
+
loaded.contract.diagnostics.map((item) => item.message).join(" "),
|
|
92
|
+
context.warnings
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const owner = `openspec/changes/${change}/tasks.md#${taskId}`;
|
|
97
|
+
if (
|
|
98
|
+
event === "worktree"
|
|
99
|
+
&& (!options.expectedOwner || options.expectedOwner !== owner)
|
|
100
|
+
) {
|
|
101
|
+
return blocked(
|
|
102
|
+
options.target,
|
|
103
|
+
event,
|
|
104
|
+
`Current checkout owner ${owner} does not match the explicit expected owner.`,
|
|
105
|
+
context.warnings
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const authorization = new Set(options.authorizations || []);
|
|
110
|
+
const requiredAuthorization =
|
|
111
|
+
event === "goal"
|
|
112
|
+
? "goal"
|
|
113
|
+
: event === "task-view"
|
|
114
|
+
? "task-view"
|
|
115
|
+
: event.startsWith("subagent-")
|
|
116
|
+
? "subagent"
|
|
117
|
+
: null;
|
|
118
|
+
if (requiredAuthorization && !authorization.has(requiredAuthorization)) {
|
|
119
|
+
return blocked(
|
|
120
|
+
options.target,
|
|
121
|
+
event,
|
|
122
|
+
`${event} projection requires explicit ${requiredAuthorization} authorization.`,
|
|
123
|
+
context.warnings
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const contract = loaded.contract;
|
|
128
|
+
const capsule = contract.capsule;
|
|
129
|
+
const capabilities = probeCapabilities(repo, options.target);
|
|
130
|
+
const capability = capabilities.capabilities[capabilityKey(event)];
|
|
131
|
+
const warnings = [...context.warnings];
|
|
132
|
+
if (options.nativeComplete) {
|
|
133
|
+
warnings.push(
|
|
134
|
+
"Native completion was ignored; only task-complete plus current-agent "
|
|
135
|
+
+ "durable updates can complete OpenSpec work."
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
const projection = {
|
|
139
|
+
objective: capsule.task.title,
|
|
140
|
+
acceptance: capsule.acceptance,
|
|
141
|
+
stopBoundary: [
|
|
142
|
+
...capsule.boundaries.stop,
|
|
143
|
+
...capsule.boundaries.autonomy,
|
|
144
|
+
],
|
|
145
|
+
nextAction: context.nextAction,
|
|
146
|
+
read: capsule.read,
|
|
147
|
+
touch: capsule.touch,
|
|
148
|
+
verification: capsule.verification,
|
|
149
|
+
evidenceContract: capsule.verification.commands.map(
|
|
150
|
+
(item) => `${item.label}: ${item.check}`
|
|
151
|
+
),
|
|
152
|
+
owner: capsule.owner,
|
|
153
|
+
helperAuthority: capsule.helperAuthority,
|
|
154
|
+
fingerprint: contract.fingerprint,
|
|
155
|
+
prohibitions: capsule.prohibitions,
|
|
156
|
+
};
|
|
157
|
+
if (event === "subagent-stop") {
|
|
158
|
+
projection.returnAuthority = "report-and-evidence-only";
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
schemaVersion: 1,
|
|
163
|
+
status: "ready",
|
|
164
|
+
target: options.target,
|
|
165
|
+
event,
|
|
166
|
+
source: {
|
|
167
|
+
authority: "OpenSpec",
|
|
168
|
+
owner,
|
|
169
|
+
change,
|
|
170
|
+
task: taskId,
|
|
171
|
+
},
|
|
172
|
+
capability,
|
|
173
|
+
contract,
|
|
174
|
+
projection,
|
|
175
|
+
reasons: [],
|
|
176
|
+
warnings,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function renderProjection(result) {
|
|
181
|
+
const lines = [
|
|
182
|
+
`Keel projection: ${result.status}`,
|
|
183
|
+
`Target: ${result.target}`,
|
|
184
|
+
`Event: ${result.event}`,
|
|
185
|
+
];
|
|
186
|
+
if (result.source) lines.push(`Owner: ${result.source.owner}`);
|
|
187
|
+
for (const reason of result.reasons) lines.push(`Reason: ${reason}`);
|
|
188
|
+
for (const warning of result.warnings) lines.push(`Warning: ${warning}`);
|
|
189
|
+
return `${lines.join("\n")}\n`;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
module.exports = {
|
|
193
|
+
projectRuntime,
|
|
194
|
+
renderProjection,
|
|
195
|
+
};
|