@llblab/pi-actors 0.45.1 → 0.46.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.
Files changed (228) hide show
  1. package/AGENTS.md +9 -7
  2. package/BACKLOG.md +582 -0
  3. package/CHANGELOG.md +17 -1
  4. package/README.md +27 -13
  5. package/dist/fixtures/protocol/recipe-summary.json +1 -1
  6. package/dist/index.js +1 -1
  7. package/dist/lib/async-runs.d.ts +5 -1
  8. package/dist/lib/async-runs.js +26 -24
  9. package/dist/lib/automatic-review-runtime.d.ts +2 -0
  10. package/dist/lib/automatic-review-runtime.js +12 -5
  11. package/dist/lib/draft-sleep.js +0 -1
  12. package/dist/lib/execution.d.ts +1 -0
  13. package/dist/lib/execution.js +1 -0
  14. package/dist/lib/extension-runtime.d.ts +1 -1
  15. package/dist/lib/extension-runtime.js +26 -4
  16. package/dist/lib/inspector.js +46 -7
  17. package/dist/lib/paths.d.ts +0 -1
  18. package/dist/lib/paths.js +0 -7
  19. package/dist/lib/prompts.d.ts +1 -1
  20. package/dist/lib/prompts.js +4 -4
  21. package/dist/lib/recipes-context.d.ts +12 -3
  22. package/dist/lib/recipes-context.js +30 -6
  23. package/dist/lib/recipes-discovery.d.ts +18 -0
  24. package/dist/lib/recipes-discovery.js +95 -22
  25. package/dist/lib/recipes-references.d.ts +42 -9
  26. package/dist/lib/recipes-references.js +248 -135
  27. package/dist/lib/registry.d.ts +14 -1
  28. package/dist/lib/registry.js +110 -87
  29. package/dist/lib/runtime.d.ts +30 -4
  30. package/dist/lib/runtime.js +74 -12
  31. package/dist/lib/tools-inspect.d.ts +3 -1
  32. package/dist/lib/tools-inspect.js +56 -9
  33. package/dist/lib/tools-local.d.ts +2 -0
  34. package/dist/lib/tools-local.js +5 -2
  35. package/dist/lib/tools-response.js +2 -0
  36. package/dist/lib/tools-spawn.d.ts +2 -0
  37. package/dist/lib/tools-spawn.js +24 -52
  38. package/dist/lib/tools.d.ts +4 -1
  39. package/dist/lib/tools.js +2 -0
  40. package/dist/scripts/build-dist.mjs +13 -4
  41. package/dist/skills/actors/SKILL.md +8 -8
  42. package/dist/skills/actors/recipes/recipe-validate.json +11 -0
  43. package/{recipes/utility-coordinator-lock-snapshot.json → dist/skills/actors/recipes/resource-locker-snapshot.json} +1 -3
  44. package/dist/{recipes → skills/actors/recipes}/resource-locker.json +1 -3
  45. package/dist/{recipes/utility-run-ops-snapshot.json → skills/actors/recipes/run-ops-snapshot.json} +1 -3
  46. package/dist/skills/actors/recipes/run-summary.json +9 -0
  47. package/dist/{scripts/locker.mjs → skills/actors/scripts/resource-locker.mjs} +2 -2
  48. package/dist/skills/actors/scripts/run-utils.mjs +164 -0
  49. package/{scripts → dist/skills/actors/scripts}/validate-recipe.mjs +134 -17
  50. package/dist/skills/artifacts/SKILL.md +16 -0
  51. package/{recipes/pipeline-artifact-bundle.json → dist/skills/artifacts/recipes/bundle.json} +4 -4
  52. package/dist/{recipes/utility-artifact-write.json → skills/artifacts/recipes/file-write.json} +1 -3
  53. package/dist/{recipes/utility-artifact-manifest.json → skills/artifacts/recipes/manifest.json} +1 -3
  54. package/{recipes/pipeline-artifact-report.json → dist/skills/artifacts/recipes/report.json} +2 -2
  55. package/dist/{recipes/pipeline-artifact-write.json → skills/artifacts/recipes/write.json} +3 -3
  56. package/dist/skills/artifacts/scripts/artifact-utils.mjs +63 -0
  57. package/dist/skills/media/SKILL.md +16 -0
  58. package/dist/{recipes/pipeline-media-library.json → skills/media/recipes/library.json} +2 -2
  59. package/{recipes/music-player.json → dist/skills/media/recipes/player.json} +1 -3
  60. package/dist/{recipes/utility-playlist-build.json → skills/media/recipes/playlist-build.json} +1 -3
  61. package/dist/skills/media/scripts/media-utils.mjs +47 -0
  62. package/dist/{scripts → skills/media/scripts}/music-player.mjs +9 -3
  63. package/dist/skills/project-work/SKILL.md +16 -0
  64. package/dist/skills/project-work/recipes/changelog-section.json +11 -0
  65. package/{recipes/pipeline-docs-maintenance.json → dist/skills/project-work/recipes/docs-maintenance.json} +4 -4
  66. package/dist/skills/project-work/recipes/package-summary.json +9 -0
  67. package/{recipes/pipeline-release-readiness.json → dist/skills/project-work/recipes/release-readiness.json} +6 -6
  68. package/dist/{recipes/pipeline-release-summary.json → skills/project-work/recipes/release-summary.json} +5 -5
  69. package/{recipes/pipeline-repo-health.json → dist/skills/project-work/recipes/repo-health.json} +6 -6
  70. package/{recipes/pipeline-async-run-ops.json → dist/skills/project-work/recipes/run-ops.json} +3 -3
  71. package/dist/skills/project-work/recipes/skill-summary.json +11 -0
  72. package/dist/skills/project-work/scripts/project-utils.mjs +89 -0
  73. package/dist/skills/recipe-memory/SKILL.md +16 -0
  74. package/dist/skills/swarm/SKILL.md +4 -4
  75. package/{recipes/pipeline-architect-coordinator.json → dist/skills/swarm/recipes/architect.json} +5 -5
  76. package/{recipes/pipeline-checkpoint-continuation.json → dist/skills/swarm/recipes/checkpoint-continuation.json} +3 -3
  77. package/dist/{recipes/pipeline-development-tasking.json → skills/swarm/recipes/development-tasking.json} +4 -4
  78. package/dist/{recipes/lens-swarm.json → skills/swarm/recipes/lens-review.json} +1 -1
  79. package/dist/{recipes/pipeline-quorum-review.json → skills/swarm/recipes/quorum-review.json} +4 -4
  80. package/{recipes/pipeline-research-synthesis.json → dist/skills/swarm/recipes/research-synthesis.json} +6 -6
  81. package/{recipes/pipeline-review-readiness.json → dist/skills/swarm/recipes/review-readiness.json} +1 -1
  82. package/{recipes/subagents-prompts.json → dist/skills/swarm/recipes/subagent-prompts.json} +1 -1
  83. package/{recipes → dist/skills/swarm/recipes}/subagent-quorum.json +1 -1
  84. package/dist/{recipes → skills/swarm/recipes}/subagent-review-coordinator.json +6 -6
  85. package/docs/actor-inspector.md +4 -3
  86. package/docs/async-runs.md +2 -2
  87. package/docs/recipe-library.md +23 -23
  88. package/docs/releasing.md +2 -2
  89. package/docs/template-recipes.md +32 -14
  90. package/docs/tool-registry.md +10 -4
  91. package/fixtures/protocol/recipe-summary.json +1 -1
  92. package/index.ts +2 -1
  93. package/lib/async-runs.ts +52 -25
  94. package/lib/automatic-review-runtime.ts +16 -3
  95. package/lib/draft-sleep.ts +0 -1
  96. package/lib/execution.ts +2 -0
  97. package/lib/extension-runtime.ts +42 -4
  98. package/lib/inspector.ts +57 -11
  99. package/lib/paths.ts +0 -7
  100. package/lib/prompts.ts +4 -4
  101. package/lib/recipes-context.ts +51 -6
  102. package/lib/recipes-discovery.ts +164 -24
  103. package/lib/recipes-references.ts +359 -146
  104. package/lib/registry.ts +185 -94
  105. package/lib/runtime.ts +109 -17
  106. package/lib/tools-inspect.ts +77 -10
  107. package/lib/tools-local.ts +9 -1
  108. package/lib/tools-response.ts +2 -0
  109. package/lib/tools-spawn.ts +38 -74
  110. package/lib/tools.ts +4 -1
  111. package/package.json +2 -4
  112. package/scripts/build-dist.mjs +13 -4
  113. package/skills/actors/SKILL.md +8 -8
  114. package/skills/actors/recipes/recipe-validate.json +11 -0
  115. package/{dist/recipes/utility-coordinator-lock-snapshot.json → skills/actors/recipes/resource-locker-snapshot.json} +1 -3
  116. package/{recipes → skills/actors/recipes}/resource-locker.json +1 -3
  117. package/{recipes/utility-run-ops-snapshot.json → skills/actors/recipes/run-ops-snapshot.json} +1 -3
  118. package/skills/actors/recipes/run-summary.json +9 -0
  119. package/{scripts/locker.mjs → skills/actors/scripts/resource-locker.mjs} +2 -2
  120. package/skills/actors/scripts/run-utils.mjs +164 -0
  121. package/{dist → skills/actors}/scripts/validate-recipe.mjs +134 -17
  122. package/skills/artifacts/SKILL.md +16 -0
  123. package/{dist/recipes/pipeline-artifact-bundle.json → skills/artifacts/recipes/bundle.json} +4 -4
  124. package/{recipes/utility-artifact-write.json → skills/artifacts/recipes/file-write.json} +1 -3
  125. package/{recipes/utility-artifact-manifest.json → skills/artifacts/recipes/manifest.json} +1 -3
  126. package/{dist/recipes/pipeline-artifact-report.json → skills/artifacts/recipes/report.json} +2 -2
  127. package/{recipes/pipeline-artifact-write.json → skills/artifacts/recipes/write.json} +3 -3
  128. package/skills/artifacts/scripts/artifact-utils.mjs +63 -0
  129. package/skills/media/SKILL.md +16 -0
  130. package/{recipes/pipeline-media-library.json → skills/media/recipes/library.json} +2 -2
  131. package/{dist/recipes/music-player.json → skills/media/recipes/player.json} +1 -3
  132. package/{recipes/utility-playlist-build.json → skills/media/recipes/playlist-build.json} +1 -3
  133. package/skills/media/scripts/media-utils.mjs +47 -0
  134. package/{scripts → skills/media/scripts}/music-player.mjs +9 -3
  135. package/skills/project-work/SKILL.md +16 -0
  136. package/skills/project-work/recipes/changelog-section.json +11 -0
  137. package/{dist/recipes/pipeline-docs-maintenance.json → skills/project-work/recipes/docs-maintenance.json} +4 -4
  138. package/skills/project-work/recipes/package-summary.json +9 -0
  139. package/{dist/recipes/pipeline-release-readiness.json → skills/project-work/recipes/release-readiness.json} +6 -6
  140. package/{recipes/pipeline-release-summary.json → skills/project-work/recipes/release-summary.json} +5 -5
  141. package/{dist/recipes/pipeline-repo-health.json → skills/project-work/recipes/repo-health.json} +6 -6
  142. package/{dist/recipes/pipeline-async-run-ops.json → skills/project-work/recipes/run-ops.json} +3 -3
  143. package/skills/project-work/recipes/skill-summary.json +11 -0
  144. package/skills/project-work/scripts/project-utils.mjs +89 -0
  145. package/skills/recipe-memory/SKILL.md +16 -0
  146. package/skills/swarm/SKILL.md +4 -4
  147. package/{dist/recipes/pipeline-architect-coordinator.json → skills/swarm/recipes/architect.json} +5 -5
  148. package/{dist/recipes/pipeline-checkpoint-continuation.json → skills/swarm/recipes/checkpoint-continuation.json} +3 -3
  149. package/{recipes/pipeline-development-tasking.json → skills/swarm/recipes/development-tasking.json} +4 -4
  150. package/{recipes/lens-swarm.json → skills/swarm/recipes/lens-review.json} +1 -1
  151. package/{recipes/pipeline-quorum-review.json → skills/swarm/recipes/quorum-review.json} +4 -4
  152. package/{dist/recipes/pipeline-research-synthesis.json → skills/swarm/recipes/research-synthesis.json} +6 -6
  153. package/{dist/recipes/pipeline-review-readiness.json → skills/swarm/recipes/review-readiness.json} +1 -1
  154. package/{dist/recipes/subagents-prompts.json → skills/swarm/recipes/subagent-prompts.json} +1 -1
  155. package/{dist → skills/swarm}/recipes/subagent-quorum.json +1 -1
  156. package/{recipes → skills/swarm/recipes}/subagent-review-coordinator.json +6 -6
  157. package/dist/recipes/utility-changelog-section.json +0 -13
  158. package/dist/recipes/utility-package-summary.json +0 -11
  159. package/dist/recipes/utility-run-summary.json +0 -11
  160. package/dist/recipes/utility-skill-summary.json +0 -13
  161. package/dist/recipes/utility-validate-recipe.json +0 -13
  162. package/dist/scripts/executable-block-style.d.mts +0 -1
  163. package/dist/scripts/executable-block-style.mjs +0 -73
  164. package/dist/scripts/recipe-utils.mjs +0 -404
  165. package/dist/scripts/release-gates.mjs +0 -281
  166. package/recipes/utility-changelog-section.json +0 -13
  167. package/recipes/utility-package-summary.json +0 -11
  168. package/recipes/utility-run-summary.json +0 -11
  169. package/recipes/utility-skill-summary.json +0 -13
  170. package/recipes/utility-validate-recipe.json +0 -13
  171. package/scripts/executable-block-style.d.mts +0 -1
  172. package/scripts/executable-block-style.mjs +0 -73
  173. package/scripts/recipe-utils.mjs +0 -404
  174. package/scripts/release-gates.mjs +0 -281
  175. /package/dist/{recipes/utility-validation-wrapper.json → skills/actors/recipes/command-validate.json} +0 -0
  176. /package/dist/{recipes/utility-jsonl-tail.json → skills/actors/recipes/jsonl-tail.json} +0 -0
  177. /package/dist/{recipes/utility-run-state-files.json → skills/actors/recipes/run-state-files.json} +0 -0
  178. /package/dist/{recipes/utility-playlist-scan.json → skills/media/recipes/playlist-scan.json} +0 -0
  179. /package/dist/{recipes/utility-changelog-head.json → skills/project-work/recipes/changelog-head.json} +0 -0
  180. /package/dist/{recipes/utility-git-log.json → skills/project-work/recipes/git-log.json} +0 -0
  181. /package/dist/{recipes/utility-git-status.json → skills/project-work/recipes/git-status.json} +0 -0
  182. /package/dist/{recipes/utility-markdown-index.json → skills/project-work/recipes/markdown-index.json} +0 -0
  183. /package/dist/{recipes → skills/recipe-memory/recipes}/draft-review.json +0 -0
  184. /package/dist/{recipes → skills/recipe-memory/recipes}/tool-review.json +0 -0
  185. /package/dist/{recipes → skills/swarm/recipes}/subagent-artifact.json +0 -0
  186. /package/dist/{recipes → skills/swarm/recipes}/subagent-checkpoint.json +0 -0
  187. /package/dist/{recipes → skills/swarm/recipes}/subagent-conflict-report.json +0 -0
  188. /package/dist/{recipes → skills/swarm/recipes}/subagent-contradiction-map.json +0 -0
  189. /package/dist/{recipes → skills/swarm/recipes}/subagent-critic.json +0 -0
  190. /package/dist/{recipes → skills/swarm/recipes}/subagent-evidence-map.json +0 -0
  191. /package/dist/{recipes → skills/swarm/recipes}/subagent-followup.json +0 -0
  192. /package/dist/{recipes → skills/swarm/recipes}/subagent-judge.json +0 -0
  193. /package/dist/{recipes → skills/swarm/recipes}/subagent-merge.json +0 -0
  194. /package/dist/{recipes → skills/swarm/recipes}/subagent-normalize.json +0 -0
  195. /package/dist/{recipes → skills/swarm/recipes}/subagent-plan.json +0 -0
  196. /package/dist/{recipes → skills/swarm/recipes}/subagent-preflight.json +0 -0
  197. /package/dist/{recipes → skills/swarm/recipes}/subagent-prompt.json +0 -0
  198. /package/dist/{recipes → skills/swarm/recipes}/subagent-review.json +0 -0
  199. /package/dist/{recipes → skills/swarm/recipes}/subagent-task-card.json +0 -0
  200. /package/dist/{recipes → skills/swarm/recipes}/subagent-tools.json +0 -0
  201. /package/dist/{recipes → skills/swarm/recipes}/subagent-verify.json +0 -0
  202. /package/{recipes/utility-validation-wrapper.json → skills/actors/recipes/command-validate.json} +0 -0
  203. /package/{recipes/utility-jsonl-tail.json → skills/actors/recipes/jsonl-tail.json} +0 -0
  204. /package/{recipes/utility-run-state-files.json → skills/actors/recipes/run-state-files.json} +0 -0
  205. /package/{recipes/utility-playlist-scan.json → skills/media/recipes/playlist-scan.json} +0 -0
  206. /package/{recipes/utility-changelog-head.json → skills/project-work/recipes/changelog-head.json} +0 -0
  207. /package/{recipes/utility-git-log.json → skills/project-work/recipes/git-log.json} +0 -0
  208. /package/{recipes/utility-git-status.json → skills/project-work/recipes/git-status.json} +0 -0
  209. /package/{recipes/utility-markdown-index.json → skills/project-work/recipes/markdown-index.json} +0 -0
  210. /package/{recipes → skills/recipe-memory/recipes}/draft-review.json +0 -0
  211. /package/{recipes → skills/recipe-memory/recipes}/tool-review.json +0 -0
  212. /package/{recipes → skills/swarm/recipes}/subagent-artifact.json +0 -0
  213. /package/{recipes → skills/swarm/recipes}/subagent-checkpoint.json +0 -0
  214. /package/{recipes → skills/swarm/recipes}/subagent-conflict-report.json +0 -0
  215. /package/{recipes → skills/swarm/recipes}/subagent-contradiction-map.json +0 -0
  216. /package/{recipes → skills/swarm/recipes}/subagent-critic.json +0 -0
  217. /package/{recipes → skills/swarm/recipes}/subagent-evidence-map.json +0 -0
  218. /package/{recipes → skills/swarm/recipes}/subagent-followup.json +0 -0
  219. /package/{recipes → skills/swarm/recipes}/subagent-judge.json +0 -0
  220. /package/{recipes → skills/swarm/recipes}/subagent-merge.json +0 -0
  221. /package/{recipes → skills/swarm/recipes}/subagent-normalize.json +0 -0
  222. /package/{recipes → skills/swarm/recipes}/subagent-plan.json +0 -0
  223. /package/{recipes → skills/swarm/recipes}/subagent-preflight.json +0 -0
  224. /package/{recipes → skills/swarm/recipes}/subagent-prompt.json +0 -0
  225. /package/{recipes → skills/swarm/recipes}/subagent-review.json +0 -0
  226. /package/{recipes → skills/swarm/recipes}/subagent-task-card.json +0 -0
  227. /package/{recipes → skills/swarm/recipes}/subagent-tools.json +0 -0
  228. /package/{recipes → skills/swarm/recipes}/subagent-verify.json +0 -0
@@ -4,9 +4,8 @@
4
4
  * Owns detection, loading, and recipe-layer expansion for template recipe files
5
5
  */
6
6
 
7
- import { existsSync, readFileSync, statSync } from "node:fs";
8
- import { homedir } from "node:os";
9
- import { basename, dirname, extname, isAbsolute, relative, resolve } from "node:path";
7
+ import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
8
+ import { basename, dirname, extname, isAbsolute, join, relative, resolve } from "node:path";
10
9
 
11
10
  import type {
12
11
  CommandTemplateConfig,
@@ -29,7 +28,6 @@ export interface TemplateRecipeImportBinding {
29
28
  export type TemplateRecipeImport = string | TemplateRecipeImportBinding;
30
29
 
31
30
  export interface TemplateRecipeDefinition {
32
- name?: string;
33
31
  description?: string;
34
32
  disabled?: boolean;
35
33
  imports?: Record<string, TemplateRecipeImport>;
@@ -57,6 +55,7 @@ export interface TemplateRecipeDefinition {
57
55
  }
58
56
 
59
57
  export interface TemplateRecipeConfig extends TemplateRecipeDefinition {
58
+ name?: string;
60
59
  async?: boolean;
61
60
  recipe_dir?: string;
62
61
  skill_dir?: string;
@@ -66,7 +65,7 @@ interface ImportedRecipe {
66
65
  alias: string;
67
66
  file: string;
68
67
  name: string;
69
- config: TemplateRecipeDefinition;
68
+ config: TemplateRecipeConfig;
70
69
  defaults: Record<string, unknown>;
71
70
  values: Record<string, unknown>;
72
71
  }
@@ -74,20 +73,48 @@ interface ImportedRecipe {
74
73
  export interface TemplateRecipeContextRecord {
75
74
  alias?: string;
76
75
  depth: number;
77
- file: string;
78
76
  import_path: string[];
77
+ logical_reference: string;
79
78
  name: string;
80
- qualified_name?: string;
81
79
  recipe: Record<string, unknown>;
82
80
  role: "entry" | "import";
81
+ skill?: string;
82
+ source_file: string;
83
+ source_kind:
84
+ | "active_skill_component"
85
+ | "explicit_file_recipe"
86
+ | "user_registry_capability";
83
87
  }
84
88
 
85
89
  export interface ReadResolvedRecipeConfigOptions {
90
+ authoredEntry?: { path: string; recipe: Record<string, unknown> };
86
91
  includeActorRecipeContext?: boolean;
92
+ skillContext?: ActiveSkillRecipeContext;
87
93
  }
88
94
 
89
- const RUNTIME_RECIPE_VALUE_NAMES = new Set(["recipe_dir", "skill_dir"]);
90
- const activeSkillRecipeRoots = new Map<string, Set<string>>();
95
+ const RESERVED_AUTHORED_RECIPE_VALUE_NAMES = new Set([
96
+ "recipe_dir",
97
+ "skill_dir",
98
+ ]);
99
+
100
+ export const RUNTIME_OWNED_RECIPE_INPUT_NAMES = Object.freeze([
101
+ "owner_id",
102
+ "recipe_dir",
103
+ "run_instance_id",
104
+ "runtime_state_root",
105
+ "session_id",
106
+ "skill_dir",
107
+ "state_dir",
108
+ "trace_file",
109
+ ]);
110
+
111
+ const RUNTIME_OWNED_RECIPE_INPUT_SET = new Set(
112
+ RUNTIME_OWNED_RECIPE_INPUT_NAMES,
113
+ );
114
+
115
+ export function isRuntimeOwnedRecipeInput(name: string): boolean {
116
+ return RUNTIME_OWNED_RECIPE_INPUT_SET.has(name);
117
+ }
91
118
 
92
119
  export interface ActiveSkillRecipeSource {
93
120
  name: string;
@@ -95,10 +122,14 @@ export interface ActiveSkillRecipeSource {
95
122
  baseDir?: string;
96
123
  }
97
124
 
98
- export function setActiveSkillRecipeSources(
125
+ export interface ActiveSkillRecipeContext {
126
+ readonly namespaces: Readonly<Record<string, readonly string[]>>;
127
+ }
128
+
129
+ export function createActiveSkillRecipeContext(
99
130
  skills: ActiveSkillRecipeSource[],
100
- ): void {
101
- activeSkillRecipeRoots.clear();
131
+ ): ActiveSkillRecipeContext {
132
+ const rootsByName = new Map<string, Set<string>>();
102
133
  for (const skill of skills) {
103
134
  const name = skill.name.trim();
104
135
  const skillDir = skill.baseDir
@@ -107,72 +138,236 @@ export function setActiveSkillRecipeSources(
107
138
  ? dirname(resolve(skill.filePath))
108
139
  : undefined;
109
140
  if (!name || !skillDir) continue;
110
- const roots = activeSkillRecipeRoots.get(name) ?? new Set<string>();
141
+ const roots = rootsByName.get(name) ?? new Set<string>();
111
142
  roots.add(resolve(skillDir, "recipes"));
112
- activeSkillRecipeRoots.set(name, roots);
143
+ rootsByName.set(name, roots);
113
144
  }
145
+ const namespaces = Object.fromEntries(
146
+ [...rootsByName.entries()]
147
+ .sort(([left], [right]) => left.localeCompare(right))
148
+ .map(([name, roots]) => [name, Object.freeze([...roots].sort())]),
149
+ );
150
+ return Object.freeze({ namespaces: Object.freeze(namespaces) });
114
151
  }
115
152
 
116
- export function getActiveSkillRecipeNamespaces(): Record<string, string[]> {
153
+ export const EMPTY_ACTIVE_SKILL_RECIPE_CONTEXT =
154
+ createActiveSkillRecipeContext([]);
155
+
156
+ export function getActiveSkillRecipeNamespaces(
157
+ context: ActiveSkillRecipeContext,
158
+ ): Record<string, string[]> {
117
159
  return Object.fromEntries(
118
- [...activeSkillRecipeRoots.entries()]
119
- .sort(([left], [right]) => left.localeCompare(right))
120
- .map(([name, roots]) => [name, [...roots].sort()]),
160
+ Object.entries(context.namespaces).map(([name, roots]) => [
161
+ name,
162
+ [...roots],
163
+ ]),
121
164
  );
122
165
  }
123
166
 
124
- function qualifiedRecipeNameForFile(file: string): string | undefined {
125
- const path = resolve(file);
126
- const stdRelation = relative(Paths.getPackagedRecipeRoot(), path);
127
- if (
128
- stdRelation &&
129
- !stdRelation.startsWith("..") &&
130
- !isAbsolute(stdRelation)
131
- ) {
132
- return `std:${stdRelation.replace(/\.(?:json|md)$/u, "")}`;
167
+ export interface ActiveSkillRecipeComponent {
168
+ file: string;
169
+ identity: string;
170
+ imports: Record<string, string>;
171
+ skill: string;
172
+ stem: string;
173
+ }
174
+
175
+ export interface SkillRecipeComponentDiagnostic {
176
+ file?: string;
177
+ reason: string;
178
+ skill: string;
179
+ stem?: string;
180
+ }
181
+
182
+ export interface SkillRecipeComponentInventory {
183
+ components: ActiveSkillRecipeComponent[];
184
+ partial: boolean;
185
+ rejected: SkillRecipeComponentDiagnostic[];
186
+ }
187
+
188
+ function nestedRecipeFiles(root: string): string[] {
189
+ if (!existsSync(root)) return [];
190
+ return readdirSync(root, { withFileTypes: true }).flatMap((entry) => {
191
+ const path = join(root, entry.name);
192
+ if (entry.isDirectory()) return nestedRecipeFiles(path);
193
+ return entry.isFile() && /\.(?:json|md)$/u.test(entry.name) ? [path] : [];
194
+ });
195
+ }
196
+
197
+ export function inventoryActiveSkillRecipeComponents(
198
+ context: ActiveSkillRecipeContext,
199
+ ): SkillRecipeComponentInventory {
200
+ const components: ActiveSkillRecipeComponent[] = [];
201
+ const rejected: SkillRecipeComponentDiagnostic[] = [];
202
+ for (const [skill, roots] of Object.entries(context.namespaces)) {
203
+ if (roots.length > 1) {
204
+ rejected.push({
205
+ reason: `Duplicate active Skill identity ${skill}: ${roots.join(", ")}`,
206
+ skill,
207
+ });
208
+ continue;
209
+ }
210
+ const root = roots[0];
211
+ if (!root || !existsSync(root)) continue;
212
+ const direct = readdirSync(root, { withFileTypes: true });
213
+ for (const entry of direct.filter((candidate) => candidate.isDirectory())) {
214
+ const nested = nestedRecipeFiles(join(root, entry.name));
215
+ for (const file of nested) {
216
+ rejected.push({
217
+ file,
218
+ reason: `Nested Skill Recipe file is not addressable: ${file}`,
219
+ skill,
220
+ stem: getRecipeIdFromPath(file),
221
+ });
222
+ }
223
+ }
224
+ const files = direct
225
+ .filter(
226
+ (entry) => entry.isFile() && /\.(?:json|md)$/u.test(entry.name),
227
+ )
228
+ .map((entry) => join(root, entry.name));
229
+ const byStem = new Map<string, string[]>();
230
+ for (const file of files) {
231
+ const stem = getRecipeIdFromPath(file);
232
+ const matches = byStem.get(stem) ?? [];
233
+ matches.push(file);
234
+ byStem.set(stem, matches);
235
+ }
236
+ for (const [stem, matches] of [...byStem].sort(([a], [b]) => a.localeCompare(b))) {
237
+ if (!/^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/u.test(stem)) {
238
+ rejected.push({
239
+ file: matches[0],
240
+ reason: `Invalid Skill Recipe filename stem: ${stem}`,
241
+ skill,
242
+ stem,
243
+ });
244
+ continue;
245
+ }
246
+ if (matches.length > 1) {
247
+ rejected.push({
248
+ file: matches[0],
249
+ reason: `Skill Recipe stem collision: ${skill}/${stem} has both .json and .md files`,
250
+ skill,
251
+ stem,
252
+ });
253
+ continue;
254
+ }
255
+ const file = matches[0];
256
+ try {
257
+ const raw = readRawRecipeConfig(file);
258
+ if (!raw || !Object.hasOwn(raw, "template"))
259
+ throw new Error(`Invalid Skill Recipe component: ${skill}/${stem}`);
260
+ assertRecipeHasNoDeclaredName(raw);
261
+ components.push({
262
+ file,
263
+ identity: `${skill}/${stem}`,
264
+ imports: Object.fromEntries(
265
+ Object.entries(getRecipeImports(raw)).map(([alias, value]) => [
266
+ alias,
267
+ getImportFrom(value),
268
+ ]),
269
+ ),
270
+ skill,
271
+ stem,
272
+ });
273
+ } catch (error) {
274
+ rejected.push({
275
+ file,
276
+ reason: error instanceof Error ? error.message : String(error),
277
+ skill,
278
+ stem,
279
+ });
280
+ }
281
+ }
133
282
  }
134
- for (const [skillName, roots] of activeSkillRecipeRoots) {
135
- if (roots.size !== 1) continue;
136
- const root = [...roots][0];
283
+ return { components, partial: rejected.length > 0, rejected };
284
+ }
285
+
286
+ export function listActiveSkillRecipeComponents(
287
+ context: ActiveSkillRecipeContext,
288
+ ): ActiveSkillRecipeComponent[] {
289
+ const inventory = inventoryActiveSkillRecipeComponents(context);
290
+ if (inventory.rejected.length > 0)
291
+ throw new Error(inventory.rejected[0].reason);
292
+ return inventory.components;
293
+ }
294
+
295
+ const SKILL_RECIPE_REFERENCE =
296
+ /^([a-z][a-z0-9]*(?:-[a-z0-9]+)*)\/([a-z][a-z0-9]*(?:-[a-z0-9]+)*)$/;
297
+
298
+ function qualifiedRecipeNameForFile(
299
+ file: string,
300
+ context: ActiveSkillRecipeContext,
301
+ ): string | undefined {
302
+ const path = resolve(file);
303
+ for (const [skillName, roots] of Object.entries(context.namespaces)) {
304
+ if (roots.length !== 1) continue;
305
+ const root = roots[0];
137
306
  const relation = relative(root, path);
138
- if (relation && !relation.startsWith("..") && !isAbsolute(relation))
139
- return `skill:${skillName}/${relation.replaceAll("\\", "/").replace(/\.(?:json|md)$/u, "")}`;
307
+ if (
308
+ relation &&
309
+ dirname(relation) === "." &&
310
+ !relation.startsWith("..") &&
311
+ !isAbsolute(relation)
312
+ ) {
313
+ return `${skillName}/${basename(relation, extname(relation))}`;
314
+ }
140
315
  }
141
316
  return undefined;
142
317
  }
143
318
 
144
- function qualifiedRecipeCandidates(value: string): string[] | undefined {
319
+ function assertRecipeReferencePrefixWasNotRemoved(value: string): void {
145
320
  if (value.startsWith("std:")) {
146
- return recipeNameFiles(value.slice(4)).map((file) =>
147
- resolve(Paths.getPackagedRecipeRoot(), file),
321
+ throw new Error(
322
+ "std: Recipe references were removed in pi-actors 0.46.\nUse <skill>/<recipe> or an explicit .json/.md file path.",
148
323
  );
149
324
  }
150
- if (!value.startsWith("skill:")) return undefined;
151
- const qualified = value.slice(6);
152
- const separator = qualified.indexOf("/");
153
- if (separator <= 0 || separator === qualified.length - 1)
154
- throw new Error(`Invalid Skill Recipe reference: ${value}`);
155
- const skillName = qualified.slice(0, separator);
156
- const recipeName = qualified.slice(separator + 1);
157
- const roots = [...(activeSkillRecipeRoots.get(skillName) ?? [])];
325
+ if (value.startsWith("skill:")) {
326
+ throw new Error(
327
+ "skill: Recipe references were removed in pi-actors 0.46.\nUse <skill>/<recipe> without a prefix.",
328
+ );
329
+ }
330
+ }
331
+
332
+ function skillRecipeCandidates(
333
+ value: string,
334
+ context: ActiveSkillRecipeContext,
335
+ ): string[] | undefined {
336
+ const match = SKILL_RECIPE_REFERENCE.exec(value);
337
+ if (!match) return undefined;
338
+ const [, skillName, recipeName] = match;
339
+ const roots = [...(context.namespaces[skillName] ?? [])];
158
340
  if (roots.length === 0)
159
- throw new Error(`Active Skill Recipe namespace not found: ${skillName}`);
341
+ throw new Error(`Active Skill Recipe not found: ${skillName}/${recipeName}`);
160
342
  if (roots.length > 1)
161
343
  throw new Error(
162
- `Ambiguous active Skill Recipe namespace ${skillName}: ${roots.join(", ")}`,
344
+ `Duplicate active Skill identity ${skillName}: ${roots.join(", ")}`,
163
345
  );
164
346
  const root = roots[0];
165
- return recipeNameFiles(recipeName).map((file) => {
166
- const candidate = resolve(root, file);
167
- const relation = relative(root, candidate);
168
- if (!relation || relation.startsWith("..") || isAbsolute(relation))
169
- throw new Error(`Skill Recipe reference escapes its recipes root: ${value}`);
170
- return candidate;
171
- });
347
+ const candidates = ["json", "md"].map((extension) =>
348
+ resolve(root, `${recipeName}.${extension}`),
349
+ );
350
+ const existing = candidates.filter((candidate) => existsSync(candidate));
351
+ if (existing.length > 1) {
352
+ throw new Error(
353
+ `Skill Recipe stem collision: ${skillName}/${recipeName} has both .json and .md files`,
354
+ );
355
+ }
356
+ return candidates;
172
357
  }
173
358
 
174
- function hasWhitespace(value: string): boolean {
175
- return /\s/.test(value);
359
+ function recipeReferenceCandidates(
360
+ value: string,
361
+ baseDir: string,
362
+ context: ActiveSkillRecipeContext,
363
+ ): string[] | undefined {
364
+ const trimmed = value.trim();
365
+ if (!trimmed) return undefined;
366
+ assertRecipeReferencePrefixWasNotRemoved(trimmed);
367
+ if (trimmed.endsWith(".json") || trimmed.endsWith(".md")) {
368
+ return [resolve(baseDir, trimmed)];
369
+ }
370
+ return skillRecipeCandidates(trimmed, context);
176
371
  }
177
372
 
178
373
  function findOwningSkillDir(recipeFile: string): string | undefined {
@@ -185,11 +380,18 @@ function findOwningSkillDir(recipeFile: string): string | undefined {
185
380
  }
186
381
  }
187
382
 
383
+ function assertRecipeHasNoDeclaredName(raw: Record<string, unknown>): void {
384
+ if (!Object.hasOwn(raw, "name")) return;
385
+ throw new Error(
386
+ "Recipe.name was removed in pi-actors 0.46.\nFile-backed Recipe identity is derived from its file name.",
387
+ );
388
+ }
389
+
188
390
  function assertRuntimeRecipeValuesNotDeclared(raw: Record<string, unknown>): void {
189
391
  const args = Array.isArray(raw.args)
190
392
  ? Schema.getExplicitToolArgNames(raw.args.map(String))
191
393
  : [];
192
- for (const name of RUNTIME_RECIPE_VALUE_NAMES) {
394
+ for (const name of RESERVED_AUTHORED_RECIPE_VALUE_NAMES) {
193
395
  if (
194
396
  args.includes(name) ||
195
397
  (isRecord(raw.defaults) && Object.hasOwn(raw.defaults, name)) ||
@@ -202,100 +404,51 @@ function assertRuntimeRecipeValuesNotDeclared(raw: Record<string, unknown>): voi
202
404
 
203
405
  export function resolveRecipePath(
204
406
  value: string,
205
- recipeRoot = Paths.getRecipeRoot(),
407
+ baseDir = Paths.getRecipeRoot(),
206
408
  ): string {
207
- const trimmed = value.trim();
208
- const repoRoot = resolve(recipeRoot, "..");
209
- const expanded = trimmed
210
- .replaceAll("{repo}", repoRoot)
211
- .replaceAll("{agent}", Paths.getAgentDir());
212
- if (expanded.startsWith("~/")) return resolve(homedir(), expanded.slice(2));
213
- if (expanded.includes("/")) return resolve(expanded);
214
- return resolve(
215
- recipeRoot,
216
- expanded.endsWith(".json") || expanded.endsWith(".md")
217
- ? expanded
218
- : `${expanded}.json`,
219
- );
220
- }
221
-
222
- function isBareRecipeName(value: string): boolean {
223
- const trimmed = value.trim();
224
- return (
225
- Boolean(trimmed) &&
226
- !trimmed.includes("/") &&
227
- !trimmed.startsWith("~") &&
228
- !trimmed.includes("{")
409
+ const candidates = recipeReferenceCandidates(
410
+ value,
411
+ baseDir,
412
+ EMPTY_ACTIVE_SKILL_RECIPE_CONTEXT,
229
413
  );
230
- }
231
-
232
- function recipeNameFiles(value: string): string[] {
233
- const trimmed = value.trim();
234
- if (trimmed.endsWith(".json") || trimmed.endsWith(".md")) return [trimmed];
235
- return [`${trimmed}.json`, `${trimmed}.md`];
236
- }
237
-
238
- function recipeCandidatePaths(
239
- value: string,
240
- currentRecipeRoot: string,
241
- ): string[] {
242
- const qualified = qualifiedRecipeCandidates(value.trim());
243
- if (qualified) return qualified;
244
- if (!isBareRecipeName(value))
245
- return [resolveRecipePath(value, currentRecipeRoot)];
246
- const roots = [
247
- Paths.getRecipeRoot(),
248
- currentRecipeRoot,
249
- Paths.getPackagedRecipeRoot(),
250
- ];
251
- return [
252
- ...new Set(
253
- roots.flatMap((root) =>
254
- recipeNameFiles(value).map((file) => resolve(root, file)),
255
- ),
256
- ),
257
- ];
414
+ if (!candidates || candidates.length !== 1) {
415
+ throw new Error(
416
+ `Recipe file reference must be an explicit .json or .md path: ${value}`,
417
+ );
418
+ }
419
+ return candidates[0];
258
420
  }
259
421
 
260
422
  function resolveRecipeImportPath(
261
423
  value: string,
262
- currentRecipeRoot: string,
424
+ recipeDir: string,
425
+ context: ActiveSkillRecipeContext,
263
426
  ): string {
264
- const candidates = recipeCandidatePaths(value, currentRecipeRoot);
427
+ const candidates = recipeReferenceCandidates(value, recipeDir, context);
428
+ if (!candidates) {
429
+ throw new Error(
430
+ `Recipe import must use <skill>/<recipe> or an explicit .json/.md file path: ${value}`,
431
+ );
432
+ }
265
433
  return candidates.find((candidate) => existsSync(candidate)) ?? candidates[0];
266
434
  }
267
435
 
268
436
  export function resolveRecipeReferencePath(
269
437
  value: unknown,
270
- currentRecipeRoot = Paths.getRecipeRoot(),
438
+ baseDir = Paths.getRecipeRoot(),
439
+ context = EMPTY_ACTIVE_SKILL_RECIPE_CONTEXT,
271
440
  ): string | undefined {
272
441
  if (typeof value !== "string") return undefined;
273
- const trimmed = value.trim();
274
- if (!trimmed || hasWhitespace(trimmed)) return undefined;
275
- for (const path of recipeCandidatePaths(trimmed, currentRecipeRoot)) {
276
- if (!existsSync(path)) continue;
277
- try {
278
- const raw = readRawRecipeConfig(path);
279
- if (raw && typeof raw === "object" && Object.hasOwn(raw, "template"))
280
- return path;
281
- return path;
282
- } catch {
283
- return path;
284
- }
285
- }
286
- return undefined;
442
+ const candidates = recipeReferenceCandidates(value, baseDir, context);
443
+ return candidates?.find((candidate) => existsSync(candidate));
287
444
  }
288
445
 
289
446
  export function getRecipePath(
290
447
  value: unknown,
291
- recipeRoot = Paths.getRecipeRoot(),
448
+ baseDir = Paths.getRecipeRoot(),
449
+ context = EMPTY_ACTIVE_SKILL_RECIPE_CONTEXT,
292
450
  ): string | undefined {
293
- if (typeof value !== "string") return undefined;
294
- const trimmed = value.trim();
295
- if (!trimmed || hasWhitespace(trimmed)) return undefined;
296
- if (trimmed.endsWith(".json") || trimmed.endsWith(".md"))
297
- return resolveRecipePath(trimmed, recipeRoot);
298
- return resolveRecipeReferencePath(trimmed, recipeRoot);
451
+ return resolveRecipeReferencePath(value, baseDir, context);
299
452
  }
300
453
 
301
454
  function isImportNode(value: unknown): boolean {
@@ -873,7 +1026,11 @@ function loadDelegatedRecipe(
873
1026
  stack: string[],
874
1027
  options: ReadResolvedRecipeConfigOptions,
875
1028
  ): TemplateRecipeConfig | undefined {
876
- const path = resolveRecipeReferencePath(value, dirname(currentRecipeFile));
1029
+ const path = resolveRecipeReferencePath(
1030
+ value,
1031
+ dirname(currentRecipeFile),
1032
+ options.skillContext,
1033
+ );
877
1034
  if (!path) return undefined;
878
1035
  const config = readResolvedRecipeConfig(
879
1036
  path,
@@ -991,6 +1148,46 @@ function getDirectDelegatedRecipe(
991
1148
  return undefined;
992
1149
  }
993
1150
 
1151
+ function resolveDirectDelegationArgs(
1152
+ delegated: TemplateRecipeConfig,
1153
+ wrapperArgs: unknown,
1154
+ wrapperDefaults: Record<string, unknown> | undefined,
1155
+ ): string[] | undefined {
1156
+ if (!Array.isArray(delegated.args))
1157
+ return Array.isArray(wrapperArgs) ? wrapperArgs.map(String) : undefined;
1158
+ const delegatedSpec = Schema.parseToolArgDeclarationList(delegated.args);
1159
+ if (delegatedSpec.error) throw new Error(delegatedSpec.error);
1160
+ const wrapperDeclarations = Array.isArray(wrapperArgs)
1161
+ ? wrapperArgs.map(String)
1162
+ : [];
1163
+ const wrapperSpec = Schema.parseToolArgDeclarationList(wrapperDeclarations);
1164
+ if (wrapperSpec.error) throw new Error(wrapperSpec.error);
1165
+ const declared = new Set(delegatedSpec.args);
1166
+ for (const arg of wrapperSpec.args) {
1167
+ if (!declared.has(arg))
1168
+ throw new Error(`Unknown delegated Recipe argument: ${arg}`);
1169
+ }
1170
+ Schema.assertCompatibleToolArgTypes(
1171
+ delegatedSpec.argTypes,
1172
+ wrapperSpec.argTypes,
1173
+ );
1174
+ assertRecipeDefaultsDeclared(
1175
+ delegated.args,
1176
+ wrapperDefaults,
1177
+ "delegated Recipe default",
1178
+ );
1179
+ Schema.normalizeRuntimeValues(
1180
+ Object.fromEntries(
1181
+ Object.entries(wrapperDefaults ?? {}).filter(
1182
+ ([, value]) =>
1183
+ typeof value !== "string" || !/\{[^{}]+\}/.test(value),
1184
+ ),
1185
+ ),
1186
+ delegatedSpec.argTypes,
1187
+ );
1188
+ return delegated.args;
1189
+ }
1190
+
994
1191
  function expandImportNodes(
995
1192
  value: CommandTemplateValue,
996
1193
  imports: Record<string, ImportedRecipe>,
@@ -1080,8 +1277,12 @@ export function readResolvedRecipeConfig(
1080
1277
  `Recipe import depth exceeds limit ${MAX_RECIPE_IMPORT_DEPTH}: ${[...stack, path].join(" -> ")}`,
1081
1278
  );
1082
1279
  }
1083
- const raw = readRawRecipeConfig(path);
1280
+ const raw =
1281
+ options.authoredEntry?.path === path
1282
+ ? options.authoredEntry.recipe
1283
+ : readRawRecipeConfig(path);
1084
1284
  if (!raw || !Object.hasOwn(raw, "template")) return undefined;
1285
+ assertRecipeHasNoDeclaredName(raw);
1085
1286
  RecipeControl.assertRecipeHasNoMailbox(raw);
1086
1287
  assertRuntimeRecipeValuesNotDeclared(raw);
1087
1288
  assertRecipeArgumentContract(raw as unknown as TemplateRecipeDefinition);
@@ -1090,6 +1291,7 @@ export function readResolvedRecipeConfig(
1090
1291
  const importPath = resolveRecipeImportPath(
1091
1292
  getImportFrom(binding),
1092
1293
  dirname(path),
1294
+ options.skillContext ?? EMPTY_ACTIVE_SKILL_RECIPE_CONTEXT,
1093
1295
  );
1094
1296
  const config = readResolvedRecipeConfig(
1095
1297
  importPath,
@@ -1109,7 +1311,7 @@ export function readResolvedRecipeConfig(
1109
1311
  imports[alias] = {
1110
1312
  alias,
1111
1313
  file: importPath,
1112
- name: config.name ?? alias,
1314
+ name: getRecipeIdFromPath(importPath),
1113
1315
  config,
1114
1316
  defaults: { ...(config.defaults ?? {}), ...(bindingDefaults ?? {}) },
1115
1317
  values: {
@@ -1165,10 +1367,15 @@ export function readResolvedRecipeConfig(
1165
1367
  role: stack.length > 0 ? "import" : "entry",
1166
1368
  })
1167
1369
  : expandedTemplate;
1168
- const mergedDefaults = mergeDefaults(
1169
- delegated?.defaults,
1170
- isRecord(substituted.defaults) ? substituted.defaults : undefined,
1171
- );
1370
+ const wrapperDefaults = isRecord(substituted.defaults)
1371
+ ? substituted.defaults
1372
+ : undefined;
1373
+ const mergedDefaults = mergeDefaults(delegated?.defaults, wrapperDefaults);
1374
+ const effectiveArgs = delegated
1375
+ ? resolveDirectDelegationArgs(delegated, substituted.args, wrapperDefaults)
1376
+ : Array.isArray(substituted.args)
1377
+ ? (substituted.args as string[])
1378
+ : undefined;
1172
1379
  const artifactSource = isRecord(substituted.artifacts)
1173
1380
  ? substituted.artifacts
1174
1381
  : delegated?.artifacts;
@@ -1205,11 +1412,7 @@ export function readResolvedRecipeConfig(
1205
1412
  ? { imports: getRecipeImports(raw) }
1206
1413
  : {}),
1207
1414
  template: templateWithContext,
1208
- ...(Array.isArray(substituted.args)
1209
- ? { args: substituted.args as string[] }
1210
- : Array.isArray(delegated?.args)
1211
- ? { args: delegated.args }
1212
- : {}),
1415
+ ...(effectiveArgs ? { args: effectiveArgs } : {}),
1213
1416
  ...(mergedDefaults ? { defaults: mergedDefaults } : {}),
1214
1417
  ...(typeof substituted.parallel === "boolean"
1215
1418
  ? { parallel: substituted.parallel }
@@ -1286,6 +1489,7 @@ function collectRecipeContextRecords(
1286
1489
  file: string,
1287
1490
  stack: string[],
1288
1491
  importPath: string[],
1492
+ context: ActiveSkillRecipeContext,
1289
1493
  alias?: string,
1290
1494
  ): TemplateRecipeContextRecord[] {
1291
1495
  const path = resolveRecipePath(
@@ -1302,27 +1506,35 @@ function collectRecipeContextRecords(
1302
1506
  }
1303
1507
  const raw = readRawRecipeConfig(path);
1304
1508
  if (!raw || !Object.hasOwn(raw, "template")) return [];
1305
- const qualifiedName = qualifiedRecipeNameForFile(path);
1509
+ assertRecipeHasNoDeclaredName(raw);
1510
+ const qualifiedName = qualifiedRecipeNameForFile(path, context);
1511
+ const skill = qualifiedName?.split("/", 1)[0];
1306
1512
  const record: TemplateRecipeContextRecord = {
1307
1513
  ...(alias ? { alias } : {}),
1308
1514
  depth: stack.length,
1309
- file: path,
1310
1515
  import_path: importPath,
1516
+ logical_reference: qualifiedName ?? basename(path),
1311
1517
  name: getRecipeIdFromPath(path),
1312
- ...(qualifiedName ? { qualified_name: qualifiedName } : {}),
1313
1518
  recipe: raw,
1314
1519
  role: stack.length === 0 ? "entry" : "import",
1520
+ ...(skill ? { skill } : {}),
1521
+ source_file: path,
1522
+ source_kind: qualifiedName
1523
+ ? "active_skill_component"
1524
+ : "explicit_file_recipe",
1315
1525
  };
1316
1526
  const imports = getRecipeImports(raw);
1317
1527
  const children = Object.entries(imports).flatMap(([childAlias, binding]) => {
1318
1528
  const importFile = resolveRecipeImportPath(
1319
1529
  getImportFrom(binding),
1320
1530
  dirname(path),
1531
+ context,
1321
1532
  );
1322
1533
  return collectRecipeContextRecords(
1323
1534
  importFile,
1324
1535
  [...stack, path],
1325
1536
  [...importPath, childAlias],
1537
+ context,
1326
1538
  childAlias,
1327
1539
  );
1328
1540
  });
@@ -1331,8 +1543,9 @@ function collectRecipeContextRecords(
1331
1543
 
1332
1544
  export function buildRecipeContextRecords(
1333
1545
  file: string,
1546
+ context = EMPTY_ACTIVE_SKILL_RECIPE_CONTEXT,
1334
1547
  ): TemplateRecipeContextRecord[] {
1335
- return collectRecipeContextRecords(file, [], []);
1548
+ return collectRecipeContextRecords(file, [], [], context);
1336
1549
  }
1337
1550
 
1338
1551
  export function getRecipeTemplate(