@llblab/pi-actors 0.57.0 → 0.58.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 (47) hide show
  1. package/AGENTS.md +9 -1
  2. package/BACKLOG.md +14 -1
  3. package/CHANGELOG.md +11 -0
  4. package/README.md +9 -1
  5. package/dist/lib/file-state.d.ts +1 -0
  6. package/dist/lib/file-state.js +8 -2
  7. package/dist/lib/prompts.d.ts +2 -1
  8. package/dist/lib/prompts.js +6 -2
  9. package/dist/lib/recipes-discovery.d.ts +4 -0
  10. package/dist/lib/recipes-discovery.js +22 -0
  11. package/dist/lib/recipes-references.d.ts +1 -0
  12. package/dist/lib/recipes-references.js +1 -1
  13. package/dist/lib/recipes-usage.js +12 -9
  14. package/dist/lib/registry.d.ts +5 -1
  15. package/dist/lib/registry.js +199 -7
  16. package/dist/lib/run-delivery.d.ts +1 -1
  17. package/dist/lib/run-delivery.js +146 -32
  18. package/dist/lib/tools-inspect.js +6 -3
  19. package/dist/lib/tools-register.js +1 -0
  20. package/dist/lib/tools-response.js +6 -1
  21. package/dist/skills/actors/SKILL.md +11 -2
  22. package/dist/skills/actors/references/persistent-tools.md +20 -2
  23. package/dist/skills/actors/references/runs.md +1 -1
  24. package/dist/skills/recipe-memory/SKILL.md +6 -0
  25. package/docs/README.md +1 -0
  26. package/docs/agent-experience.md +42 -3
  27. package/docs/capability-memory.md +166 -0
  28. package/docs/coordinator-delivery.md +2 -2
  29. package/docs/inspection.md +3 -1
  30. package/docs/output-projection-map.md +4 -4
  31. package/docs/releasing.md +4 -0
  32. package/docs/tool-registry.md +20 -1
  33. package/lib/file-state.ts +7 -3
  34. package/lib/prompts.ts +7 -2
  35. package/lib/recipes-discovery.ts +26 -0
  36. package/lib/recipes-references.ts +1 -1
  37. package/lib/recipes-usage.ts +11 -8
  38. package/lib/registry.ts +195 -10
  39. package/lib/run-delivery.ts +130 -31
  40. package/lib/tools-inspect.ts +6 -3
  41. package/lib/tools-register.ts +1 -0
  42. package/lib/tools-response.ts +8 -1
  43. package/package.json +1 -1
  44. package/skills/actors/SKILL.md +11 -2
  45. package/skills/actors/references/persistent-tools.md +20 -2
  46. package/skills/actors/references/runs.md +1 -1
  47. package/skills/recipe-memory/SKILL.md +6 -0
@@ -3,8 +3,8 @@
3
3
  * Zones: registry mutations, persistence, runtime activation
4
4
  * Owns register/update/delete validation, persistence, runtime side effects, and result payloads
5
5
  */
6
- import { existsSync, mkdirSync, readFileSync, unlinkSync } from "node:fs";
7
- import { dirname, join, relative, resolve } from "node:path";
6
+ import { existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, unlinkSync } from "node:fs";
7
+ import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
8
8
  import * as CommandTemplates from "./command-templates.js";
9
9
  import * as Config from "./config.js";
10
10
  import * as ExecutionOutput from "./execution-output.js";
@@ -560,6 +560,14 @@ function executeRegisterToolUnlocked(params, ctx, deps) {
560
560
  ? params
561
561
  : {};
562
562
  assertRegisterToolInputModes(rawInput);
563
+ if (Object.hasOwn(rawInput, "expose")) {
564
+ const exposure = rawInput.expose;
565
+ if (typeof exposure !== "boolean")
566
+ throw new RetainedRecipeRefusal("expose", "expose must be Boolean.");
567
+ // Select under the existing name reservation; acquire retention locks only after it releases.
568
+ if (exposure === false)
569
+ return () => retainRecipe(rawInput, ctx, deps);
570
+ }
563
571
  let input = rawInput;
564
572
  if (!input.name)
565
573
  return listTools(deps);
@@ -702,12 +710,196 @@ function executeRegisterToolUnlocked(params, ctx, deps) {
702
710
  },
703
711
  };
704
712
  }
713
+ class RetainedRecipeRefusal extends Error {
714
+ reason;
715
+ constructor(field, reason, cause) {
716
+ super(ExecutionOutput.formatToolText(registryMetadataText({ field, result: reason })), { cause });
717
+ this.reason = reason;
718
+ }
719
+ }
720
+ function retainedDirectory(path) {
721
+ const stat = lstatSync(path, { bigint: true });
722
+ if (!stat.isDirectory() || stat.isSymbolicLink()) {
723
+ throw new RetainedRecipeRefusal("storage", "Retained storage must be a regular non-symlink directory.");
724
+ }
725
+ return { path, realpath: realpathSync.native(path), dev: stat.dev, ino: stat.ino };
726
+ }
727
+ function recheckRetainedDirectory(frame) {
728
+ const current = retainedDirectory(frame.path);
729
+ if (current.realpath !== frame.realpath || current.dev !== frame.dev || current.ino !== frame.ino) {
730
+ throw new RetainedRecipeRefusal("storage", "Retained storage identity changed.");
731
+ }
732
+ }
733
+ function retainedSlotAbsent(path, ownTargetPublished = false) {
734
+ for (const candidate of ownTargetPublished ? [path.replace(/\.json$/u, ".md")] : [path, path.replace(/\.json$/u, ".md")]) {
735
+ try {
736
+ lstatSync(candidate);
737
+ }
738
+ catch (error) {
739
+ let code;
740
+ try {
741
+ code = error?.code;
742
+ }
743
+ catch (observationError) {
744
+ throw new AggregateError([error, observationError], "Retained slot observation failed.", { cause: error });
745
+ }
746
+ if (code === "ENOENT")
747
+ continue;
748
+ throw error;
749
+ }
750
+ throw new RetainedRecipeRefusal("name", "Retained Recipe already exists; this path is create-only.");
751
+ }
752
+ }
753
+ function retainedFile(path) {
754
+ const stat = lstatSync(path, { bigint: true });
755
+ if (!stat.isFile() || stat.isSymbolicLink()) {
756
+ throw new RetainedRecipeRefusal("storage", "Retained target must be a regular non-symlink file.");
757
+ }
758
+ if (stat.size > BigInt(RecipesReferences.MAX_RECIPE_FILE_BYTES))
759
+ throw new RetainedRecipeRefusal("storage", "Retained target exceeds the source size limit.");
760
+ return { dev: stat.dev, ino: stat.ino, size: stat.size, mtime: stat.mtimeNs, ctime: stat.ctimeNs };
761
+ }
762
+ function withinRetainedSource(root, path) {
763
+ const relation = relative(resolve(root), resolve(path));
764
+ return relation === "" || (!isAbsolute(relation) && !/^\.\.(?:[\\/]|$)/u.test(relation));
765
+ }
766
+ function assertRetainedSourceLifetime(source, root, field) {
767
+ const drafts = join(root, "drafts");
768
+ if (withinRetainedSource(drafts, source) ||
769
+ (existsSync(drafts) && existsSync(source) && withinRetainedSource(realpathSync.native(drafts), realpathSync.native(source)))) {
770
+ throw new RetainedRecipeRefusal(field, "Retained creation cannot reference a managed disposable draft.");
771
+ }
772
+ }
773
+ function assertRetainedTemplateLifetimes(value, path, root, context) {
774
+ if (typeof value === "string") {
775
+ const source = RecipesReferences.resolveRecipeReferencePath(value, dirname(path), context.activeSkills);
776
+ if (source)
777
+ assertRetainedSourceLifetime(source, root, "template");
778
+ }
779
+ else if (Array.isArray(value)) {
780
+ for (const child of value)
781
+ assertRetainedTemplateLifetimes(child, path, root, context);
782
+ }
783
+ else if (value && typeof value === "object") {
784
+ const node = value;
785
+ assertRetainedTemplateLifetimes(node.template, path, root, context);
786
+ assertRetainedTemplateLifetimes(node.recover, path, root, context);
787
+ }
788
+ }
789
+ function retainRecipe(input, ctx, deps) {
790
+ assertRegisterToolInputModes(input);
791
+ if (typeof input.name !== "string" || !input.name.trim()) {
792
+ throw new RetainedRecipeRefusal("name", "Retained creation requires a name.");
793
+ }
794
+ const name = Identity.normalizeToolName(input.name);
795
+ if (!name || deps.reservedToolNames.has(name)) {
796
+ throw new RetainedRecipeRefusal("name", "Invalid or reserved retained Recipe name.");
797
+ }
798
+ const blocker = deps.getToolNameBlocker(name);
799
+ if (blocker)
800
+ throw new Error(ExecutionOutput.formatToolText(blocker));
801
+ if (input.update || Object.hasOwn(input, "draft")) {
802
+ throw new RetainedRecipeRefusal("expose", "Retained creation does not support update or draft promotion.");
803
+ }
804
+ const template = getInputTemplate(input.template);
805
+ if (template === null || template === "") {
806
+ throw new RetainedRecipeRefusal("template", "Retained deletion is not supported.");
807
+ }
808
+ if (template === undefined && !input.from) {
809
+ throw new RetainedRecipeRefusal("source", "Retained creation requires template or from.");
810
+ }
811
+ const root = resolve(getRecipeRoot(deps)), library = join(root, "library"), path = join(library, `${name}.json`);
812
+ const context = getRegistrationResolutionContext(ctx, deps);
813
+ const from = input.from === undefined ? undefined : normalizeFromReference(input.from, context);
814
+ if (from) {
815
+ const source = from.endsWith(".json") || from.endsWith(".md") ? from
816
+ : RecipesReferences.resolveRecipeReferencePath(from, context.cwd, context.activeSkills);
817
+ if (source)
818
+ assertRetainedSourceLifetime(source, root, "from");
819
+ }
820
+ const authored = buildAuthoredRecipe({ ...input, ...(from ? { from } : {}) }, undefined, undefined);
821
+ if (!from)
822
+ assertRetainedTemplateLifetimes(authored.template, path, root, context);
823
+ const bytes = `${JSON.stringify(authored, null, 2)}\n`;
824
+ if (Buffer.byteLength(bytes) > RecipesReferences.MAX_RECIPE_FILE_BYTES) {
825
+ throw new RetainedRecipeRefusal("source", "Retained Recipe exceeds the 1 MiB source limit.");
826
+ }
827
+ const candidate = RecipesDiscovery.admitUserRecipe(path, context, authored);
828
+ if (!candidate.validated || !candidate.tool) {
829
+ throw new RetainedRecipeRefusal("source", "Retained Recipe admission failed; check the source and caller argument contract.", candidate.diagnostics);
830
+ }
831
+ let stage = "storage";
832
+ try {
833
+ return withFileMutationLock(root, () => {
834
+ mkdirSync(root, { recursive: true });
835
+ const rootFrame = retainedDirectory(root);
836
+ return withFileMutationLock(library, () => {
837
+ recheckRetainedDirectory(rootFrame);
838
+ mkdirSync(library, { recursive: true });
839
+ const libraryFrame = retainedDirectory(library);
840
+ const relation = relative(rootFrame.realpath, libraryFrame.realpath);
841
+ if ((process.platform === "win32" ? relation.toLowerCase() : relation) !== "library") {
842
+ throw new RetainedRecipeRefusal("storage", "Retained library escapes its Recipe root.");
843
+ }
844
+ const recheck = () => { recheckRetainedDirectory(rootFrame); recheckRetainedDirectory(libraryFrame); };
845
+ return withFileMutationLock(path, () => {
846
+ recheck();
847
+ retainedSlotAbsent(path);
848
+ stage = "persistence";
849
+ writeTextAtomic(path, bytes, { exclusive: true, onBeforeReplace: () => { recheck(); retainedSlotAbsent(path); } });
850
+ stage = "persisted_admission";
851
+ recheck();
852
+ retainedSlotAbsent(path, true);
853
+ const before = retainedFile(path);
854
+ if (readFileSync(path, "utf8") !== bytes)
855
+ throw new RetainedRecipeRefusal("storage", "Persisted retained content changed.");
856
+ const admitted = RecipesDiscovery.admitUserRecipe(path, context);
857
+ if (!admitted.validated || !admitted.tool)
858
+ throw new RetainedRecipeRefusal("source", "Persisted retained Recipe admission failed.", admitted.diagnostics);
859
+ const after = retainedFile(path);
860
+ if (Object.keys(before).some(key => before[key] !== after[key]) ||
861
+ readFileSync(path, "utf8") !== bytes)
862
+ throw new RetainedRecipeRefusal("storage", "Persisted retained content changed.");
863
+ recheck();
864
+ retainedSlotAbsent(path, true);
865
+ stage = "receipt";
866
+ const args = RecipesDiscovery.summarizeRegisteredToolArgs(admitted.tool);
867
+ const source = from ? from.endsWith(".json") || from.endsWith(".md") ? `<explicit-file:${basename(from)}>` : from : "template";
868
+ const recipe = registrySelected("recipe", path), selectedName = registrySelected("tool", name);
869
+ const selectedSource = registrySelected("source", source);
870
+ const required = ToolsResponse.selectYamlField("required_args", args.required, REGISTRY_METADATA_BUDGET);
871
+ const optional = ToolsResponse.selectYamlField("optional_args", args.optional, REGISTRY_METADATA_BUDGET);
872
+ const facts = { kind: "retained_recipe", result: "retained", persisted: true, resolved: true,
873
+ validated: true, activation: "not_requested", async: admitted.async };
874
+ const text = growRegistryDocument(() => ({ ...selectedName.selected, ...facts, ...recipe.selected, ...selectedSource.selected,
875
+ ...required.selected, ...optional.selected, next: { tool: "inspect", target: "recipes", view: "summary", verbose: true } }), [recipe, selectedName, selectedSource, required, optional]);
876
+ const result = {
877
+ content: [textContent(ExecutionOutput.formatToolText(text))],
878
+ details: { ...facts, tool: name, recipe: path, source, required_args: args.required, optional_args: args.optional,
879
+ next_actions: ["inspect target=recipes view=summary verbose=true"] },
880
+ };
881
+ stage = "release";
882
+ return result;
883
+ });
884
+ });
885
+ });
886
+ }
887
+ catch (error) {
888
+ const reason = error instanceof RetainedRecipeRefusal ? { reason: error.reason } : {};
889
+ throw new Error(ExecutionOutput.formatToolText(registryMetadataText({ operation: "retain", stage, result: "unconfirmed",
890
+ effects: "unknown; inspect retained inventory before retry", ...reason,
891
+ next: { tool: "inspect", target: "recipes", view: "summary", verbose: true } })), { cause: error });
892
+ }
893
+ }
705
894
  export async function executeRegisterTool(params, ctx, deps) {
706
895
  const input = params;
896
+ let result;
707
897
  if (!input.name)
708
- return executeRegisterToolUnlocked(params, ctx, deps);
709
- const name = Identity.normalizeToolName(input.name);
710
- if (!name)
711
- return executeRegisterToolUnlocked(params, ctx, deps);
712
- return withFileMutationLock(getToolRecipePath(deps, name), () => executeRegisterToolUnlocked(params, ctx, deps));
898
+ result = executeRegisterToolUnlocked(params, ctx, deps);
899
+ else {
900
+ const name = Identity.normalizeToolName(input.name);
901
+ result = !name ? executeRegisterToolUnlocked(params, ctx, deps)
902
+ : withFileMutationLock(getToolRecipePath(deps, name), () => executeRegisterToolUnlocked(params, ctx, deps));
903
+ }
904
+ return typeof result === "function" ? result() : result;
713
905
  }
@@ -66,7 +66,7 @@ export interface RunCompletionBatch {
66
66
  /** Retained singleton capture; absence preserves older indexed bytes. Never a new-capture selector. */
67
67
  readonly single_result?: true;
68
68
  /** Captured record-list shape; no public format/version choice or source authority. */
69
- readonly result_layout?: "records" | "run_headers" | "headings" | "headings_flat_omissions" | "headings_outcomes" | "headings_shared_results" | "headings_shared_kinds" | "frontmatter_v1" | "frontmatter_v2" | "completion_yaml_core_v1";
69
+ readonly result_layout?: "records" | "run_headers" | "headings" | "headings_flat_omissions" | "headings_outcomes" | "headings_shared_results" | "headings_shared_kinds" | "frontmatter_v1" | "frontmatter_v2" | "completion_yaml_core_v1" | "completion_yaml_files_v1";
70
70
  /** Immutable scope/order/range data, not a caller-selected format or allocation authority. */
71
71
  readonly numbering?: RunCompletionNumbering;
72
72
  /** Formatting version/data only; no recipient ownership, read or publication capability. */
@@ -278,8 +278,11 @@ function parseInventorySource(value, batch) {
278
278
  function isFrontmatterCompletionLayout(layout) {
279
279
  return layout === "frontmatter_v1" || layout === "frontmatter_v2";
280
280
  }
281
+ function isYamlCompletionLayout(layout) {
282
+ return layout === "completion_yaml_core_v1" || layout === "completion_yaml_files_v1";
283
+ }
281
284
  function isSharedCompletionLayout(layout) {
282
- return layout === "headings_shared_results" || layout === "headings_shared_kinds" || layout === "completion_yaml_core_v1" || isFrontmatterCompletionLayout(layout);
285
+ return layout === "headings_shared_results" || layout === "headings_shared_kinds" || isYamlCompletionLayout(layout) || isFrontmatterCompletionLayout(layout);
283
286
  }
284
287
  function parseBatch(value) {
285
288
  const record = asRecord(value, "Completion batch");
@@ -322,7 +325,7 @@ function parseBatch(value) {
322
325
  throw new Error("Completion shared text budget is invalid");
323
326
  }
324
327
  const numberingDescriptor = Object.getOwnPropertyDescriptor(record, "numbering");
325
- if (numberingDescriptor && (resultLayout === "completion_yaml_core_v1" || !Object.hasOwn(numberingDescriptor, "value") || presentationFormat !== "ax_records" || (resultLayout !== "headings" && resultLayout !== "headings_flat_omissions" && resultLayout !== "headings_outcomes" && !isSharedCompletionLayout(resultLayout)))) {
328
+ if (numberingDescriptor && (isYamlCompletionLayout(resultLayout) || !Object.hasOwn(numberingDescriptor, "value") || presentationFormat !== "ax_records" || (resultLayout !== "headings" && resultLayout !== "headings_flat_omissions" && resultLayout !== "headings_outcomes" && !isSharedCompletionLayout(resultLayout)))) {
326
329
  throw new Error("Completion numbering requires a canonical captured heading layout");
327
330
  }
328
331
  const numbering = numberingDescriptor ? parseCompletionNumbering(numberingDescriptor.value, members.length) : undefined;
@@ -685,7 +688,7 @@ export function requestRunCompletionStream(input) {
685
688
  });
686
689
  }
687
690
  export function admitRunCompletionBatch(input) {
688
- return captureCompletionBatch(input, false, "completion_yaml_core_v1");
691
+ return captureCompletionBatch(input, false, "completion_yaml_files_v1");
689
692
  }
690
693
  /** Kernel-only opt-in; ordinary Pi capture stays fenced until session/lifecycle integration is admitted. */
691
694
  export function captureRunCompletionStreamBatch(input) {
@@ -693,7 +696,7 @@ export function captureRunCompletionStreamBatch(input) {
693
696
  }
694
697
  /** Private ordinary candidate composition; stream capture retains its separately gated codec. */
695
698
  export function captureRunCompletionYamlCandidateBatch(input) {
696
- return captureCompletionBatch(input, false, "completion_yaml_core_v1");
699
+ return captureCompletionBatch(input, false, "completion_yaml_files_v1");
697
700
  }
698
701
  function captureCompletionBatch(input, numbered, layout = "frontmatter_v2") {
699
702
  const now = (input.now ?? new Date()).toISOString();
@@ -710,7 +713,7 @@ function captureCompletionBatch(input, numbered, layout = "frontmatter_v2") {
710
713
  members: input.members,
711
714
  phase: "pending",
712
715
  });
713
- const draft = layout === "completion_yaml_core_v1"
716
+ const draft = isYamlCompletionLayout(layout)
714
717
  ? parseBatch({ ...captured, result_layout: layout }) : { ...captured, result_layout: layout };
715
718
  return mutateJournal(input.tempDir, input.ownerId, (journal) => {
716
719
  let batch = draft;
@@ -741,10 +744,10 @@ function captureCompletionBatch(input, numbered, layout = "frontmatter_v2") {
741
744
  else if (journal.completion_stream) {
742
745
  throw new Error("Completion stream capture requires the numbered kernel path; stream state is retained");
743
746
  }
744
- if (layout !== "completion_yaml_core_v1")
747
+ if (!isYamlCompletionLayout(layout))
745
748
  batch = retainSharedCompletionTexts(batch);
746
749
  if (projectCompletionBatchMessage(batch).membersShown === batch.members.length) {
747
- if (layout === "completion_yaml_core_v1")
750
+ if (isYamlCompletionLayout(layout))
748
751
  batch = retainSharedCompletionTexts(batch);
749
752
  journal.completion_batch = batch;
750
753
  return batch;
@@ -1281,7 +1284,7 @@ function isRoutineCompletionSummary(member) {
1281
1284
  return member.summary === (member.status === "done" ? "Run completed." : `Run ${member.status}.`);
1282
1285
  }
1283
1286
  function completionRecordRow(member, index, byIdentity, flat = false, layout, textBudget) {
1284
- const frontmatter = isFrontmatterCompletionLayout(layout) || layout === "completion_yaml_core_v1";
1287
+ const frontmatter = isFrontmatterCompletionLayout(layout) || isYamlCompletionLayout(layout);
1285
1288
  const simpleKinds = frontmatter || layout === "headings_shared_kinds", shared = isSharedCompletionLayout(layout);
1286
1289
  const outcomes = shared || layout === "headings_outcomes", directOmission = outcomes || layout === "headings_flat_omissions";
1287
1290
  const run = member.run.startsWith("run:") ? member.run.slice(4) : member.run;
@@ -1604,8 +1607,97 @@ function completionYamlReferences(row) {
1604
1607
  })) };
1605
1608
  return technical;
1606
1609
  }
1610
+ /** Split selected paths without normalize/resolve/relative: dot and symlink routes must not be rewritten. */
1611
+ function rootedCompletionPath(path, runRoot) {
1612
+ if (typeof path !== "string" || !path.isWellFormed())
1613
+ return undefined;
1614
+ const windows = /^(?:[A-Za-z]:[\\/]|\\\\[^\\/]+[\\/][^\\/]+[\\/])/u.test(path);
1615
+ const separator = windows ? /[\\/]/u : /\//u;
1616
+ if (!windows && !path.startsWith("/"))
1617
+ return undefined;
1618
+ if (path.split(separator).some(part => part === "." || part === ".."))
1619
+ return undefined;
1620
+ if (runRoot && path.startsWith(runRoot)) {
1621
+ const tail = path.slice(runRoot.length);
1622
+ if (separator.test(runRoot.slice(-1)) ? tail.length > 0 : separator.test(tail[0] ?? "")) {
1623
+ const relative = separator.test(runRoot.slice(-1)) ? tail : tail.slice(1);
1624
+ if (relative && !separator.test(relative[0]))
1625
+ return { root: runRoot, path: relative };
1626
+ }
1627
+ }
1628
+ let split = -1;
1629
+ for (let i = 0; i < path.length; i++)
1630
+ if (separator.test(path[i]))
1631
+ split = i;
1632
+ if (split < 0 || split === path.length - 1)
1633
+ return undefined;
1634
+ const end = split === 0 || (windows && split === 2 && path[1] === ":") ? split + 1 : split;
1635
+ return { root: path.slice(0, end), path: path.slice(split + 1) };
1636
+ }
1637
+ /** Body-free, read-free navigation over already selected references. Roots are presentation bases, not containment proof. */
1638
+ function completionRootedFiles(row, member, locations) {
1639
+ const technical = completionYamlReferences(row), files = {};
1640
+ const groups = [];
1641
+ const runRoot = locations?.state === "available" ? locations.directory : undefined;
1642
+ const group = (root) => {
1643
+ const found = groups.findIndex(item => item.root === root);
1644
+ if (found >= 0)
1645
+ return found;
1646
+ groups.push({ root, entries: [] });
1647
+ return groups.length - 1;
1648
+ };
1649
+ let priority = Infinity;
1650
+ const add = (path, entry, rank = Infinity, base) => {
1651
+ const selected = rootedCompletionPath(path, runRoot);
1652
+ const index = group(selected?.root ?? (base === undefined ? null : completionYamlString(base)));
1653
+ const selectedPath = selected?.path ?? path;
1654
+ groups[index].entries.push({ ...entry, path: selectedPath,
1655
+ ...(!selected && base === undefined ? { path_basis: "as_recorded", base_unavailable: true } : {}) });
1656
+ if (selected && rank < priority) {
1657
+ priority = rank;
1658
+ files.read_first = { group: index, entry: groups[index].entries.length - 1 };
1659
+ }
1660
+ };
1661
+ for (const [role, purpose, rank] of [
1662
+ ["primary", "Full result text", 0], ["diagnostic_metadata", "Recorded error and command metadata", 1],
1663
+ ["artifact_inventory", "Declared artifact inventory; target availability unproven", 2],
1664
+ ]) {
1665
+ const detail = technical[role];
1666
+ if (!detail)
1667
+ continue;
1668
+ const { source: rawSource, ...facts } = detail;
1669
+ const { path, ...binding } = rawSource;
1670
+ add(path, { role, purpose, ...facts, source: binding }, rank);
1671
+ delete technical[role];
1672
+ }
1673
+ const artifacts = technical.artifacts;
1674
+ if (artifacts) {
1675
+ for (const item of artifacts.declarations)
1676
+ add(item.path, { role: "artifact", purpose: "Declared artifact", name: item.name, scope: artifacts.scope, state: "declared" });
1677
+ delete technical.artifacts;
1678
+ }
1679
+ if (locations?.state === "available") {
1680
+ const { directory, files: selectedFiles, ...observation } = locations;
1681
+ files.observation = { ...observation, run: completionYamlString(locations.run), run_instance_id: completionYamlString(locations.run_instance_id),
1682
+ group: group(completionYamlString(directory)) };
1683
+ for (const item of selectedFiles) {
1684
+ const { path, ...facts } = item;
1685
+ const purpose = item.role === "result" ? "Recorded result metadata" : item.role === "execution" ? "Recorded execution and capture locations" : "Recorded Run file";
1686
+ add(typeof path === "string" ? completionYamlString(path) : null, { ...facts, purpose }, item.state === "present" && item.role === "result" ? 3 : item.state === "present" && item.role === "execution" ? 4 : Infinity, directory);
1687
+ }
1688
+ }
1689
+ else if (locations)
1690
+ files.observation = { ...locations, run: completionYamlString(member.run), run_instance_id: completionYamlString(member.run_instance_id) };
1691
+ if (groups.length) {
1692
+ files.groups = groups;
1693
+ }
1694
+ if (Object.keys(technical).length)
1695
+ files.notes = technical;
1696
+ return files;
1697
+ }
1607
1698
  function prepareCompletionYamlProjection(safe, candidates, references, ordered) {
1608
1699
  const packet = { batchId: safe.batch_id, records: [] };
1700
+ const rooted = safe.result_layout === "completion_yaml_files_v1";
1609
1701
  if (!safe.batch_id.isWellFormed())
1610
1702
  return { state: "unsupported" };
1611
1703
  for (const [index, { row, location }] of candidates.entries()) {
@@ -1630,18 +1722,24 @@ function prepareCompletionYamlProjection(safe, candidates, references, ordered)
1630
1722
  else
1631
1723
  metadata[key] = completionYamlString(value);
1632
1724
  }
1633
- const technical = completionYamlReferences(row);
1634
- if (Object.keys(technical).length)
1635
- metadata.references = technical;
1636
- if (references.has(index) && location) {
1637
- const navigation = safe.file_navigation.find(item => safe.members[item.member_index] === member);
1638
- // Navigation is already decoded native data, not the JSON display field.
1639
- const locations = navigation.locations;
1640
- metadata.run_files = { run: completionYamlString(member.run), run_instance_id: completionYamlString(member.run_instance_id),
1641
- locations: locations.state === "available" ? { ...locations, run: completionYamlString(locations.run),
1642
- run_instance_id: completionYamlString(locations.run_instance_id), directory: completionYamlString(locations.directory),
1643
- files: locations.files.map(file => ({ ...file, ...(typeof file.path === "string" ? { path: completionYamlString(file.path) } : {}) })) }
1644
- : { ...locations } };
1725
+ const navigation = references.has(index) && location
1726
+ ? safe.file_navigation.find(item => safe.members[item.member_index] === member).locations : undefined;
1727
+ if (rooted) {
1728
+ const files = completionRootedFiles(row, member, navigation);
1729
+ if (Object.keys(files).length)
1730
+ metadata.files = files;
1731
+ }
1732
+ else {
1733
+ const technical = completionYamlReferences(row);
1734
+ if (Object.keys(technical).length)
1735
+ metadata.references = technical;
1736
+ if (navigation) {
1737
+ metadata.run_files = { run: completionYamlString(member.run), run_instance_id: completionYamlString(member.run_instance_id),
1738
+ locations: navigation.state === "available" ? { ...navigation, run: completionYamlString(navigation.run),
1739
+ run_instance_id: completionYamlString(navigation.run_instance_id), directory: completionYamlString(navigation.directory),
1740
+ files: navigation.files.map(file => ({ ...file, ...(typeof file.path === "string" ? { path: completionYamlString(file.path) } : {}) })) }
1741
+ : { ...navigation } };
1742
+ }
1645
1743
  }
1646
1744
  const parts = [];
1647
1745
  const add = (part) => { if (parts.length)
@@ -1650,13 +1748,22 @@ function prepareCompletionYamlProjection(safe, candidates, references, ordered)
1650
1748
  if (piece.block) {
1651
1749
  if (!piece.text.isWellFormed())
1652
1750
  return { state: "unsupported" };
1653
- add(piece.block === "body" ? { body: piece.text } : { marker: piece.text });
1751
+ if (rooted && piece.block === "marker" && !/^\[[0-9]+ characters omitted\]$/u.test(piece.text)) {
1752
+ metadata.text_warnings ??= [];
1753
+ metadata.text_warnings.push(piece.text);
1754
+ }
1755
+ else
1756
+ add(piece.block === "body" ? { body: piece.text } : { marker: piece.text });
1654
1757
  }
1655
- if (safe.file_navigation !== undefined && !references.has(index))
1656
- add({ marker: "Run-file reference omitted (packet byte limit)." });
1758
+ if (safe.file_navigation !== undefined && !references.has(index)) {
1759
+ if (rooted)
1760
+ metadata.run_files_omission = { reason: "packet_byte_limit", shown: 0, omitted: 1, total: 1 };
1761
+ else
1762
+ add({ marker: "Run-file reference omitted (packet byte limit)." });
1763
+ }
1657
1764
  packet.records.push({ metadata, parts });
1658
1765
  }
1659
- const service = {};
1766
+ const service = rooted ? { header_layout: "rooted_files_v1" } : {};
1660
1767
  const omitted = safe.members.length - candidates.length;
1661
1768
  if (omitted) {
1662
1769
  service.omitted_results = omitted;
@@ -1676,8 +1783,11 @@ function prepareCompletionYamlProjection(safe, candidates, references, ordered)
1676
1783
  if (safe.numbering.kind === "unavailable")
1677
1784
  service.numbering_unavailable = safe.numbering.reason;
1678
1785
  }
1679
- if (safe.inventory_source)
1680
- service.result_inventory = { ...safe.inventory_source, path: completionYamlString(safe.inventory_source.path) };
1786
+ if (safe.inventory_source) {
1787
+ const path = rooted ? rootedCompletionPath(safe.inventory_source.path) : undefined;
1788
+ service.result_inventory = { ...safe.inventory_source,
1789
+ ...(path ? { root: path.root, path: path.path } : { path: completionYamlString(safe.inventory_source.path) }) };
1790
+ }
1681
1791
  if (Object.keys(service).length)
1682
1792
  packet.service = service;
1683
1793
  const chunks = [];
@@ -1737,7 +1847,7 @@ export function projectRunCompletionYamlPrototype(batch) {
1737
1847
  throw error;
1738
1848
  }
1739
1849
  }
1740
- function projectCompletionRecords(safe, assembler = safe.result_layout === "completion_yaml_core_v1" ? "yaml" : "legacy") {
1850
+ function projectCompletionRecords(safe, assembler = isYamlCompletionLayout(safe.result_layout) ? "yaml" : "legacy") {
1741
1851
  const frontmatter = isFrontmatterCompletionLayout(safe.result_layout) || assembler === "yaml";
1742
1852
  const shared = isSharedCompletionLayout(safe.result_layout);
1743
1853
  const maxBytes = shared ? Limits.RUN_COMPLETION_MODEL_MAX_BYTES : Limits.RUN_DELIVERY_MODEL_MAX_BYTES;
@@ -2024,7 +2134,7 @@ export function projectRunCompletionBatchPresentation(batch) {
2024
2134
  if (safe.presentation_format !== "ax_records" && safe.presentation_format !== "ax_records_v3")
2025
2135
  return { content: projectCompletionBatchMessage(safe).message };
2026
2136
  const result = safe.result_layout !== undefined ? projectCompletionRecords(safe) : projectRetainedRecordCompletionBatch(safe);
2027
- if (safe.result_layout === "completion_yaml_core_v1")
2137
+ if (isYamlCompletionLayout(safe.result_layout))
2028
2138
  return { content: result.message, presentation: Object.freeze({
2029
2139
  format: "completion_yaml_core_v1", content_sha256: createHash("sha256").update(result.message).digest("hex"),
2030
2140
  }) };
@@ -2192,10 +2302,14 @@ export function readRunCompletionPresentation(content, value) {
2192
2302
  hint.content_sha256 !== createHash("sha256").update(content).digest("hex"))
2193
2303
  return full;
2194
2304
  const segments = [];
2305
+ const rooted = core.packet.service?.header_layout === "rooted_files_v1";
2306
+ const facts = Object.fromEntries(Object.entries(core.packet.service ?? {}).filter(([key]) => key !== "header_layout"));
2195
2307
  for (const [index, record] of core.packet.records.entries()) {
2196
2308
  if (index)
2197
2309
  segments.push({ kind: "separator", text: "\n" });
2198
- const metadata = Object.fromEntries(Object.entries(record.metadata).filter(([key]) => key !== "references" && key !== "run_files"));
2310
+ const metadata = Object.fromEntries(Object.entries(record.metadata).filter(([key]) => rooted || (key !== "references" && key !== "run_files")));
2311
+ if (rooted && index === 0 && Object.keys(facts).length)
2312
+ metadata.delivery = facts;
2199
2313
  const header = renderYamlMetadata(metadata, completionYamlMaxBytes);
2200
2314
  if (header.state !== "available")
2201
2315
  return full;
@@ -2203,13 +2317,13 @@ export function readRunCompletionPresentation(content, value) {
2203
2317
  for (const part of record.parts)
2204
2318
  segments.push("body" in part ? { kind: "body", text: part.body } : { kind: "marker", text: part.marker });
2205
2319
  }
2206
- if (core.packet.service) {
2207
- const service = renderYamlMetadata(core.packet.service, completionYamlMaxBytes);
2320
+ if (core.packet.service && (!rooted || !core.packet.records.length)) {
2321
+ const service = renderYamlMetadata(rooted ? facts : core.packet.service, completionYamlMaxBytes);
2208
2322
  if (service.state !== "available")
2209
2323
  return full;
2210
2324
  if (segments.length)
2211
2325
  segments.push({ kind: "separator", text: "\n" });
2212
- segments.push({ kind: "service", text: service.text.replace(/\n$/u, "") });
2326
+ segments.push({ kind: rooted ? "header" : "service", text: service.text.replace(/\n$/u, "") });
2213
2327
  }
2214
2328
  return { text: segments.map(segment => segment.text).join("\n"), compact: true, records: true, yaml: true,
2215
2329
  segments: Object.freeze(segments.map(segment => Object.freeze(segment))) };
@@ -364,9 +364,6 @@ function inspectRecipes(requestedView, input, deps, context) {
364
364
  if (view === "reviews") {
365
365
  return ReviewDiagnostics.readAutomaticReviewDiagnostics({ recipeRoot });
366
366
  }
367
- const discovered = RecipesDiscovery.discoverRecipeSources([
368
- { root: recipeRoot, defaultTool: true, mutableUsage: true },
369
- ]);
370
367
  const resolutionContext = context && typeof context === "object" && "recipeResolutionContext" in context
371
368
  ? context.recipeResolutionContext
372
369
  : undefined;
@@ -399,7 +396,11 @@ function inspectRecipes(requestedView, input, deps, context) {
399
396
  skill: diagnostic.skill,
400
397
  ...(diagnostic.stem ? { stem: diagnostic.stem } : {}),
401
398
  }));
399
+ const discovered = RecipesDiscovery.discoverRecipeSources([
400
+ { root: recipeRoot, defaultTool: true, mutableUsage: true, resolutionContext },
401
+ ]);
402
402
  const discoverySummary = RecipesDiscovery.summarizeDiscovery(discovered);
403
+ const retained = RecipesDiscovery.inventoryRetainedRecipes(recipeRoot, resolutionContext);
403
404
  const summary = {
404
405
  ...discoverySummary,
405
406
  ...(registryStatus
@@ -425,6 +426,8 @@ function inspectRecipes(requestedView, input, deps, context) {
425
426
  error: `Ambiguous active Skill Recipe namespace ${name}`,
426
427
  })),
427
428
  drafts: RecipesDiscovery.listDraftRecipes(join(recipeRoot, "drafts")),
429
+ retained: retained.recipes,
430
+ retained_diagnostics: retained.diagnostics,
428
431
  };
429
432
  return {
430
433
  ...summary,
@@ -26,6 +26,7 @@ export function createRegisterToolDefinition(deps) {
26
26
  description: stringSchema(Prompts.REGISTER_TOOL_PARAM_DESCRIPTIONS.description),
27
27
  defaults: looseObjectSchema(Prompts.REGISTER_TOOL_PARAM_DESCRIPTIONS.defaults),
28
28
  draft: stringSchema(Prompts.REGISTER_TOOL_PARAM_DESCRIPTIONS.draft),
29
+ expose: booleanSchema(Prompts.REGISTER_TOOL_PARAM_DESCRIPTIONS.expose),
29
30
  from: stringSchema(Prompts.REGISTER_TOOL_PARAM_DESCRIPTIONS.from),
30
31
  name: stringSchema(Prompts.REGISTER_TOOL_PARAM_DESCRIPTIONS.name),
31
32
  template: unionSchema([
@@ -512,6 +512,10 @@ export function recipeRegistryNextActions(summary, view) {
512
512
  if (summary.skill_recipe_catalog_partial === true) {
513
513
  actions.push("inspect target=recipes view=imports verbose=true");
514
514
  }
515
+ if ((Array.isArray(summary.retained) && summary.retained.length > 0) ||
516
+ (Array.isArray(summary.retained_diagnostics) && summary.retained_diagnostics.length > 0)) {
517
+ actions.push("inspect target=recipes view=summary verbose=true");
518
+ }
515
519
  if (drafts.length > 0) {
516
520
  actions.push("inspect target=recipes view=summary verbose=true");
517
521
  const firstPath = typeof drafts[0]?.path === "string" ? drafts[0].path : undefined;
@@ -555,6 +559,7 @@ export function compactRecipeRegistry(summary) {
555
559
  const active = count("active", summary.active), shadowed = count("shadowed", summary.shadowed);
556
560
  const invalid = count("invalid", summary.invalid), disabled = count("disabled", summary.disabled);
557
561
  const diagnostics = count("diagnostics", summary.diagnostics), drafts = count("drafts", summary.drafts);
562
+ const retained = count("retained", summary.retained);
558
563
  const recommendations = count("recommendations", summary.recommendations);
559
564
  const skillCatalogPartial = summary.skill_recipe_catalog_partial === true;
560
565
  const currentPolicy = Array.isArray(summary.active)
@@ -564,7 +569,7 @@ export function compactRecipeRegistry(summary) {
564
569
  ? summary.next_actions
565
570
  : [];
566
571
  const hint = nextActions.length ? nextActionMetadata(nextActions[0], Limits.RUN_DELIVERY_MODEL_MAX_BYTES) : undefined;
567
- const render = () => ({ recipes: "registry", active, drafts, shadowed, invalid, disabled,
572
+ const render = () => ({ recipes: "registry", active, drafts, retained, shadowed, invalid, disabled,
568
573
  current_policy: currentPolicy, skill_catalog_partial: skillCatalogPartial, recommendations, diagnostics,
569
574
  ...(hint ? hint.selected : {}) });
570
575
  return hint