@kisev/skills-opencode 1.1.1 → 2.0.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/README.md +134 -108
- package/README.ru.md +242 -0
- package/dist/agent-profiles.d.ts +1 -1
- package/dist/agent-profiles.js +21 -10
- package/dist/assets/agents/architect.md +67 -0
- package/{assets → dist/assets}/agents/critic.md +17 -2
- package/dist/assets/agents/manager.md +36 -0
- package/{assets → dist/assets}/agents/mapper.md +2 -2
- package/dist/assets/agents/review.md +25 -0
- package/{assets → dist/assets}/agents/worker.md +7 -5
- package/dist/assets/commands/agent-profiles.md +9 -0
- package/dist/assets/commands/agents-md.md +10 -0
- package/dist/assets/commands/askme.md +10 -0
- package/dist/assets/commands/ast-grep.md +10 -0
- package/dist/assets/commands/capabilities.md +9 -0
- package/dist/assets/commands/code-explain.md +10 -0
- package/dist/assets/commands/code-review.md +10 -0
- package/dist/assets/commands/commit-msg.md +10 -0
- package/dist/assets/commands/docs-prepare.md +10 -0
- package/dist/assets/commands/docs-review.md +10 -0
- package/dist/assets/commands/doctor.md +9 -0
- package/dist/assets/commands/doit.md +10 -0
- package/dist/assets/commands/goal.md +10 -0
- package/dist/assets/commands/humanize.md +10 -0
- package/dist/assets/commands/lsp-report.md +10 -0
- package/dist/assets/commands/mattermost.md +10 -0
- package/dist/assets/commands/mr-prepare.md +10 -0
- package/dist/assets/commands/reconcile.md +9 -0
- package/dist/assets/commands/release-prepare.md +10 -0
- package/dist/assets/commands/release-review.md +10 -0
- package/dist/assets/commands/rtk.md +10 -0
- package/dist/assets/commands/skill-improve.md +10 -0
- package/dist/assets/commands/slides-prompts-prepare.md +10 -0
- package/dist/assets/commands/spec-manage.md +10 -0
- package/dist/assets/commands/stopit.md +10 -0
- package/dist/assets/commands/summary.md +10 -0
- package/dist/assets/commands/task-prepare.md +10 -0
- package/dist/assets/commands/task-review.md +10 -0
- package/dist/assets/commands/task-triage.md +10 -0
- package/dist/assets/commands/team-retro.md +10 -0
- package/dist/assets/commands/team-roadmap.md +10 -0
- package/dist/assets/commands/team-sprint-close.md +10 -0
- package/dist/assets/commands/team-sprint-start.md +10 -0
- package/dist/assets/lsp-catalog.json +30 -0
- package/dist/assets/migration-inventory.json +294 -0
- package/dist/catalog.d.ts +8 -0
- package/dist/catalog.js +38 -0
- package/dist/cli-output.d.ts +7 -0
- package/dist/cli-output.js +68 -1
- package/dist/cli.js +256 -59
- package/dist/contracts.d.ts +52 -0
- package/dist/contracts.js +98 -0
- package/dist/doctor.d.ts +44 -0
- package/dist/doctor.js +595 -0
- package/dist/generate-assets.js +17 -2
- package/dist/index.d.ts +84 -55
- package/dist/index.js +170 -37
- package/dist/installer.d.ts +33 -5
- package/dist/installer.js +252 -28
- package/dist/lifecycle.d.ts +3 -0
- package/dist/lifecycle.js +49 -17
- package/dist/plugins/rtk.js +4 -1
- package/dist/reconcile.d.ts +43 -0
- package/dist/reconcile.js +507 -0
- package/dist/registry.d.ts +7 -4
- package/dist/registry.js +28 -88
- package/dist/routing.d.ts +97 -3
- package/dist/routing.js +470 -18
- package/dist/runtime/state.d.ts +1 -0
- package/dist/runtime/state.js +12 -5
- package/dist/runtime/worktree.d.ts +33 -0
- package/dist/runtime/worktree.js +220 -0
- package/dist/terminal-wizard.d.ts +2 -0
- package/dist/terminal-wizard.js +125 -0
- package/package.json +10 -26
- package/assets/agents/architect.md +0 -33
- package/assets/agents/manager.md +0 -82
- package/assets/agents/review.md +0 -25
- package/assets/commands/agent-list.md +0 -9
- package/assets/commands/agent-model-set.md +0 -9
- package/assets/commands/askme.md +0 -10
- package/assets/commands/ast-grep-rewrite.md +0 -10
- package/assets/commands/ast-grep-search.md +0 -10
- package/assets/commands/attempt-cancel.md +0 -10
- package/assets/commands/attempt-list.md +0 -10
- package/assets/commands/attempt-result.md +0 -10
- package/assets/commands/attempt-show.md +0 -10
- package/assets/commands/capabilities.md +0 -9
- package/assets/commands/code-review.md +0 -10
- package/assets/commands/commit-msg.md +0 -10
- package/assets/commands/critic-add.md +0 -9
- package/assets/commands/critic-remove.md +0 -9
- package/assets/commands/docs-prepare.md +0 -10
- package/assets/commands/docs-review.md +0 -10
- package/assets/commands/doctor.md +0 -9
- package/assets/commands/doit.md +0 -10
- package/assets/commands/goal-list.md +0 -10
- package/assets/commands/goal-pause.md +0 -10
- package/assets/commands/goal-prepare.md +0 -10
- package/assets/commands/goal-remove.md +0 -10
- package/assets/commands/goal-show.md +0 -10
- package/assets/commands/goal-start.md +0 -10
- package/assets/commands/lsp-report.md +0 -10
- package/assets/commands/mattermost.md +0 -10
- package/assets/commands/mr-prepare.md +0 -10
- package/assets/commands/mr-review.md +0 -10
- package/assets/commands/multi-run-cancel.md +0 -10
- package/assets/commands/multi-run-compare.md +0 -10
- package/assets/commands/multi-run-fusion.md +0 -10
- package/assets/commands/multi-run-start.md +0 -10
- package/assets/commands/multi-run-status.md +0 -10
- package/assets/commands/overview.md +0 -10
- package/assets/commands/release-prepare.md +0 -10
- package/assets/commands/release-review.md +0 -10
- package/assets/commands/route.md +0 -9
- package/assets/commands/schedule-add.md +0 -10
- package/assets/commands/schedule-disable.md +0 -10
- package/assets/commands/schedule-enable.md +0 -10
- package/assets/commands/schedule-list.md +0 -10
- package/assets/commands/schedule-remove.md +0 -10
- package/assets/commands/schedule-status.md +0 -10
- package/assets/commands/skill-improver.md +0 -10
- package/assets/commands/spec-audit.md +0 -10
- package/assets/commands/spec-init.md +0 -10
- package/assets/commands/spec-onboard.md +0 -10
- package/assets/commands/spec-update.md +0 -10
- package/assets/commands/stopit.md +0 -10
- package/assets/commands/summary.md +0 -10
- package/assets/commands/task-prepare-batch.md +0 -10
- package/assets/commands/task-prepare.md +0 -10
- package/assets/commands/task-review.md +0 -10
- package/assets/commands/task-triage.md +0 -10
- package/assets/commands/team-planning.md +0 -10
- package/assets/commands/team-retro.md +0 -10
- package/assets/commands/team-roadmap.md +0 -10
- package/assets/commands/team-slides-prompts.md +0 -10
- package/assets/commands/team-sprint-close.md +0 -10
- package/assets/commands/team-sprint-status.md +0 -10
- package/assets/commands/walkthrough.md +0 -10
- package/assets/plugins/autonomy-policy.js +0 -3
- package/assets/plugins/background-attempts.js +0 -3
- package/assets/plugins/goal-loop.js +0 -3
- package/assets/plugins/schedule.js +0 -3
- package/assets/plugins/zed-clickable-paths.js +0 -3
- package/dist/plugins/autonomy-policy.d.ts +0 -21
- package/dist/plugins/autonomy-policy.js +0 -71
- package/dist/plugins/background-attempts.d.ts +0 -76
- package/dist/plugins/background-attempts.js +0 -113
- package/dist/plugins/goal-loop.d.ts +0 -51
- package/dist/plugins/goal-loop.js +0 -87
- package/dist/plugins/schedule.d.ts +0 -36
- package/dist/plugins/schedule.js +0 -100
- package/dist/plugins/zed-clickable-paths.d.ts +0 -11
- package/dist/plugins/zed-clickable-paths.js +0 -10
- /package/{assets → dist/assets}/plugins/rtk.js +0 -0
- /package/{assets → dist/assets}/plugins/rules-injector.js +0 -0
- /package/{assets → dist/assets}/plugins/zed-bell.js +0 -0
package/dist/routing.js
CHANGED
|
@@ -1,15 +1,38 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
|
|
2
|
+
import { validateAgentReport } from "./contracts.js";
|
|
3
|
+
export const CATEGORIES = ["exploration", "architecture", "implementation", "review"];
|
|
3
4
|
const MATRIX = {
|
|
4
5
|
schema_version: 1,
|
|
5
6
|
categories: {
|
|
6
|
-
exploration: {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
7
|
+
exploration: {
|
|
8
|
+
profiles: ["mapper", "architect"],
|
|
9
|
+
capabilities: ["read", "search"],
|
|
10
|
+
tools: ["read", "glob", "grep"],
|
|
11
|
+
cost: "low",
|
|
12
|
+
latency: "fast",
|
|
13
|
+
},
|
|
14
|
+
architecture: {
|
|
15
|
+
profiles: ["architect", "mapper"],
|
|
16
|
+
capabilities: ["read", "architecture"],
|
|
17
|
+
tools: ["read", "glob", "grep"],
|
|
18
|
+
cost: "medium",
|
|
19
|
+
latency: "standard",
|
|
20
|
+
},
|
|
21
|
+
implementation: {
|
|
22
|
+
profiles: ["worker"],
|
|
23
|
+
capabilities: ["read", "write", "verify"],
|
|
24
|
+
tools: ["read", "edit", "bash"],
|
|
25
|
+
cost: "medium",
|
|
26
|
+
latency: "standard",
|
|
27
|
+
},
|
|
28
|
+
review: {
|
|
29
|
+
profiles: ["review", "critic"],
|
|
30
|
+
capabilities: ["read", "review"],
|
|
31
|
+
tools: ["read", "glob", "grep"],
|
|
32
|
+
cost: "medium",
|
|
33
|
+
latency: "standard",
|
|
34
|
+
},
|
|
35
|
+
},
|
|
13
36
|
};
|
|
14
37
|
function stable(value) {
|
|
15
38
|
if (value === null || typeof value !== "object")
|
|
@@ -17,11 +40,66 @@ function stable(value) {
|
|
|
17
40
|
if (Array.isArray(value))
|
|
18
41
|
return `[${value.map(stable).join(",")}]`;
|
|
19
42
|
const object = value;
|
|
20
|
-
return `{${Object.keys(object)
|
|
43
|
+
return `{${Object.keys(object)
|
|
44
|
+
.sort()
|
|
45
|
+
.map((key) => `${JSON.stringify(key)}:${stable(object[key])}`)
|
|
46
|
+
.join(",")}}`;
|
|
21
47
|
}
|
|
22
48
|
function digest(value) {
|
|
23
49
|
return createHash("sha256").update(stable(value), "utf8").digest("hex");
|
|
24
50
|
}
|
|
51
|
+
export function validateRoutingReceipt(value, context) {
|
|
52
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
53
|
+
throw new Error("routing receipt is not an object");
|
|
54
|
+
const decision = value;
|
|
55
|
+
if ("destination" in decision) {
|
|
56
|
+
const receipt = value;
|
|
57
|
+
if (receipt.schema_version !== 1 ||
|
|
58
|
+
!CATEGORIES.includes(receipt.destination) ||
|
|
59
|
+
!receipt.agent ||
|
|
60
|
+
!receipt.nonce ||
|
|
61
|
+
!/^[a-f0-9]{64}$/.test(receipt.host_inventory_revision) ||
|
|
62
|
+
!/^[a-f0-9]{64}$/.test(receipt.receipt_digest) ||
|
|
63
|
+
!Number.isFinite(Date.parse(receipt.expires_at)) ||
|
|
64
|
+
(receipt.card_revision !== null &&
|
|
65
|
+
(!Number.isInteger(receipt.card_revision) || receipt.card_revision < 1)))
|
|
66
|
+
throw new Error("routing receipt is malformed");
|
|
67
|
+
if (Date.parse(receipt.expires_at) < Date.now())
|
|
68
|
+
throw new Error("routing receipt has expired");
|
|
69
|
+
const { receipt_digest: _receiptDigest, ...receiptBase } = receipt;
|
|
70
|
+
if (digest(receiptBase) !== receipt.receipt_digest)
|
|
71
|
+
throw new Error("routing receipt integrity is invalid");
|
|
72
|
+
if (context?.category !== undefined && receipt.destination !== context.category)
|
|
73
|
+
throw new Error("routing receipt destination does not match");
|
|
74
|
+
if (context?.task !== undefined && receipt.task_digest !== digest(context.task))
|
|
75
|
+
throw new Error("routing receipt task does not match");
|
|
76
|
+
if (context?.requirements !== undefined &&
|
|
77
|
+
receipt.requirements_digest !== digest(context.requirements))
|
|
78
|
+
throw new Error("routing receipt requirements do not match");
|
|
79
|
+
if (context?.card !== undefined && receipt.card_digest !== digest(context.card))
|
|
80
|
+
throw new Error("routing receipt card does not match");
|
|
81
|
+
return receipt;
|
|
82
|
+
}
|
|
83
|
+
if (decision.schema_version !== 1 ||
|
|
84
|
+
decision.status !== "selected" ||
|
|
85
|
+
!decision.agent ||
|
|
86
|
+
!CATEGORIES.includes(decision.category))
|
|
87
|
+
throw new Error("routing receipt is not selected");
|
|
88
|
+
if (!decision.matrix_revision ||
|
|
89
|
+
!decision.decision_digest ||
|
|
90
|
+
!/^[a-f0-9]{64}$/.test(decision.decision_digest))
|
|
91
|
+
throw new Error("routing receipt is incomplete");
|
|
92
|
+
const { decision_digest: _digest, ...base } = decision;
|
|
93
|
+
if (digest(base) !== decision.decision_digest)
|
|
94
|
+
throw new Error("routing receipt digest is forged or stale");
|
|
95
|
+
if (decision.matrix_revision !== digest(MATRIX))
|
|
96
|
+
throw new Error("routing receipt matrix is stale");
|
|
97
|
+
if (context?.category !== undefined && decision.category !== context.category)
|
|
98
|
+
throw new Error("routing receipt category does not match");
|
|
99
|
+
if (context?.task !== undefined && decision.task_digest !== digest(context.task))
|
|
100
|
+
throw new Error("routing receipt task does not match");
|
|
101
|
+
return decision;
|
|
102
|
+
}
|
|
25
103
|
function missing(required, actual) {
|
|
26
104
|
const values = new Set(actual ?? []);
|
|
27
105
|
return required.some((item) => !values.has(item));
|
|
@@ -32,10 +110,20 @@ function exceeds(value, limit, order) {
|
|
|
32
110
|
export function resolveRouting(input) {
|
|
33
111
|
const category = MATRIX.categories[input.category];
|
|
34
112
|
const revision = digest(MATRIX);
|
|
113
|
+
const hostInventoryRevision = input.inventory_revision ?? digest(input.agents);
|
|
35
114
|
const inventory = new Map(input.agents.map((agent) => [agent.agent, agent]));
|
|
36
115
|
const alternatives = [];
|
|
37
116
|
const eligible = [];
|
|
38
|
-
|
|
117
|
+
const dynamicCritics = input.category === "review"
|
|
118
|
+
? input.agents
|
|
119
|
+
.map(({ agent }) => agent)
|
|
120
|
+
.filter((agent) => /^critic-[a-z0-9]+(?:-[a-z0-9]+)*$/.test(agent))
|
|
121
|
+
.sort()
|
|
122
|
+
: [];
|
|
123
|
+
const profiles = input.trusted_override && input.override && !category.profiles.includes(input.override)
|
|
124
|
+
? [...category.profiles, ...dynamicCritics, input.override]
|
|
125
|
+
: [...category.profiles, ...dynamicCritics];
|
|
126
|
+
for (const profile of profiles) {
|
|
39
127
|
const agent = inventory.get(profile);
|
|
40
128
|
const reasons = [];
|
|
41
129
|
if (!agent)
|
|
@@ -57,34 +145,398 @@ export function resolveRouting(input) {
|
|
|
57
145
|
else if (agent)
|
|
58
146
|
eligible.push(agent);
|
|
59
147
|
}
|
|
60
|
-
|
|
148
|
+
if (input.override &&
|
|
149
|
+
!input.trusted_override &&
|
|
150
|
+
!category.profiles.includes(input.override))
|
|
151
|
+
throw new Error("Unknown or user-owned profile requires a trusted override");
|
|
152
|
+
const selected = input.override
|
|
153
|
+
? eligible.find((agent) => agent.agent === input.override)
|
|
154
|
+
: eligible[0];
|
|
155
|
+
const card = input.execution_card === undefined ? undefined : validateExecutionCard(input.execution_card);
|
|
156
|
+
if (card && !card.valid)
|
|
157
|
+
throw new Error(`Invalid execution card: ${card.failedField}`);
|
|
61
158
|
const base = selected
|
|
62
|
-
? {
|
|
63
|
-
|
|
159
|
+
? {
|
|
160
|
+
schema_version: 1,
|
|
161
|
+
status: "selected",
|
|
162
|
+
category: input.category,
|
|
163
|
+
agent: selected.agent,
|
|
164
|
+
reason_codes: [
|
|
165
|
+
input.override
|
|
166
|
+
? "explicit_override"
|
|
167
|
+
: selected.agent === category.profiles[0]
|
|
168
|
+
? "primary_available"
|
|
169
|
+
: "fallback_selected",
|
|
170
|
+
"capabilities_match",
|
|
171
|
+
],
|
|
172
|
+
alternatives,
|
|
173
|
+
matrix_revision: revision,
|
|
174
|
+
host_inventory_revision: hostInventoryRevision,
|
|
175
|
+
task_digest: digest(input.task ?? ""),
|
|
176
|
+
requirements_digest: digest(input.requirements),
|
|
177
|
+
...(card?.valid
|
|
178
|
+
? {
|
|
179
|
+
execution_card_digest: digest(card.card),
|
|
180
|
+
execution_card_revision: card.card.revision,
|
|
181
|
+
}
|
|
182
|
+
: {}),
|
|
183
|
+
}
|
|
184
|
+
: {
|
|
185
|
+
schema_version: 1,
|
|
186
|
+
status: "escalate",
|
|
187
|
+
category: input.category,
|
|
188
|
+
reason_codes: [input.override ? "override_unavailable" : "no_eligible_profile"],
|
|
189
|
+
alternatives,
|
|
190
|
+
matrix_revision: revision,
|
|
191
|
+
host_inventory_revision: hostInventoryRevision,
|
|
192
|
+
task_digest: digest(input.task ?? ""),
|
|
193
|
+
requirements_digest: digest(input.requirements),
|
|
194
|
+
...(card?.valid
|
|
195
|
+
? {
|
|
196
|
+
execution_card_digest: digest(card.card),
|
|
197
|
+
execution_card_revision: card.card.revision,
|
|
198
|
+
}
|
|
199
|
+
: {}),
|
|
200
|
+
};
|
|
64
201
|
return { ...base, decision_digest: digest(base) };
|
|
65
202
|
}
|
|
203
|
+
function isNonEmptyStringArray(value) {
|
|
204
|
+
return (Array.isArray(value) &&
|
|
205
|
+
value.length > 0 &&
|
|
206
|
+
value.every((item) => typeof item === "string" && item.length > 0));
|
|
207
|
+
}
|
|
208
|
+
export function validateExecutionCard(card) {
|
|
209
|
+
if (!card || typeof card !== "object" || Array.isArray(card)) {
|
|
210
|
+
return { valid: false, failedField: "root" };
|
|
211
|
+
}
|
|
212
|
+
const c = card;
|
|
213
|
+
const expectedFields = [
|
|
214
|
+
"schema_version",
|
|
215
|
+
"status",
|
|
216
|
+
"card_id",
|
|
217
|
+
"revision",
|
|
218
|
+
"objective",
|
|
219
|
+
"evidence",
|
|
220
|
+
"changed_behavior",
|
|
221
|
+
"risks",
|
|
222
|
+
"write_set",
|
|
223
|
+
"control_markers",
|
|
224
|
+
"decisions",
|
|
225
|
+
"steps",
|
|
226
|
+
"acceptance_criteria",
|
|
227
|
+
"checks",
|
|
228
|
+
"operations",
|
|
229
|
+
"confirmations",
|
|
230
|
+
"boundaries",
|
|
231
|
+
];
|
|
232
|
+
if (Object.keys(c).sort().join(",") !== expectedFields.slice().sort().join(","))
|
|
233
|
+
return { valid: false, failedField: "schema" };
|
|
234
|
+
if (c.schema_version !== 1)
|
|
235
|
+
return { valid: false, failedField: "schema_version" };
|
|
236
|
+
if (c.status !== "READY")
|
|
237
|
+
return { valid: false, failedField: "status" };
|
|
238
|
+
if (typeof c.card_id !== "string" || !c.card_id)
|
|
239
|
+
return { valid: false, failedField: "card_id" };
|
|
240
|
+
if (typeof c.revision !== "number" || !Number.isInteger(c.revision) || c.revision < 1)
|
|
241
|
+
return { valid: false, failedField: "revision" };
|
|
242
|
+
if (typeof c.objective !== "string" || !c.objective)
|
|
243
|
+
return { valid: false, failedField: "objective" };
|
|
244
|
+
if (!isNonEmptyStringArray(c.evidence))
|
|
245
|
+
return { valid: false, failedField: "evidence" };
|
|
246
|
+
if (!isNonEmptyStringArray(c.changed_behavior))
|
|
247
|
+
return { valid: false, failedField: "changed_behavior" };
|
|
248
|
+
if (!isNonEmptyStringArray(c.risks))
|
|
249
|
+
return { valid: false, failedField: "risks" };
|
|
250
|
+
if (!isNonEmptyStringArray(c.write_set))
|
|
251
|
+
return { valid: false, failedField: "write_set" };
|
|
252
|
+
if (new Set(c.write_set).size !== c.write_set.length)
|
|
253
|
+
return { valid: false, failedField: "write_set" };
|
|
254
|
+
if (!Array.isArray(c.control_markers) || c.control_markers.length === 0)
|
|
255
|
+
return { valid: false, failedField: "control_markers" };
|
|
256
|
+
for (const marker of c.control_markers) {
|
|
257
|
+
if (!marker || typeof marker !== "object" || Array.isArray(marker))
|
|
258
|
+
return { valid: false, failedField: "control_markers" };
|
|
259
|
+
const m = marker;
|
|
260
|
+
if (typeof m.path !== "string" || !m.path)
|
|
261
|
+
return { valid: false, failedField: "control_markers" };
|
|
262
|
+
const hasExpected = typeof m.expected === "string" && m.expected.length > 0;
|
|
263
|
+
const hasExpectedAbsent = m.expected_absent === true;
|
|
264
|
+
if (hasExpected === hasExpectedAbsent)
|
|
265
|
+
return { valid: false, failedField: "control_markers" };
|
|
266
|
+
if (Object.keys(m).some((k) => k !== "path" && k !== "expected" && k !== "expected_absent"))
|
|
267
|
+
return { valid: false, failedField: "control_markers" };
|
|
268
|
+
}
|
|
269
|
+
if (!isNonEmptyStringArray(c.decisions))
|
|
270
|
+
return { valid: false, failedField: "decisions" };
|
|
271
|
+
if (!Array.isArray(c.steps) || c.steps.length === 0)
|
|
272
|
+
return { valid: false, failedField: "steps" };
|
|
273
|
+
for (const step of c.steps) {
|
|
274
|
+
if (!step || typeof step !== "object" || Array.isArray(step))
|
|
275
|
+
return { valid: false, failedField: "steps" };
|
|
276
|
+
const s = step;
|
|
277
|
+
if (typeof s.path !== "string" || !s.path)
|
|
278
|
+
return { valid: false, failedField: "steps" };
|
|
279
|
+
if (typeof s.operation !== "string" || !s.operation)
|
|
280
|
+
return { valid: false, failedField: "steps" };
|
|
281
|
+
if (Object.keys(s).some((k) => k !== "path" && k !== "operation"))
|
|
282
|
+
return { valid: false, failedField: "steps" };
|
|
283
|
+
}
|
|
284
|
+
if (!isNonEmptyStringArray(c.acceptance_criteria))
|
|
285
|
+
return { valid: false, failedField: "acceptance_criteria" };
|
|
286
|
+
if (!isNonEmptyStringArray(c.checks))
|
|
287
|
+
return { valid: false, failedField: "checks" };
|
|
288
|
+
if (!Array.isArray(c.operations) || c.operations.length === 0)
|
|
289
|
+
return { valid: false, failedField: "operations" };
|
|
290
|
+
const operationKinds = new Set([
|
|
291
|
+
"write",
|
|
292
|
+
"check",
|
|
293
|
+
"commit",
|
|
294
|
+
"rebase",
|
|
295
|
+
"push",
|
|
296
|
+
"merge",
|
|
297
|
+
"tag",
|
|
298
|
+
"release",
|
|
299
|
+
]);
|
|
300
|
+
for (const operation of c.operations) {
|
|
301
|
+
if (!operation || typeof operation !== "object" || Array.isArray(operation))
|
|
302
|
+
return { valid: false, failedField: "operations" };
|
|
303
|
+
const item = operation;
|
|
304
|
+
if (typeof item.kind !== "string" ||
|
|
305
|
+
!operationKinds.has(item.kind) ||
|
|
306
|
+
typeof item.command !== "string" ||
|
|
307
|
+
!item.command)
|
|
308
|
+
return { valid: false, failedField: "operations" };
|
|
309
|
+
if (item.path !== undefined && (typeof item.path !== "string" || !item.path))
|
|
310
|
+
return { valid: false, failedField: "operations" };
|
|
311
|
+
if (item.confirmation_ref !== undefined &&
|
|
312
|
+
(typeof item.confirmation_ref !== "string" || !item.confirmation_ref))
|
|
313
|
+
return { valid: false, failedField: "operations" };
|
|
314
|
+
if (Object.keys(item).some((key) => !["kind", "command", "path", "confirmation_ref"].includes(key)))
|
|
315
|
+
return { valid: false, failedField: "operations" };
|
|
316
|
+
if ((item.kind === "write" || item.kind === "check") && typeof item.path !== "string")
|
|
317
|
+
return { valid: false, failedField: "operations" };
|
|
318
|
+
}
|
|
319
|
+
const operationSet = new Set(c.operations.map(({ kind }) => kind));
|
|
320
|
+
for (const required of ["commit", "rebase", "push", "merge", "tag", "release"])
|
|
321
|
+
if (!operationSet.has(required))
|
|
322
|
+
return { valid: false, failedField: "operations" };
|
|
323
|
+
if (!c.confirmations || typeof c.confirmations !== "object" || Array.isArray(c.confirmations))
|
|
324
|
+
return { valid: false, failedField: "confirmations" };
|
|
325
|
+
const confirmations = c.confirmations;
|
|
326
|
+
if (typeof confirmations.execution !== "string" || !confirmations.execution)
|
|
327
|
+
return { valid: false, failedField: "confirmations" };
|
|
328
|
+
if (operationSet.has("push") ||
|
|
329
|
+
operationSet.has("merge") ||
|
|
330
|
+
operationSet.has("tag") ||
|
|
331
|
+
operationSet.has("release"))
|
|
332
|
+
if (typeof confirmations.publication !== "string" || !confirmations.publication)
|
|
333
|
+
return { valid: false, failedField: "confirmations" };
|
|
334
|
+
if (operationSet.has("rebase"))
|
|
335
|
+
if (typeof confirmations.history_rewrite !== "string" || !confirmations.history_rewrite)
|
|
336
|
+
return { valid: false, failedField: "confirmations" };
|
|
337
|
+
if (Object.keys(confirmations).some((key) => !["execution", "publication", "history_rewrite"].includes(key)))
|
|
338
|
+
return { valid: false, failedField: "confirmations" };
|
|
339
|
+
if (!c.boundaries || typeof c.boundaries !== "object" || Array.isArray(c.boundaries))
|
|
340
|
+
return { valid: false, failedField: "boundaries" };
|
|
341
|
+
const b = c.boundaries;
|
|
342
|
+
if (!isNonEmptyStringArray(b.forbidden_paths) || typeof b.scope !== "string" || !b.scope)
|
|
343
|
+
return { valid: false, failedField: "boundaries" };
|
|
344
|
+
if (Object.keys(b).some((k) => k !== "forbidden_paths" && k !== "scope"))
|
|
345
|
+
return { valid: false, failedField: "boundaries" };
|
|
346
|
+
const writeSet = c.write_set;
|
|
347
|
+
const forbiddenPaths = b.forbidden_paths;
|
|
348
|
+
const safePath = (path) => !path.startsWith("/") &&
|
|
349
|
+
!path.includes("\\") &&
|
|
350
|
+
!path.split("/").some((part) => !part || part === "." || part === "..");
|
|
351
|
+
if (![...writeSet, ...forbiddenPaths].every(safePath))
|
|
352
|
+
return { valid: false, failedField: "boundaries" };
|
|
353
|
+
const stepPaths = c.steps.map((s) => s.path);
|
|
354
|
+
const markerPaths = c.control_markers.map((m) => m.path);
|
|
355
|
+
const referenced = new Set([...stepPaths, ...markerPaths]);
|
|
356
|
+
if (![...referenced].every((p) => writeSet.includes(p)))
|
|
357
|
+
return { valid: false, failedField: "steps" };
|
|
358
|
+
const overlaps = (left, right) => left === right || left.startsWith(`${right}/`) || right.startsWith(`${left}/`);
|
|
359
|
+
if (writeSet.some((p) => forbiddenPaths.some((forbidden) => overlaps(p, forbidden))))
|
|
360
|
+
return { valid: false, failedField: "boundaries" };
|
|
361
|
+
if (!writeSet.every((path) => stepPaths.includes(path)))
|
|
362
|
+
return { valid: false, failedField: "steps" };
|
|
363
|
+
const operations = c.operations;
|
|
364
|
+
if (!writeSet.every((path) => operations.some((operation) => operation.kind === "write" && operation.path === path) &&
|
|
365
|
+
operations.some((operation) => operation.kind === "check" && operation.path === path)))
|
|
366
|
+
return { valid: false, failedField: "operations" };
|
|
367
|
+
for (const operation of operations) {
|
|
368
|
+
if (["commit", "rebase", "push", "merge", "tag", "release"].includes(operation.kind) &&
|
|
369
|
+
!operation.confirmation_ref)
|
|
370
|
+
return { valid: false, failedField: "operations" };
|
|
371
|
+
if (operation.kind === "rebase" && operation.confirmation_ref !== confirmations.history_rewrite)
|
|
372
|
+
return { valid: false, failedField: "confirmations" };
|
|
373
|
+
if (["push", "merge", "tag", "release"].includes(operation.kind) &&
|
|
374
|
+
operation.confirmation_ref !== confirmations.publication)
|
|
375
|
+
return { valid: false, failedField: "confirmations" };
|
|
376
|
+
}
|
|
377
|
+
return { valid: true, card: c };
|
|
378
|
+
}
|
|
379
|
+
export class ExecutionCardLifecycle {
|
|
380
|
+
#status = "READY";
|
|
381
|
+
#cardID;
|
|
382
|
+
#revision;
|
|
383
|
+
constructor(card) {
|
|
384
|
+
const result = validateExecutionCard(card);
|
|
385
|
+
if (!result.valid)
|
|
386
|
+
throw new Error(`Invalid execution card: ${result.failedField}`);
|
|
387
|
+
this.#cardID = result.card.card_id;
|
|
388
|
+
this.#revision = result.card.revision;
|
|
389
|
+
}
|
|
390
|
+
get status() {
|
|
391
|
+
return this.#status;
|
|
392
|
+
}
|
|
393
|
+
get card_id() {
|
|
394
|
+
return this.#cardID;
|
|
395
|
+
}
|
|
396
|
+
get revision() {
|
|
397
|
+
return this.#revision;
|
|
398
|
+
}
|
|
399
|
+
transition(status, identity) {
|
|
400
|
+
if (identity.card_id !== this.#cardID || identity.revision !== this.#revision)
|
|
401
|
+
throw new Error("Execution card identity is stale");
|
|
402
|
+
const allowed = {
|
|
403
|
+
READY: ["RUNNING", "CANCELLED"],
|
|
404
|
+
RUNNING: ["COMPLETED", "BLOCKED", "FAILED", "REJECTED_PLAN", "CANCELLED"],
|
|
405
|
+
COMPLETED: ["APPROVED", "CHANGES_REQUIRED"],
|
|
406
|
+
BLOCKED: [],
|
|
407
|
+
FAILED: [],
|
|
408
|
+
REJECTED_PLAN: [],
|
|
409
|
+
APPROVED: [],
|
|
410
|
+
CHANGES_REQUIRED: [],
|
|
411
|
+
CANCELLED: [],
|
|
412
|
+
};
|
|
413
|
+
if (!allowed[this.#status].includes(status))
|
|
414
|
+
throw new Error(`Invalid execution-card transition: ${this.#status} -> ${status}`);
|
|
415
|
+
this.#status = status;
|
|
416
|
+
return this.#status;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
66
419
|
export class RoutingGate {
|
|
67
420
|
#receipts = new Map();
|
|
421
|
+
#active = new Map();
|
|
422
|
+
#ttlMs = 10 * 60 * 1000;
|
|
68
423
|
preview(input) {
|
|
69
424
|
return resolveRouting(input);
|
|
70
425
|
}
|
|
71
426
|
dispatch(input, provided) {
|
|
72
427
|
const decision = resolveRouting(input);
|
|
73
|
-
if (!provided || typeof provided !== "object" || provided
|
|
428
|
+
if (!provided || typeof provided !== "object" || stable(provided) !== stable(decision))
|
|
74
429
|
throw new Error("Routing decision is stale; request a new preview");
|
|
75
430
|
if (decision.status !== "selected" || !decision.agent)
|
|
76
431
|
throw new Error("Routing escalated; no agent was dispatched");
|
|
77
432
|
return decision;
|
|
78
433
|
}
|
|
79
|
-
grant(sessionID, decision) {
|
|
80
|
-
|
|
434
|
+
grant(sessionID, decision, context) {
|
|
435
|
+
const now = Date.now();
|
|
436
|
+
const taskDigest = context?.task === undefined ? decision.task_digest : digest(context.task);
|
|
437
|
+
const requirementsDigest = context?.requirements === undefined
|
|
438
|
+
? decision.requirements_digest
|
|
439
|
+
: digest(context.requirements);
|
|
440
|
+
const cardDigest = context?.card === undefined ? decision.execution_card_digest : digest(context.card);
|
|
441
|
+
const receiptBase = {
|
|
442
|
+
schema_version: 1,
|
|
443
|
+
task_digest: taskDigest,
|
|
444
|
+
requirements_digest: requirementsDigest,
|
|
445
|
+
destination: decision.category,
|
|
446
|
+
agent: decision.agent,
|
|
447
|
+
card_digest: cardDigest ?? null,
|
|
448
|
+
card_revision: decision.execution_card_revision ?? null,
|
|
449
|
+
host_inventory_revision: decision.host_inventory_revision,
|
|
450
|
+
expires_at: new Date(now + this.#ttlMs).toISOString(),
|
|
451
|
+
nonce: createHash("sha256")
|
|
452
|
+
.update(`${sessionID}:${now}:${decision.decision_digest}`)
|
|
453
|
+
.digest("hex"),
|
|
454
|
+
};
|
|
455
|
+
const receipt = { ...receiptBase, receipt_digest: digest(receiptBase) };
|
|
456
|
+
this.#receipts.set(sessionID, {
|
|
457
|
+
decision,
|
|
458
|
+
taskDigest,
|
|
459
|
+
requirementsDigest,
|
|
460
|
+
cardDigest,
|
|
461
|
+
expiresAt: Date.parse(receipt.expires_at),
|
|
462
|
+
});
|
|
463
|
+
return receipt;
|
|
81
464
|
}
|
|
82
|
-
|
|
465
|
+
cancel(sessionID) {
|
|
466
|
+
this.#receipts.delete(sessionID);
|
|
467
|
+
this.#active.delete(sessionID);
|
|
468
|
+
}
|
|
469
|
+
consume(sessionID, agent, context) {
|
|
83
470
|
const receipt = this.#receipts.get(sessionID);
|
|
84
471
|
if (!receipt)
|
|
85
472
|
throw new Error("Native Task requires an active routing receipt; use the route tool");
|
|
86
|
-
if (receipt.agent !== agent)
|
|
473
|
+
if (receipt.decision.agent !== agent)
|
|
87
474
|
throw new Error("Native Task agent does not match the active routing receipt");
|
|
475
|
+
if (Date.now() > receipt.expiresAt)
|
|
476
|
+
throw new Error("Routing receipt has expired; request a new preview");
|
|
477
|
+
if (context) {
|
|
478
|
+
const taskDigest = digest(context.task ?? "");
|
|
479
|
+
if (taskDigest !== receipt.taskDigest)
|
|
480
|
+
throw new Error("Routing receipt task does not match the active routing receipt");
|
|
481
|
+
const requirementsDigest = digest(context.requirements ?? []);
|
|
482
|
+
if (requirementsDigest !== receipt.requirementsDigest)
|
|
483
|
+
throw new Error("Routing receipt requirements do not match the active routing receipt");
|
|
484
|
+
const cardDigest = context.card !== undefined ? digest(context.card) : undefined;
|
|
485
|
+
if (cardDigest !== receipt.cardDigest)
|
|
486
|
+
throw new Error("Routing receipt execution card does not match the active routing receipt");
|
|
487
|
+
}
|
|
88
488
|
this.#receipts.delete(sessionID);
|
|
489
|
+
const validatedCard = context?.card === undefined ? undefined : validateExecutionCard(context.card);
|
|
490
|
+
const card = validatedCard?.valid ? validatedCard.card : undefined;
|
|
491
|
+
const lifecycle = card ? new ExecutionCardLifecycle(card) : undefined;
|
|
492
|
+
if (lifecycle)
|
|
493
|
+
lifecycle.transition("RUNNING", { card_id: card.card_id, revision: card.revision });
|
|
494
|
+
this.#active.set(sessionID, { agent, card, lifecycle, expiresAt: receipt.expiresAt });
|
|
495
|
+
}
|
|
496
|
+
complete(sessionID, agent, output) {
|
|
497
|
+
const active = this.#active.get(sessionID);
|
|
498
|
+
if (!active || active.agent !== agent)
|
|
499
|
+
throw new Error("Task result has no matching active routing receipt");
|
|
500
|
+
if (Date.now() > active.expiresAt)
|
|
501
|
+
throw new Error("Task result routing receipt has expired");
|
|
502
|
+
const report = output && typeof output === "object" ? output : undefined;
|
|
503
|
+
const value = report?.[agent === "architect" ? "execution_card" : `${agent}_report`] ?? report?.report;
|
|
504
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
505
|
+
throw new Error("Task result is missing a structured agent report");
|
|
506
|
+
const result = value;
|
|
507
|
+
validateAgentReport(agent, report, active.card);
|
|
508
|
+
if (active.card &&
|
|
509
|
+
(result.card_id !== active.card.card_id || result.revision !== active.card.revision))
|
|
510
|
+
throw new Error("Task result does not match the active execution card");
|
|
511
|
+
const status = result.status;
|
|
512
|
+
const allowed = agent === "worker"
|
|
513
|
+
? ["COMPLETED", "BLOCKED", "FAILED", "REJECTED_PLAN"]
|
|
514
|
+
: agent === "critic"
|
|
515
|
+
? ["APPROVED", "CHANGES_REQUIRED"]
|
|
516
|
+
: undefined;
|
|
517
|
+
if (allowed && (typeof status !== "string" || !allowed.includes(status)))
|
|
518
|
+
throw new Error("Task result has an invalid machine transition");
|
|
519
|
+
if (active.lifecycle && status === "COMPLETED")
|
|
520
|
+
active.lifecycle.transition("COMPLETED", {
|
|
521
|
+
card_id: active.card.card_id,
|
|
522
|
+
revision: active.card.revision,
|
|
523
|
+
});
|
|
524
|
+
if (active.lifecycle &&
|
|
525
|
+
(status === "BLOCKED" || status === "FAILED" || status === "REJECTED_PLAN"))
|
|
526
|
+
active.lifecycle.transition(status, {
|
|
527
|
+
card_id: active.card.card_id,
|
|
528
|
+
revision: active.card.revision,
|
|
529
|
+
});
|
|
530
|
+
if (active.lifecycle && (status === "APPROVED" || status === "CHANGES_REQUIRED")) {
|
|
531
|
+
active.lifecycle.transition("COMPLETED", {
|
|
532
|
+
card_id: active.card.card_id,
|
|
533
|
+
revision: active.card.revision,
|
|
534
|
+
});
|
|
535
|
+
active.lifecycle.transition(status, {
|
|
536
|
+
card_id: active.card.card_id,
|
|
537
|
+
revision: active.card.revision,
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
this.#active.delete(sessionID);
|
|
89
541
|
}
|
|
90
542
|
}
|
package/dist/runtime/state.d.ts
CHANGED
|
@@ -4,4 +4,5 @@ export declare function readState<T>(path: string, boundary: string): Promise<T
|
|
|
4
4
|
export declare function listState(boundary: string, suffix?: string): Promise<string[]>;
|
|
5
5
|
export declare function writeState(path: string, boundary: string, value: unknown): Promise<void>;
|
|
6
6
|
export declare function appendState(path: string, boundary: string, value: unknown): Promise<void>;
|
|
7
|
+
export declare function withStateLock<T>(boundary: string, callback: () => Promise<T>): Promise<T>;
|
|
7
8
|
export declare function digest(value: unknown): string;
|
package/dist/runtime/state.js
CHANGED
|
@@ -2,7 +2,7 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import { lstat, mkdir, readFile, readdir } from "node:fs/promises";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
5
|
-
import { appendPrivate, writeAtomic } from "../lifecycle.js";
|
|
5
|
+
import { appendPrivate, withLifecycleLock, writeAtomic } from "../lifecycle.js";
|
|
6
6
|
function home(value = process.env.HOME ?? homedir()) {
|
|
7
7
|
if (!isAbsolute(value) || value.split(sep).includes(".."))
|
|
8
8
|
throw new Error("HOME must be an absolute safe path");
|
|
@@ -94,14 +94,21 @@ export async function listState(boundary, suffix = ".json") {
|
|
|
94
94
|
export async function writeState(path, boundary, value) {
|
|
95
95
|
if (!inside(boundary, path))
|
|
96
96
|
throw new Error("state path escapes its boundary");
|
|
97
|
-
await
|
|
98
|
-
|
|
97
|
+
await withLifecycleLock(boundary, async () => {
|
|
98
|
+
await safeDirectory(dirname(path), boundary, true);
|
|
99
|
+
await writeAtomic(path, Buffer.from(`${JSON.stringify(value)}\n`), 0o600);
|
|
100
|
+
});
|
|
99
101
|
}
|
|
100
102
|
export async function appendState(path, boundary, value) {
|
|
101
103
|
if (!inside(boundary, path))
|
|
102
104
|
throw new Error("state path escapes its boundary");
|
|
103
|
-
await
|
|
104
|
-
|
|
105
|
+
await withLifecycleLock(boundary, async () => {
|
|
106
|
+
await safeDirectory(dirname(path), boundary, true);
|
|
107
|
+
await appendPrivate(path, value);
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
export async function withStateLock(boundary, callback) {
|
|
111
|
+
return withLifecycleLock(boundary, callback);
|
|
105
112
|
}
|
|
106
113
|
export function digest(value) {
|
|
107
114
|
return createHash("sha256").update(JSON.stringify(value), "utf8").digest("hex");
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export type WorktreeStatus = "managed" | "missing" | "blocked" | "unknown";
|
|
2
|
+
export type WorktreeRecord = {
|
|
3
|
+
schema_version: 1;
|
|
4
|
+
workspace_id: string;
|
|
5
|
+
project: string;
|
|
6
|
+
path: string;
|
|
7
|
+
branch: string;
|
|
8
|
+
start_ref: string;
|
|
9
|
+
repository_fingerprint: string;
|
|
10
|
+
marker: string;
|
|
11
|
+
status: WorktreeStatus;
|
|
12
|
+
created_at: string;
|
|
13
|
+
updated_at: string;
|
|
14
|
+
};
|
|
15
|
+
export declare function worktreePlan(project: string, workspaceID?: `${string}-${string}-${string}-${string}-${string}`): Promise<Record<string, unknown>>;
|
|
16
|
+
export declare function worktreeCreate(input: {
|
|
17
|
+
project: string;
|
|
18
|
+
workspace_id: string;
|
|
19
|
+
path?: string;
|
|
20
|
+
branch?: string;
|
|
21
|
+
start_ref?: string;
|
|
22
|
+
}): Promise<WorktreeRecord>;
|
|
23
|
+
export declare function worktreeStatus(project: string, workspaceID: string): Promise<WorktreeRecord | undefined>;
|
|
24
|
+
export declare function worktreeList(project: string): Promise<WorktreeRecord[]>;
|
|
25
|
+
export declare function worktreeRelease(project: string, workspaceID: string): Promise<{
|
|
26
|
+
status: "released" | "blocked";
|
|
27
|
+
workspace_id: string;
|
|
28
|
+
reason?: string;
|
|
29
|
+
}>;
|
|
30
|
+
export declare function worktreeRecover(project: string): Promise<{
|
|
31
|
+
recovered: number;
|
|
32
|
+
blocked: number;
|
|
33
|
+
}>;
|