@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.
Files changed (157) hide show
  1. package/README.md +134 -108
  2. package/README.ru.md +242 -0
  3. package/dist/agent-profiles.d.ts +1 -1
  4. package/dist/agent-profiles.js +21 -10
  5. package/dist/assets/agents/architect.md +67 -0
  6. package/{assets → dist/assets}/agents/critic.md +17 -2
  7. package/dist/assets/agents/manager.md +36 -0
  8. package/{assets → dist/assets}/agents/mapper.md +2 -2
  9. package/dist/assets/agents/review.md +25 -0
  10. package/{assets → dist/assets}/agents/worker.md +7 -5
  11. package/dist/assets/commands/agent-profiles.md +9 -0
  12. package/dist/assets/commands/agents-md.md +10 -0
  13. package/dist/assets/commands/askme.md +10 -0
  14. package/dist/assets/commands/ast-grep.md +10 -0
  15. package/dist/assets/commands/capabilities.md +9 -0
  16. package/dist/assets/commands/code-explain.md +10 -0
  17. package/dist/assets/commands/code-review.md +10 -0
  18. package/dist/assets/commands/commit-msg.md +10 -0
  19. package/dist/assets/commands/docs-prepare.md +10 -0
  20. package/dist/assets/commands/docs-review.md +10 -0
  21. package/dist/assets/commands/doctor.md +9 -0
  22. package/dist/assets/commands/doit.md +10 -0
  23. package/dist/assets/commands/goal.md +10 -0
  24. package/dist/assets/commands/humanize.md +10 -0
  25. package/dist/assets/commands/lsp-report.md +10 -0
  26. package/dist/assets/commands/mattermost.md +10 -0
  27. package/dist/assets/commands/mr-prepare.md +10 -0
  28. package/dist/assets/commands/reconcile.md +9 -0
  29. package/dist/assets/commands/release-prepare.md +10 -0
  30. package/dist/assets/commands/release-review.md +10 -0
  31. package/dist/assets/commands/rtk.md +10 -0
  32. package/dist/assets/commands/skill-improve.md +10 -0
  33. package/dist/assets/commands/slides-prompts-prepare.md +10 -0
  34. package/dist/assets/commands/spec-manage.md +10 -0
  35. package/dist/assets/commands/stopit.md +10 -0
  36. package/dist/assets/commands/summary.md +10 -0
  37. package/dist/assets/commands/task-prepare.md +10 -0
  38. package/dist/assets/commands/task-review.md +10 -0
  39. package/dist/assets/commands/task-triage.md +10 -0
  40. package/dist/assets/commands/team-retro.md +10 -0
  41. package/dist/assets/commands/team-roadmap.md +10 -0
  42. package/dist/assets/commands/team-sprint-close.md +10 -0
  43. package/dist/assets/commands/team-sprint-start.md +10 -0
  44. package/dist/assets/lsp-catalog.json +30 -0
  45. package/dist/assets/migration-inventory.json +294 -0
  46. package/dist/catalog.d.ts +8 -0
  47. package/dist/catalog.js +38 -0
  48. package/dist/cli-output.d.ts +7 -0
  49. package/dist/cli-output.js +68 -1
  50. package/dist/cli.js +256 -59
  51. package/dist/contracts.d.ts +52 -0
  52. package/dist/contracts.js +98 -0
  53. package/dist/doctor.d.ts +44 -0
  54. package/dist/doctor.js +595 -0
  55. package/dist/generate-assets.js +17 -2
  56. package/dist/index.d.ts +84 -55
  57. package/dist/index.js +170 -37
  58. package/dist/installer.d.ts +33 -5
  59. package/dist/installer.js +252 -28
  60. package/dist/lifecycle.d.ts +3 -0
  61. package/dist/lifecycle.js +49 -17
  62. package/dist/plugins/rtk.js +4 -1
  63. package/dist/reconcile.d.ts +43 -0
  64. package/dist/reconcile.js +507 -0
  65. package/dist/registry.d.ts +7 -4
  66. package/dist/registry.js +28 -88
  67. package/dist/routing.d.ts +97 -3
  68. package/dist/routing.js +470 -18
  69. package/dist/runtime/state.d.ts +1 -0
  70. package/dist/runtime/state.js +12 -5
  71. package/dist/runtime/worktree.d.ts +33 -0
  72. package/dist/runtime/worktree.js +220 -0
  73. package/dist/terminal-wizard.d.ts +2 -0
  74. package/dist/terminal-wizard.js +125 -0
  75. package/package.json +10 -26
  76. package/assets/agents/architect.md +0 -33
  77. package/assets/agents/manager.md +0 -82
  78. package/assets/agents/review.md +0 -25
  79. package/assets/commands/agent-list.md +0 -9
  80. package/assets/commands/agent-model-set.md +0 -9
  81. package/assets/commands/askme.md +0 -10
  82. package/assets/commands/ast-grep-rewrite.md +0 -10
  83. package/assets/commands/ast-grep-search.md +0 -10
  84. package/assets/commands/attempt-cancel.md +0 -10
  85. package/assets/commands/attempt-list.md +0 -10
  86. package/assets/commands/attempt-result.md +0 -10
  87. package/assets/commands/attempt-show.md +0 -10
  88. package/assets/commands/capabilities.md +0 -9
  89. package/assets/commands/code-review.md +0 -10
  90. package/assets/commands/commit-msg.md +0 -10
  91. package/assets/commands/critic-add.md +0 -9
  92. package/assets/commands/critic-remove.md +0 -9
  93. package/assets/commands/docs-prepare.md +0 -10
  94. package/assets/commands/docs-review.md +0 -10
  95. package/assets/commands/doctor.md +0 -9
  96. package/assets/commands/doit.md +0 -10
  97. package/assets/commands/goal-list.md +0 -10
  98. package/assets/commands/goal-pause.md +0 -10
  99. package/assets/commands/goal-prepare.md +0 -10
  100. package/assets/commands/goal-remove.md +0 -10
  101. package/assets/commands/goal-show.md +0 -10
  102. package/assets/commands/goal-start.md +0 -10
  103. package/assets/commands/lsp-report.md +0 -10
  104. package/assets/commands/mattermost.md +0 -10
  105. package/assets/commands/mr-prepare.md +0 -10
  106. package/assets/commands/mr-review.md +0 -10
  107. package/assets/commands/multi-run-cancel.md +0 -10
  108. package/assets/commands/multi-run-compare.md +0 -10
  109. package/assets/commands/multi-run-fusion.md +0 -10
  110. package/assets/commands/multi-run-start.md +0 -10
  111. package/assets/commands/multi-run-status.md +0 -10
  112. package/assets/commands/overview.md +0 -10
  113. package/assets/commands/release-prepare.md +0 -10
  114. package/assets/commands/release-review.md +0 -10
  115. package/assets/commands/route.md +0 -9
  116. package/assets/commands/schedule-add.md +0 -10
  117. package/assets/commands/schedule-disable.md +0 -10
  118. package/assets/commands/schedule-enable.md +0 -10
  119. package/assets/commands/schedule-list.md +0 -10
  120. package/assets/commands/schedule-remove.md +0 -10
  121. package/assets/commands/schedule-status.md +0 -10
  122. package/assets/commands/skill-improver.md +0 -10
  123. package/assets/commands/spec-audit.md +0 -10
  124. package/assets/commands/spec-init.md +0 -10
  125. package/assets/commands/spec-onboard.md +0 -10
  126. package/assets/commands/spec-update.md +0 -10
  127. package/assets/commands/stopit.md +0 -10
  128. package/assets/commands/summary.md +0 -10
  129. package/assets/commands/task-prepare-batch.md +0 -10
  130. package/assets/commands/task-prepare.md +0 -10
  131. package/assets/commands/task-review.md +0 -10
  132. package/assets/commands/task-triage.md +0 -10
  133. package/assets/commands/team-planning.md +0 -10
  134. package/assets/commands/team-retro.md +0 -10
  135. package/assets/commands/team-roadmap.md +0 -10
  136. package/assets/commands/team-slides-prompts.md +0 -10
  137. package/assets/commands/team-sprint-close.md +0 -10
  138. package/assets/commands/team-sprint-status.md +0 -10
  139. package/assets/commands/walkthrough.md +0 -10
  140. package/assets/plugins/autonomy-policy.js +0 -3
  141. package/assets/plugins/background-attempts.js +0 -3
  142. package/assets/plugins/goal-loop.js +0 -3
  143. package/assets/plugins/schedule.js +0 -3
  144. package/assets/plugins/zed-clickable-paths.js +0 -3
  145. package/dist/plugins/autonomy-policy.d.ts +0 -21
  146. package/dist/plugins/autonomy-policy.js +0 -71
  147. package/dist/plugins/background-attempts.d.ts +0 -76
  148. package/dist/plugins/background-attempts.js +0 -113
  149. package/dist/plugins/goal-loop.d.ts +0 -51
  150. package/dist/plugins/goal-loop.js +0 -87
  151. package/dist/plugins/schedule.d.ts +0 -36
  152. package/dist/plugins/schedule.js +0 -100
  153. package/dist/plugins/zed-clickable-paths.d.ts +0 -11
  154. package/dist/plugins/zed-clickable-paths.js +0 -10
  155. /package/{assets → dist/assets}/plugins/rtk.js +0 -0
  156. /package/{assets → dist/assets}/plugins/rules-injector.js +0 -0
  157. /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
- export const CATEGORIES = ["exploration", "architecture", "implementation", "review", "documentation", "quick"];
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: { profiles: ["mapper", "architect"], capabilities: ["read", "search"], tools: ["read", "glob", "grep"], cost: "low", latency: "fast" },
7
- architecture: { profiles: ["architect", "mapper"], capabilities: ["read", "architecture"], tools: ["read", "glob", "grep"], cost: "medium", latency: "standard" },
8
- implementation: { profiles: ["worker"], capabilities: ["read", "write", "verify"], tools: ["read", "edit", "bash"], cost: "medium", latency: "standard" },
9
- review: { profiles: ["review", "critic"], capabilities: ["read", "review"], tools: ["read", "glob", "grep"], cost: "medium", latency: "standard" },
10
- documentation: { profiles: ["worker", "review"], capabilities: ["read", "write", "documentation"], tools: ["read", "edit"], cost: "low", latency: "standard" },
11
- quick: { profiles: ["mapper", "worker"], capabilities: ["read"], tools: ["read"], cost: "low", latency: "fast" }
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).sort().map((key) => `${JSON.stringify(key)}:${stable(object[key])}`).join(",")}}`;
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
- for (const profile of category.profiles) {
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
- const selected = input.override ? eligible.find((agent) => agent.agent === input.override) : eligible[0];
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
- ? { schema_version: 1, status: "selected", category: input.category, agent: selected.agent, reason_codes: [input.override ? "explicit_override" : selected.agent === category.profiles[0] ? "primary_available" : "fallback_selected", "capabilities_match"], alternatives, matrix_revision: revision }
63
- : { schema_version: 1, status: "escalate", category: input.category, reason_codes: [input.override ? "override_unavailable" : "no_eligible_profile"], alternatives, matrix_revision: revision };
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.decision_digest !== decision.decision_digest || provided.matrix_revision !== decision.matrix_revision)
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
- this.#receipts.set(sessionID, decision);
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
- consume(sessionID, agent) {
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
  }
@@ -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;
@@ -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 safeDirectory(dirname(path), boundary, true);
98
- await writeAtomic(path, Buffer.from(`${JSON.stringify(value)}\n`), 0o600);
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 safeDirectory(dirname(path), boundary, true);
104
- await appendPrivate(path, value);
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
+ }>;