@aefree/pi-plastic 0.7.6 → 0.8.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 (148) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/README.md +48 -174
  3. package/docs/2026-10-06-development.md +101 -0
  4. package/docs/2026-10-06-diffs-and-patches.md +136 -0
  5. package/docs/2026-10-06-discovery.md +135 -0
  6. package/docs/2026-10-06-integrations.md +49 -0
  7. package/docs/2026-10-06-merging.md +153 -0
  8. package/docs/2026-10-06-safety-and-receipts.md +95 -0
  9. package/docs/2026-10-06-setup.md +101 -0
  10. package/docs/2026-10-06-shelvesets-and-reviews.md +113 -0
  11. package/docs/2026-10-06-status.md +130 -0
  12. package/docs/2026-10-06-tools-and-loading.md +64 -0
  13. package/docs/2026-10-06-workspace-operations.md +142 -0
  14. package/docs/README.md +65 -0
  15. package/extensions/bash-cm-diff-guard.ts +1 -1
  16. package/index.ts +4 -687
  17. package/package.json +10 -4
  18. package/skills/using-plastic/SKILL.md +11 -5
  19. package/skills/using-plastic/references/branch-management.md +5 -1
  20. package/skills/using-plastic/references/changeset-operations.md +9 -8
  21. package/skills/using-plastic/references/code-review-creation.md +25 -18
  22. package/skills/using-plastic/references/integration.md +9 -5
  23. package/skills/using-plastic/references/quick-reference.md +4 -4
  24. package/skills/using-plastic/references/reviewing-changes.md +10 -10
  25. package/skills/using-plastic/references/shelving.md +26 -11
  26. package/skills/using-plastic/references/troubleshooting.md +4 -4
  27. package/src/diff/local-snapshot.ts +74 -0
  28. package/src/diff/text.ts +187 -0
  29. package/src/domain/add-contract.ts +24 -0
  30. package/src/domain/branch-create-contract.ts +38 -0
  31. package/src/domain/branch-delete-contract.ts +15 -0
  32. package/src/domain/branches.ts +263 -0
  33. package/src/domain/checkin-contract.ts +70 -0
  34. package/src/domain/closeout-contract.ts +30 -0
  35. package/src/domain/copied-merge.ts +42 -0
  36. package/src/domain/diff-base-xml.ts +65 -0
  37. package/src/domain/diff-pending-xml.ts +34 -0
  38. package/src/domain/merge-output.ts +327 -0
  39. package/src/domain/paths.ts +253 -0
  40. package/src/domain/pending.ts +499 -0
  41. package/src/domain/removal-contract.ts +25 -0
  42. package/src/domain/revisions.ts +301 -0
  43. package/src/domain/server-merge-contract.ts +45 -0
  44. package/src/domain/server-merge-resolutions.ts +61 -0
  45. package/src/domain/status-xml.ts +122 -0
  46. package/src/domain/switch-contract.ts +60 -0
  47. package/src/domain/switch-policy.ts +95 -0
  48. package/src/domain/undo-contract.ts +24 -0
  49. package/src/domain/update-contract.ts +16 -0
  50. package/src/domain/workspace-merge-contract.ts +41 -0
  51. package/src/execution/add-command.ts +118 -0
  52. package/src/execution/branch-create-command.ts +118 -0
  53. package/src/execution/branch-list-command.ts +75 -0
  54. package/src/execution/checkin-command.ts +116 -0
  55. package/src/execution/cli-version.ts +31 -0
  56. package/src/execution/cm.ts +59 -0
  57. package/src/execution/code-review-find-command.ts +75 -0
  58. package/src/execution/context.ts +20 -0
  59. package/src/execution/diff-revisions-command.ts +74 -0
  60. package/src/execution/process.ts +260 -0
  61. package/src/execution/removal-command.ts +118 -0
  62. package/src/execution/server-merge-command.ts +92 -0
  63. package/src/execution/shelveset-list-command.ts +75 -0
  64. package/src/execution/status-command.ts +29 -0
  65. package/src/execution/status-xml-command.ts +72 -0
  66. package/src/execution/status-xml-limits.ts +1 -0
  67. package/src/execution/switch-command.ts +118 -0
  68. package/src/execution/test-seams.ts +10 -0
  69. package/src/execution/undo-command.ts +118 -0
  70. package/src/execution/update-command.ts +118 -0
  71. package/src/execution/workspace-list-command.ts +75 -0
  72. package/src/execution/workspace-merge-command.ts +118 -0
  73. package/src/operations/add-receipt.ts +37 -0
  74. package/src/operations/arguments.ts +3 -0
  75. package/src/operations/branch-create.ts +74 -0
  76. package/src/operations/branch-delete-receipt.ts +25 -0
  77. package/src/operations/branch-list.ts +94 -0
  78. package/src/operations/branch-reads.ts +94 -0
  79. package/src/operations/branches.ts +111 -0
  80. package/src/operations/checkin-receipt.ts +140 -0
  81. package/src/operations/checkin.ts +102 -0
  82. package/src/operations/closeout-receipt.ts +74 -0
  83. package/src/operations/closeout.ts +19 -0
  84. package/src/operations/code-review-find.ts +97 -0
  85. package/src/operations/consolidated-diff.ts +144 -0
  86. package/src/operations/diff-revisions.ts +157 -0
  87. package/src/operations/diff.ts +53 -0
  88. package/src/operations/merge.ts +26 -0
  89. package/src/operations/object-delete-receipt.ts +21 -0
  90. package/src/operations/object-write-receipt.ts +105 -0
  91. package/src/operations/patch-receipt.ts +96 -0
  92. package/src/operations/patch-support.ts +375 -0
  93. package/src/operations/patch.ts +25 -0
  94. package/src/operations/removal-receipt.ts +42 -0
  95. package/src/operations/reviews.ts +90 -0
  96. package/src/operations/server-merge.ts +130 -0
  97. package/src/operations/shelveset-list.ts +90 -0
  98. package/src/operations/shelvesets.ts +90 -0
  99. package/src/operations/status.ts +129 -0
  100. package/src/operations/switch-receipt.ts +85 -0
  101. package/src/operations/switch.ts +25 -0
  102. package/src/operations/undo-receipt.ts +37 -0
  103. package/src/operations/update-receipt.ts +35 -0
  104. package/src/operations/workspace-list.ts +92 -0
  105. package/src/operations/workspace-merge-receipt.ts +50 -0
  106. package/src/operations/workspace.ts +120 -0
  107. package/src/pi/add-output.ts +52 -0
  108. package/src/pi/arguments.ts +298 -0
  109. package/src/pi/branch-create-output.ts +83 -0
  110. package/src/pi/branch-delete-output.ts +46 -0
  111. package/src/pi/branch-list-output.ts +59 -0
  112. package/src/pi/branch-output.ts +63 -0
  113. package/src/pi/capability-diagnostics.ts +23 -0
  114. package/src/pi/checkin-output.ts +68 -0
  115. package/src/pi/closeout-output.ts +87 -0
  116. package/src/pi/code-review-find-output.ts +50 -0
  117. package/src/pi/diff-output.ts +66 -0
  118. package/src/pi/diff-revisions-output.ts +55 -0
  119. package/src/pi/object-delete-output.ts +15 -0
  120. package/src/pi/object-write-output.ts +54 -0
  121. package/src/pi/patch-output.ts +69 -0
  122. package/src/pi/register.ts +188 -0
  123. package/src/pi/removal-output.ts +59 -0
  124. package/src/pi/schemas.ts +146 -0
  125. package/src/pi/server-merge-output.ts +83 -0
  126. package/src/pi/shelveset-list-output.ts +50 -0
  127. package/src/pi/status-output.ts +204 -0
  128. package/src/pi/switch-output.ts +80 -0
  129. package/src/pi/tool-registry.ts +72 -0
  130. package/src/pi/undo-output.ts +52 -0
  131. package/src/pi/update-output.ts +48 -0
  132. package/src/pi/workspace-list-output.ts +53 -0
  133. package/src/pi/workspace-merge-output.ts +46 -0
  134. package/src/plastic-core.ts +14 -5775
  135. package/src/plastic-renderers.ts +102 -22
  136. package/src/plastic-tool-loading.ts +7 -12
  137. package/src/presentation/add-results.ts +5 -0
  138. package/src/presentation/branch-create.ts +7 -0
  139. package/src/presentation/branch-delete-results.ts +7 -0
  140. package/src/presentation/checkin-results.ts +14 -0
  141. package/src/presentation/closeout-results.ts +2 -0
  142. package/src/presentation/diff-results.ts +237 -0
  143. package/src/presentation/removal-results.ts +7 -0
  144. package/src/presentation/results.ts +84 -0
  145. package/src/presentation/switch-results.ts +7 -0
  146. package/src/presentation/undo-results.ts +5 -0
  147. package/src/presentation/update-results.ts +5 -0
  148. package/src/presentation/workspace-merge-results.ts +6 -0
@@ -0,0 +1,188 @@
1
+ import { executeObjectWriteOutput, shelvesetCreateOutputSchema, shelvesetApplyOutputSchema, codeReviewCreateOutputSchema, codeReviewUpdateOutputSchema } from "./object-write-output";
2
+ import { executePatchOutput, patchOutputSchema } from "./patch-output";
3
+ import { executeCheckinOutput, checkinOutputSchema } from "./checkin-output";
4
+ import { executeSwitchOutput, switchOutputSchema } from "./switch-output";
5
+ import { executeUpdateOutput, updateOutputSchema } from "./update-output";
6
+ import { executeAddOutput, addOutputSchema } from "./add-output";
7
+ import { executeUndoOutput, undoOutputSchema } from "./undo-output";
8
+ import { executeRemovalOutput, removalOutputSchema } from "./removal-output";
9
+ import { executeWorkspaceMergeOutput, workspaceMergeOutputSchema } from "./workspace-merge-output";
10
+ import { executeCloseoutOutput, closeoutOutputSchema } from "./closeout-output";
11
+ import { executeBranchDeleteOutput, branchDeleteOutputSchema } from "./branch-delete-output";
12
+ import { executeObjectDeleteOutput, shelvesetDeleteOutputSchema, codeReviewDeleteOutputSchema } from "./object-delete-output";
13
+ import { executeBranchCreateOutput, branchCreateOutputSchema } from "./branch-create-output";
14
+ import { executeServerMergeOutput, serverMergeOutputSchema } from "./server-merge-output";
15
+ import { executeDiffOutput, diffInputSchema, diffOutputSchema } from "./diff-output";
16
+ import { executeCodeReviewFindOutput, codeReviewFindOutputSchema } from "./code-review-find-output";
17
+ import { executeShelvesetListOutput, shelvesetListOutputSchema } from "./shelveset-list-output";
18
+ import { executeWorkspaceListOutput, workspaceListOutputSchema } from "./workspace-list-output";
19
+ import { executeBranchListOutput, branchListOutputSchema } from "./branch-list-output";
20
+ import { executeBranchOutput, currentBranchOutputSchema, branchExistsOutputSchema } from "./branch-output";
21
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
22
+ import { Type } from "typebox";
23
+ import { executeStatusOutput, statusOutputSchema } from "./status-output";
24
+ import { runWithAbortSignal } from "../execution/context";
25
+ import { renderPlasticCall, renderPlasticResult, renderPlasticSearchResult } from "../plastic-renderers";
26
+ import {
27
+ getEffectivePlasticToolOwnership,
28
+ getInitiallyInactivePlasticTools,
29
+ getPlasticToolLoadingMode,
30
+ getRestoredPlasticToolNames,
31
+ getUnknownExactPlasticToolNames,
32
+ isPlasticToolBrowseRequest,
33
+ PLASTIC_TOOL_BROWSE_TEXT,
34
+ PLASTIC_TOOL_SEARCH_NAME,
35
+ searchPlasticTools,
36
+ } from "../plastic-tool-loading";
37
+ import { TOOL_CONFIG, normalizeArgs } from "./arguments";
38
+ import { buildParameters } from "./schemas";
39
+ import { getCoreTool, getRegisteredPlasticExportNames, toToolName } from "./tool-registry";
40
+ import { notifyPatchCapabilityWarning, type CapabilityDiagnosticContext } from "./capability-diagnostics";
41
+
42
+ export function registerPlasticTools(pi: ExtensionAPI, extensionSourcePath: string) {
43
+ const exportNames = getRegisteredPlasticExportNames();
44
+ const coreDescriptions = new Map<string, string>();
45
+ for (const exportName of exportNames) {
46
+ const coreTool = getCoreTool(exportName);
47
+ coreDescriptions.set(toToolName(exportName), coreTool.description ?? toToolName(exportName));
48
+ }
49
+
50
+ for (const exportName of exportNames) {
51
+ const coreTool = getCoreTool(exportName);
52
+ const config = TOOL_CONFIG[exportName] ?? {};
53
+ pi.registerTool({
54
+ name: toToolName(exportName),
55
+ label: toToolName(exportName),
56
+ description: coreTool.description ?? toToolName(exportName),
57
+ parameters: exportName === "diff" ? diffInputSchema : buildParameters(coreTool.args),
58
+ ...(exportName === "shelvesetCreate" ? {outputSchema:shelvesetCreateOutputSchema} : exportName === "shelvesetApply" ? {outputSchema:shelvesetApplyOutputSchema} : exportName === "codeReviewCreate" ? {outputSchema:codeReviewCreateOutputSchema} : exportName === "codeReviewUpdate" ? {outputSchema:codeReviewUpdateOutputSchema} : exportName === "patch" ? {outputSchema:patchOutputSchema} : exportName === "status" ? { outputSchema: statusOutputSchema } : exportName === "currentBranch" ? { outputSchema: currentBranchOutputSchema } : exportName === "branchExists" ? { outputSchema: branchExistsOutputSchema } : exportName === "branchList" ? { outputSchema: branchListOutputSchema } : exportName === "workspaceList" ? { outputSchema: workspaceListOutputSchema } : exportName === "shelvesetList" ? { outputSchema: shelvesetListOutputSchema } : exportName === "codeReviewFind" ? { outputSchema: codeReviewFindOutputSchema } : exportName === "diff" ? { outputSchema: diffOutputSchema } : exportName === "mergeBranches" ? { outputSchema: serverMergeOutputSchema } : exportName === "checkin" ? { outputSchema: checkinOutputSchema } : exportName === "branchCreate" ? { outputSchema: branchCreateOutputSchema } : exportName === "switchBranch" ? { outputSchema: switchOutputSchema } : exportName === "update" ? { outputSchema: updateOutputSchema } : exportName === "add" ? { outputSchema: addOutputSchema } : exportName === "undo" ? { outputSchema: undoOutputSchema } : exportName === "resolveDeleteChangeConflict" ? { outputSchema: removalOutputSchema } : exportName === "merge" || exportName === "finalizeMerge" ? { outputSchema: workspaceMergeOutputSchema } : exportName === "mergeToBranch" ? { outputSchema: closeoutOutputSchema } : exportName === "branchDelete" ? { outputSchema: branchDeleteOutputSchema } : exportName === "shelvesetDelete" ? {outputSchema:shelvesetDeleteOutputSchema} : exportName === "codeReviewDelete" ? {outputSchema:codeReviewDeleteOutputSchema} : {}),
59
+ prepareArguments: config.prepareArguments,
60
+ renderCall(args, theme, context) {
61
+ return renderPlasticCall(exportName, args ?? {}, theme, context);
62
+ },
63
+ renderResult(result, options, theme, context) {
64
+ return renderPlasticResult(exportName, result, options, theme, context);
65
+ },
66
+ async execute(_toolCallId, params, signal, _onUpdate, ctx) {
67
+ const normalizedParams = normalizeArgs(params);
68
+ if (exportName !== "mergeBranches" && normalizedParams.workdir === undefined && ctx?.cwd) {
69
+ normalizedParams.workdir = ctx.cwd;
70
+ }
71
+ if (exportName === "shelvesetCreate" || exportName === "shelvesetApply" || exportName === "codeReviewCreate" || exportName === "codeReviewUpdate") return runWithAbortSignal(signal, () => executeObjectWriteOutput(({shelvesetCreate:"shelveset-create",shelvesetApply:"shelveset-apply",codeReviewCreate:"code-review-create",codeReviewUpdate:"code-review-update"} as const)[exportName], normalizedParams));
72
+ if (exportName === "patch") return runWithAbortSignal(signal, () => executePatchOutput(normalizedParams));
73
+ if (exportName === "checkin") return runWithAbortSignal(signal, () => executeCheckinOutput(normalizedParams));
74
+ if (exportName === "switchBranch") return runWithAbortSignal(signal, () => executeSwitchOutput(normalizedParams));
75
+ if (exportName === "update") return runWithAbortSignal(signal, () => executeUpdateOutput(normalizedParams));
76
+ if (exportName === "add") return runWithAbortSignal(signal, () => executeAddOutput(normalizedParams));
77
+ if (exportName === "undo") return runWithAbortSignal(signal, () => executeUndoOutput(normalizedParams));
78
+ if (exportName === "resolveDeleteChangeConflict") return runWithAbortSignal(signal, () => executeRemovalOutput(normalizedParams));
79
+ if (exportName === "merge" || exportName === "finalizeMerge") return runWithAbortSignal(signal, () => executeWorkspaceMergeOutput(exportName === "merge" ? "merge" : "finalize-merge", normalizedParams));
80
+ if (exportName === "mergeToBranch") return runWithAbortSignal(signal, () => executeCloseoutOutput(normalizedParams));
81
+ if (exportName === "branchDelete") return runWithAbortSignal(signal, () => executeBranchDeleteOutput(normalizedParams));
82
+ if (exportName === "shelvesetDelete" || exportName === "codeReviewDelete") return runWithAbortSignal(signal, () => executeObjectDeleteOutput(exportName === "shelvesetDelete" ? "shelveset-delete" : "code-review-delete", normalizedParams));
83
+ if (exportName === "branchCreate") return runWithAbortSignal(signal, () => executeBranchCreateOutput(normalizedParams));
84
+ if (exportName === "mergeBranches") return runWithAbortSignal(signal, () => executeServerMergeOutput(normalizedParams));
85
+ if (exportName === "diff") return runWithAbortSignal(signal, () => executeDiffOutput(normalizedParams));
86
+ if (exportName === "codeReviewFind") return runWithAbortSignal(signal, () => executeCodeReviewFindOutput(normalizedParams));
87
+ if (exportName === "shelvesetList") return runWithAbortSignal(signal, () => executeShelvesetListOutput(normalizedParams));
88
+ if (exportName === "workspaceList") return runWithAbortSignal(signal, () => executeWorkspaceListOutput(normalizedParams));
89
+ if (exportName === "branchList") return runWithAbortSignal(signal, () => executeBranchListOutput(normalizedParams));
90
+ if (exportName === "status") return runWithAbortSignal(signal, () => executeStatusOutput(normalizedParams));
91
+ if (exportName === "currentBranch" || exportName === "branchExists") return runWithAbortSignal(signal, () => executeBranchOutput(exportName, normalizedParams));
92
+ throw new Error(`Missing producer-owned Plastic adapter: ${exportName}`);
93
+ },
94
+ });
95
+ }
96
+
97
+ pi.registerTool({
98
+ name: PLASTIC_TOOL_SEARCH_NAME,
99
+ label: "Plastic Tool Search",
100
+ description: "Search and enable Plastic SCM / Unity Version Control tools for workspace status and sync, changes and checkins, text-only diffs and patches, branches and merges, shelvesets, code reviews, and workspaces.",
101
+ promptSnippet: "Use plastic_tool_search to find and enable Plastic SCM capabilities that are not active.",
102
+ promptGuidelines: [
103
+ "Use plastic_tool_search before a Plastic SCM action when an appropriate plastic_* tool is not active. Inspect exact mutation targets. Do not preflight routinely: use preflight for ambiguous or broad scope, moved/deleted path rewriting, compound operations, or an explicit preview request.",
104
+ ],
105
+ parameters: Type.Object({
106
+ query: Type.Optional(Type.String({ description: "Capability or workflow to search for." })),
107
+ toolNames: Type.Optional(Type.Array(Type.String({ description: "Exact public Plastic tool name." }), { maxItems: 4, description: "Optional exact tool names to enable." })),
108
+ limit: Type.Optional(Type.Integer({ minimum: 1, maximum: 4, description: "Maximum matching tools to enable. Defaults to the single best keyword match; exact toolNames requests may enable up to four." })),
109
+ }),
110
+ renderCall(args, theme, context) {
111
+ return renderPlasticCall("tool_search", args ?? {}, theme, context);
112
+ },
113
+ renderResult(result, options, theme, context) {
114
+ return renderPlasticSearchResult(result, options, theme, context);
115
+ },
116
+ async execute(_toolCallId, params) {
117
+ if (isPlasticToolBrowseRequest(params)) {
118
+ return {
119
+ content: [{ type: "text", text: PLASTIC_TOOL_BROWSE_TEXT }],
120
+ details: { browse: true, matches: [], added: [], alreadyActive: [], guidance: [] },
121
+ };
122
+ }
123
+
124
+ const ownership = getEffectivePlasticToolOwnership(pi.getAllTools(), extensionSourcePath);
125
+ const unknownToolNames = getUnknownExactPlasticToolNames(params.toolNames);
126
+ const active = pi.getActiveTools();
127
+ // Never infer ownership without canonical sourceInfo. In that
128
+ // compatibility mode we may describe known tools that are already active,
129
+ // but cannot safely activate an inactive same-named definition.
130
+ const matches = searchPlasticTools(params, coreDescriptions).filter((match) =>
131
+ ownership.usesSourceInfo ? ownership.ownedToolNames.has(match.name) : active.includes(match.name),
132
+ );
133
+ const requestedExactToolNames = Array.isArray(params.toolNames)
134
+ ? [...new Set(params.toolNames.filter((name): name is string => typeof name === "string" && name.trim().length > 0).map((name) => name.trim()))]
135
+ : [];
136
+ const unavailableToolNames = requestedExactToolNames.filter((name) => !matches.some((match) => match.name.toLowerCase() === name.toLowerCase()));
137
+ if (matches.length === 0) {
138
+ const unavailable = unavailableToolNames.length > 0
139
+ ? ` Unknown or unavailable exact tool names: ${unavailableToolNames.join(", ")}.`
140
+ : "";
141
+ return {
142
+ content: [{ type: "text", text: `No executable Plastic tools matched. Try a capability, workflow term, or exact public plastic_* tool name.${unavailable}` }],
143
+ details: { matches: [], added: [], alreadyActive: [], guidance: [], unknownToolNames, unavailableToolNames },
144
+ };
145
+ }
146
+
147
+ const matchNames = matches.map((match) => match.name);
148
+ const added = matchNames.filter((name) => !active.includes(name));
149
+ const alreadyActive = matchNames.filter((name) => active.includes(name));
150
+ if (added.length > 0) pi.setActiveTools([...new Set([...active, ...added])]);
151
+
152
+ const guidance = matches.flatMap((match) => match.guidance);
153
+ const unknownText = unavailableToolNames.length > 0 ? `\nUnknown or unavailable exact tool names: ${unavailableToolNames.join(", ")}.` : "";
154
+ const loadedText = added.length > 0 ? `Activated: ${added.join(", ")}.` : "All matching tools were already active.";
155
+ return {
156
+ content: [{
157
+ type: "text",
158
+ text: `${loadedText}\nMatches: ${matchNames.join(", ")}.\nGuidance: ${guidance.join(" ")}${unknownText}`,
159
+ }],
160
+ details: { matches: matchNames, added, alreadyActive, guidance, unknownToolNames, unavailableToolNames },
161
+ };
162
+ },
163
+ });
164
+
165
+ pi.on("session_start", async (_event, ctx) => {
166
+ await notifyPatchCapabilityWarning(ctx as CapabilityDiagnosticContext);
167
+ const ownership = getEffectivePlasticToolOwnership(pi.getAllTools(), extensionSourcePath);
168
+ const active = pi.getActiveTools();
169
+ const mode = getPlasticToolLoadingMode();
170
+ // Fail safe when provenance is unavailable or the effective loader is a
171
+ // foreign first-registration-wins collision. Preserve the active set
172
+ // exactly; even adding the public loader name could activate foreign code.
173
+ if (!ownership.usesSourceInfo) return;
174
+
175
+ // The all-active eval mode keeps every currently exposed Plastic tool
176
+ // active while omitting the dynamic loader from the provider surface.
177
+ if (mode === "all-active") {
178
+ pi.setActiveTools(active.filter((name) => name !== PLASTIC_TOOL_SEARCH_NAME));
179
+ return;
180
+ }
181
+
182
+ const initiallyInactive = getInitiallyInactivePlasticTools(mode);
183
+ const ownedInitiallyInactive = new Set([...initiallyInactive].filter((name) => ownership.ownedToolNames.has(name)));
184
+ const restored = getRestoredPlasticToolNames(ctx.sessionManager.getBranch(), ownership.ownedToolNames);
185
+ const preserved = active.filter((name) => !ownedInitiallyInactive.has(name));
186
+ pi.setActiveTools([...new Set([...preserved, PLASTIC_TOOL_SEARCH_NAME, ...restored])]);
187
+ });
188
+ }
@@ -0,0 +1,59 @@
1
+ import { Type } from "typebox";
2
+ import { Check } from "typebox/value";
3
+ import { assembleRemovalReceipt, emptyRemovalData, removalFailure, presentRemovalReceipt, safeRemovalText, REMOVAL_COMMAND, validRemovalOperands, type RemovalReceipt } from "../operations/removal-receipt";
4
+ const object = (p: Record<string, any>) => Type.Object(p, { additionalProperties: false });
5
+ const enumeration = (values: readonly string[]) => Type.Union(values.map(value => Type.Literal(value)));
6
+ const uint = (max = Number.MAX_SAFE_INTEGER) => Type.Integer({ minimum: 0, maximum: max });
7
+ const attempt = object({ state: enumeration(["not-attempted", "not-started", "started", "unknown"]), terminal: enumeration(["not-observed", "observed"]), exitCode: Type.Union([uint(2147483647), Type.Null()]), aborted: Type.Boolean(), timedOut: Type.Boolean() });
8
+ const capture = object({ stdoutBytes: uint(), stderrBytes: uint(), stdoutRetainedBytes: uint(65536), stderrRetainedBytes: uint(16384), truncated: Type.Boolean(), complete: Type.Boolean(), validUtf8: Type.Boolean() });
9
+ const data = { intendedArgv: Type.Array(Type.String({ minLength: 1, maxLength: 4096 }), { minItems: 1, maxItems: 258 }), requestedOperandCount: Type.Union([uint(256), Type.Null()]), workingDirectory: Type.Union([Type.String({ minLength: 1, maxLength: 4096 }), Type.Null()]), attempt, capture: Type.Union([capture, Type.Null()]), observedWorkspaceIdentity: Type.Null(), observedResolvedItems: Type.Null(), observedResolution: Type.Null(), observedPreservation: Type.Null(), observedPendingState: Type.Null(), keepOnDiskRequested: Type.Union([Type.Boolean(), Type.Null()]), previewRequested: Type.Union([Type.Boolean(), Type.Null()]), outputFormatRequested: Type.Union([enumeration(["text", "json"]), Type.Null()]), resolutionIntent: Type.Literal("accept-source-deletion"), verification: Type.Literal("unverified") };
10
+ const header = { schemaVersion: Type.Literal(1), action: Type.Literal("resolve-delete-change-conflict"), provenance: object({ source: Type.Literal("plastic"), producer: Type.Literal("@aefree/pi-plastic"), contentTrust: Type.Literal("external") }), completeness: object({ capture: enumeration(["complete", "incomplete", "unknown"]), projection: Type.Literal(true) }) };
11
+ const error = object({ code: enumeration(["invalid_request", "launch_failed", "aborted", "uncertain", "producer_failed"]), message: Type.String({ minLength: 1, maxLength: 256 }) });
12
+ export const removalOutputSchema = Type.Unsafe<RemovalReceipt>(Type.Union([
13
+ object({ ...header, ok: Type.Literal(true), outcome: Type.Literal("preflight"), data: object({ ...data, effect: Type.Literal("not-attempted") }) }),
14
+ object({ ...header, ok: Type.Literal(true), outcome: Type.Literal("command-completed"), data: object({ ...data, effect: Type.Literal("not-proven") }) }),
15
+ object({ ...header, ok: Type.Literal(false), outcome: Type.Literal("failed"), data: object({ ...data, effect: Type.Literal("not-attempted") }), error }),
16
+ object({ ...header, ok: Type.Literal(false), outcome: Type.Literal("uncertain"), data: object({ ...data, effect: Type.Literal("uncertain") }), error }),
17
+ ]));
18
+ export function validateRemovalOutput(value: unknown): value is RemovalReceipt {
19
+ if (!Check(removalOutputSchema, value) || Buffer.byteLength(JSON.stringify(value), "utf8") > 131072) return false;
20
+ const dto = value as RemovalReceipt, d = dto.data, a = d.attempt, c = d.capture;
21
+ if (d.intendedArgv[0] !== REMOVAL_COMMAND) return false;
22
+ if (d.requestedOperandCount === null) {
23
+ if (d.intendedArgv.length !== 1 || d.keepOnDiskRequested !== null || d.previewRequested !== null || d.outputFormatRequested !== null || d.workingDirectory !== null || c || !["not-attempted", "unknown"].includes(a.state)) return false;
24
+ } else {
25
+ if (typeof d.keepOnDiskRequested !== "boolean" || typeof d.previewRequested !== "boolean" || d.outputFormatRequested === null || !d.workingDirectory) return false;
26
+ const offset = d.keepOnDiskRequested ? 2 : 1;
27
+ if (d.keepOnDiskRequested && d.intendedArgv[1] !== "--nodisk" || !validRemovalOperands(d.intendedArgv.slice(offset)) || d.requestedOperandCount !== d.intendedArgv.length - offset) return false;
28
+ }
29
+ if (d.previewRequested === true && (d.requestedOperandCount === null || c || a.state !== "not-attempted" || a.terminal !== "not-observed" || a.exitCode !== null || a.aborted || a.timedOut || d.effect !== "not-attempted" || !dto.ok || dto.outcome !== "preflight")) return false;
30
+ if (dto.ok && dto.outcome === "preflight" && d.previewRequested !== true) return false;
31
+ if (d.workingDirectory !== null && !safeRemovalText(d.workingDirectory)) return false;
32
+ if (!dto.ok && !safeRemovalText(dto.error.message)) return false;
33
+ if (a.exitCode !== null && a.terminal !== "observed" || a.state === "not-attempted" && (a.terminal !== "not-observed" || a.exitCode !== null || a.timedOut) || a.state === "not-started" && (a.terminal !== "not-observed" || a.exitCode !== null)) return false;
34
+ if (c) {
35
+ if (!d.workingDirectory || c.stdoutRetainedBytes > c.stdoutBytes || c.stderrRetainedBytes > c.stderrBytes) return false;
36
+ if (c.complete && (!c.validUtf8 || c.truncated || c.stdoutBytes !== c.stdoutRetainedBytes || c.stderrBytes !== c.stderrRetainedBytes || a.state !== "started" || a.terminal !== "observed" || a.exitCode === null || a.aborted || a.timedOut)) return false;
37
+ if (a.state === "not-attempted" && (!a.aborted || c.stdoutBytes || c.stderrBytes || c.validUtf8 || c.complete)) return false;
38
+ }
39
+ if (dto.completeness.capture !== (c ? c.complete ? "complete" : "incomplete" : "unknown")) return false;
40
+ if (dto.ok && dto.outcome === "command-completed" && (d.previewRequested !== false || d.requestedOperandCount === null || !d.workingDirectory || !c?.complete || a.state !== "started" || a.terminal !== "observed" || a.exitCode !== 0 || a.aborted || a.timedOut)) return false;
41
+ if (!dto.ok && dto.outcome === "uncertain" && !["started", "unknown"].includes(a.state)) return false;
42
+ if (!dto.ok && dto.outcome === "failed" && !["not-attempted", "not-started"].includes(a.state)) return false;
43
+ if (!dto.ok && dto.error.code === "invalid_request" && (d.workingDirectory !== null || c || a.state !== "not-attempted" || a.aborted)) return false;
44
+ if (!dto.ok && dto.error.code === "aborted" && !a.aborted) return false;
45
+ if (!dto.ok && dto.error.code === "launch_failed" && !["not-attempted", "not-started"].includes(a.state)) return false;
46
+ return true;
47
+ }
48
+ export async function executeRemovalOutput(args: unknown) {
49
+ let dto = await assembleRemovalReceipt(args);
50
+ const valid: boolean = validateRemovalOutput(dto);
51
+ if (!valid) {
52
+ const possibleStart = !["not-attempted", "not-started"].includes(dto.data.attempt.state);
53
+ const data = emptyRemovalData();
54
+ if (possibleStart) { data.attempt.state = "unknown"; data.effect = "uncertain"; }
55
+ dto = removalFailure(data, "producer_failed", "Removal receipt validation failed; do not retry automatically.");
56
+ if (!validateRemovalOutput(dto)) throw new Error("Safe removal receipt could not be constructed.");
57
+ }
58
+ return { content: [{ type: "text" as const, text: presentRemovalReceipt(dto) }], details: {}, structuredContent: dto, isError: !dto.ok };
59
+ }
@@ -0,0 +1,146 @@
1
+ import { type TSchema, Type } from "typebox";
2
+
3
+ type CoreSchemaNode = {
4
+ kind: string;
5
+ values?: unknown;
6
+ inner?: unknown;
7
+ metadata?: {
8
+ optional?: boolean;
9
+ description?: string;
10
+ min?: number;
11
+ max?: number;
12
+ int?: boolean;
13
+ };
14
+ };
15
+
16
+ const EMPTY_PARAMETERS = Type.Object({});
17
+ const enumSchema = (values: readonly string[], description: string): TSchema =>
18
+ Type.Union(values.map((value) => Type.Literal(value)), { description });
19
+
20
+ const OUTPUT_FORMAT_SCHEMA = enumSchema(["text", "json"], "Output format. Defaults to text.");
21
+ const PENDING_CHANGES_SCHEMA = enumSchema(["shelve", "bring", "cancel"], "How to handle pending changes when switching branches.");
22
+ const MERGE_STRATEGY_SCHEMA = enumSchema(["auto", "source", "destination"], "Conflict resolution strategy.");
23
+ const COMPARISON_METHOD_SCHEMA = enumSchema(
24
+ ["ignoreeol", "ignorewhitespaces", "ignoreeolandwhitespaces", "recognizeall"],
25
+ "Comparison method used for diff calculations.",
26
+ );
27
+ const REVIEW_TARGET_TYPE_SCHEMA = enumSchema(["branch", "changeset"], "Filter by review target type.");
28
+ const BRANCH_ORDER_BY_SCHEMA = enumSchema(["date", "branchname"], "Sort field for branch queries.");
29
+ const REVIEW_ORDER_BY_SCHEMA = enumSchema(["date", "modifieddate", "status"], "Sort field for review queries.");
30
+
31
+ function isCoreSchemaNode(value: unknown): value is CoreSchemaNode {
32
+ return Boolean(value) && typeof value === "object" && typeof (value as { kind?: unknown }).kind === "string";
33
+ }
34
+
35
+ function applySchemaMetadata(schema: TSchema, node: CoreSchemaNode): TSchema {
36
+ const metadata = node.metadata ?? {};
37
+ const options: { description?: string; minimum?: number; maximum?: number } = {};
38
+
39
+ if (metadata.description) options.description = metadata.description;
40
+ if (metadata.min !== undefined) options.minimum = metadata.min;
41
+ if (metadata.max !== undefined) options.maximum = metadata.max;
42
+
43
+ let nextSchema: TSchema = schema;
44
+ if (Object.keys(options).length > 0) {
45
+ if (Type.IsString(schema)) nextSchema = Type.String(options);
46
+ else if (Type.IsNumber(schema) || Type.IsInteger(schema)) nextSchema = metadata.int ? Type.Integer(options) : Type.Number(options);
47
+ else if (Type.IsBoolean(schema)) nextSchema = Type.Boolean(options);
48
+ else if (Type.IsArray(schema)) nextSchema = Type.Array(schema.items, options);
49
+ else nextSchema = Type.Unsafe({ ...schema, ...options });
50
+ }
51
+
52
+ if (metadata.int && Type.IsNumber(nextSchema)) {
53
+ nextSchema = Type.Integer(options);
54
+ }
55
+
56
+ return metadata.optional ? Type.Optional(nextSchema) : nextSchema;
57
+ }
58
+
59
+ function literalSchema(value: unknown): TSchema {
60
+ if (typeof value === "string") return Type.Literal(value);
61
+ if (typeof value === "number") return Number.isInteger(value) ? Type.Literal(value) : Type.Literal(value);
62
+ if (typeof value === "boolean") return Type.Literal(value);
63
+ return Type.Any();
64
+ }
65
+
66
+ function convertCoreSchema(node: unknown): TSchema {
67
+ if (!isCoreSchemaNode(node)) {
68
+ return literalSchema(node);
69
+ }
70
+
71
+ let schema: TSchema;
72
+ switch (node.kind) {
73
+ case "string":
74
+ schema = Type.String();
75
+ break;
76
+ case "number":
77
+ schema = node.metadata?.int ? Type.Integer() : Type.Number();
78
+ break;
79
+ case "boolean":
80
+ schema = Type.Boolean();
81
+ break;
82
+ case "any":
83
+ schema = Type.Any();
84
+ break;
85
+ case "array":
86
+ schema = Type.Array(convertCoreSchema(node.inner));
87
+ break;
88
+ case "enum": {
89
+ const values = Array.isArray(node.values) ? node.values : [];
90
+ if (values.every((value) => typeof value === "string")) {
91
+ const valueSet = values;
92
+ if (arraysEqual(valueSet, ["text", "json"])) {
93
+ schema = OUTPUT_FORMAT_SCHEMA;
94
+ } else if (arraysEqual(valueSet, ["shelve", "bring", "cancel"])) {
95
+ schema = PENDING_CHANGES_SCHEMA;
96
+ } else if (arraysEqual(valueSet, ["auto", "source", "destination"])) {
97
+ schema = MERGE_STRATEGY_SCHEMA;
98
+ } else if (arraysEqual(valueSet, ["ignoreeol", "ignorewhitespaces", "ignoreeolandwhitespaces", "recognizeall"])) {
99
+ schema = COMPARISON_METHOD_SCHEMA;
100
+ } else if (arraysEqual(valueSet, ["branch", "changeset"])) {
101
+ schema = REVIEW_TARGET_TYPE_SCHEMA;
102
+ } else if (arraysEqual(valueSet, ["date", "branchname"])) {
103
+ schema = BRANCH_ORDER_BY_SCHEMA;
104
+ } else if (arraysEqual(valueSet, ["date", "modifieddate", "status"])) {
105
+ schema = REVIEW_ORDER_BY_SCHEMA;
106
+ } else {
107
+ schema = enumSchema(valueSet, metadataDescription(node));
108
+ }
109
+ } else {
110
+ schema = Type.Union(values.map((value) => literalSchema(value)));
111
+ }
112
+ break;
113
+ }
114
+ case "union": {
115
+ const values = Array.isArray(node.values) ? node.values : [];
116
+ const converted = values.map((value) => convertCoreSchema(value));
117
+ schema = converted.length > 1 ? Type.Union(converted) : (converted[0] ?? Type.Any());
118
+ break;
119
+ }
120
+ default:
121
+ schema = Type.Any();
122
+ break;
123
+ }
124
+
125
+ return applySchemaMetadata(schema, node);
126
+ }
127
+
128
+ function arraysEqual(left: readonly string[], right: readonly string[]): boolean {
129
+ return left.length === right.length && left.every((value, index) => value === right[index]);
130
+ }
131
+
132
+ function metadataDescription(node: CoreSchemaNode): string {
133
+ return node.metadata?.description ?? "Allowed values.";
134
+ }
135
+
136
+ export function buildParameters(args: Record<string, unknown> | undefined): TSchema {
137
+ if (!args || Object.keys(args).length === 0) {
138
+ return EMPTY_PARAMETERS;
139
+ }
140
+
141
+ const properties: Record<string, TSchema> = {};
142
+ for (const [key, value] of Object.entries(args)) {
143
+ properties[key] = convertCoreSchema(value);
144
+ }
145
+ return Type.Object(properties);
146
+ }
@@ -0,0 +1,83 @@
1
+ import { isAbsolute } from "node:path";
2
+ import { Type, type TProperties } from "typebox";
3
+ import { Check } from "typebox/value";
4
+ import { assembleServerMergeReceipt, presentServerMergeReceipt, splitServerBranch, type MergeReceipt } from "../operations/server-merge";
5
+ const object = <P extends TProperties>(p: P) => Type.Object(p, { additionalProperties: false });
6
+ const enumOf = <const V extends readonly string[]>(v: V) => Type.Unsafe<V[number]>({ anyOf: v.map(value => ({ type: "string", const: value })) });
7
+ const str = (max = 4096) => Type.String({ maxLength: max });
8
+ const uint = (max = Number.MAX_SAFE_INTEGER) => Type.Integer({ minimum: 0, maximum: max });
9
+ const changeset = object({ id: Type.String({ pattern: "^(0|[1-9][0-9]{0,19})$" }), branch: str(), repository: str(), server: str(), mount: str() });
10
+ const nullable = <T extends ReturnType<typeof object> | ReturnType<typeof Type.Array>>(s: T) => Type.Union([s, Type.Null()]);
11
+ const attempt = object({ state: enumOf(["not-attempted", "not-started", "started", "unknown"]), terminal: enumOf(["not-observed", "observed"]), exitCode: Type.Union([uint(2147483647), Type.Null()]), aborted: Type.Boolean(), timedOut: Type.Boolean() });
12
+ const capture = object({ stdoutBytes: uint(), stderrBytes: uint(), stdoutRetainedBytes: uint(65536), stderrRetainedBytes: uint(16384), truncated: Type.Boolean(), complete: Type.Boolean(), validUtf8: Type.Boolean() });
13
+ const data = {
14
+ mode: enumOf(["apply", "analyze"]),
15
+ resolutionsFile: nullable(object({ path: str(), sha256: Type.String({ pattern: "^[a-f0-9]{64}$" }), entries: uint(500), resultFiles: uint(500), keepSource: uint(500), keepDestination: uint(500) })),
16
+ unlistedConflictPolicy: Type.Union([enumOf(["source", "destination"]), Type.Null()]),
17
+ requestedIdentity: nullable(object({ source: str(), target: str(), branch: str(), repository: str(), server: str() })),
18
+ command: nullable(Type.Array(str(8192), { maxItems: 16 })),
19
+ capability: object({ state: enumOf(["not-checked", "advertised", "missing", "unavailable"]), missingTokens: Type.Array(str(64), { maxItems: 9 }) }),
20
+ attempt, capture: object({ help: nullable(capture), merge: nullable(capture) }),
21
+ parse: nullable(object({ records: uint(500), malformed: Type.Boolean(), unknownOperations: uint(500), changesetsObserved: uint(500), conflictsObserved: uint(500) })),
22
+ observedChangesets: Type.Array(changeset, { maxItems: 100 }), conflictPaths: Type.Array(str(), { maxItems: 100 }),
23
+ counts: object({ changesetsReturned: uint(100), changesetsOmitted: uint(500), conflictsReturned: uint(100), conflictsOmitted: uint(500) }),
24
+ serverAliasEquivalence: Type.Literal("unverified"), mergeLinkIdentity: Type.Literal("unverified"), xlinkEffects: Type.Literal("unverified"), wouldRun: Type.Boolean(), remoteAnalysis: enumOf(["not-performed", "performed"]),
25
+ };
26
+ const header = { schemaVersion: Type.Literal(1), action: Type.Literal("merge-branches"), provenance: object({ source: Type.Literal("plastic"), producer: Type.Literal("@aefree/pi-plastic"), contentTrust: Type.Literal("external") }), completeness: object({ capture: enumOf(["complete", "incomplete", "unknown"]), projection: Type.Boolean() }) };
27
+ const error = object({ code: enumOf(["invalid_request", "capability_unavailable", "unsupported", "aborted", "launch_failed", "conflict", "uncertain", "producer_failed"]), stage: enumOf(["input", "help", "merge", "producer"]), message: str(256) });
28
+ export const serverMergeOutputSchema = Type.Unsafe<MergeReceipt>(Type.Union([
29
+ object({ ...header, ok: Type.Literal(true), outcome: Type.Literal("completed"), data: object({ ...data, createdChangeset: changeset, effect: Type.Literal("changeset-created") }) }),
30
+ object({ ...header, ok: Type.Literal(true), outcome: Type.Literal("analyzed"), data: object({ ...data, createdChangeset: Type.Null(), effect: Type.Literal("not-attempted") }) }),
31
+ object({ ...header, ok: Type.Literal(true), outcome: Type.Literal("preflight"), data: object({ ...data, createdChangeset: Type.Null(), effect: Type.Literal("not-attempted") }) }),
32
+ object({ ...header, ok: Type.Literal(true), outcome: Type.Literal("no-op"), data: object({ ...data, createdChangeset: Type.Null(), effect: Type.Literal("not-proven") }) }),
33
+ object({ ...header, ok: Type.Literal(false), outcome: enumOf(["failed", "unsupported"]), error, data: object({ ...data, createdChangeset: Type.Null(), effect: Type.Literal("not-attempted") }) }),
34
+ object({ ...header, ok: Type.Literal(false), outcome: enumOf(["conflict", "uncertain"]), error, data: object({ ...data, createdChangeset: Type.Null(), effect: Type.Literal("uncertain") }) }),
35
+ ]));
36
+ export function validateServerMergeOutput(value: unknown): value is MergeReceipt {
37
+ if (!Check(serverMergeOutputSchema, value) || Buffer.byteLength(JSON.stringify(value), "utf8") > 131072) return false;
38
+ const dto = value as MergeReceipt, d = dto.data, a = d.attempt, p = d.parse;
39
+ const safeText = (s: string) => !/[\u0000-\u001f\u007f-\u009f]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/.test(s);
40
+ for (const cs of [...d.observedChangesets, ...(d.createdChangeset ? [d.createdChangeset] : [])]) if (!splitServerBranch("br:" + cs.branch + "@" + cs.repository + "@" + cs.server) || ![cs.branch, cs.repository, cs.server, cs.mount].every(safeText)) return false;
41
+ if (!d.conflictPaths.every(s => s.startsWith("/") && safeText(s))) return false;
42
+ if (!p && (d.observedChangesets.length || d.conflictPaths.length || Object.values(d.counts).some(Boolean))) return false;
43
+ if (dto.completeness.capture === "complete" && !(d.capture.merge?.complete ?? d.capture.help?.complete)) return false;
44
+ if (dto.outcome === "preflight" && dto.completeness.capture !== "unknown") return false;
45
+ if (d.resolutionsFile && (d.resolutionsFile.resultFiles + d.resolutionsFile.keepSource + d.resolutionsFile.keepDestination !== d.resolutionsFile.entries || !safeText(d.resolutionsFile.path) || !isAbsolute(d.resolutionsFile.path))) return false;
46
+ if (d.mode === "analyze" && (d.resolutionsFile || d.unlistedConflictPolicy || d.command?.some(a => a === "--merge" || a.startsWith("-c=") || a.startsWith("--fileconflictsresolutionsfile=") || a === "--keepsource" || a === "--keepdestination"))) return false;
47
+ if (dto.outcome === "analyzed" && (d.mode !== "analyze" || d.remoteAnalysis !== "performed" || a.exitCode !== 0 || !d.command || !d.requestedIdentity || d.command[0] !== "cm" || d.command[1] !== "merge" || !d.command.includes("--nointeractiveresolution") || d.capture.merge?.stderrBytes || p?.changesetsObserved !== 0 || !p.records || dto.completeness.capture !== "complete")) return false;
48
+ if (dto.outcome !== "analyzed" && d.remoteAnalysis !== "not-performed") return false;
49
+ if (["completed", "no-op", "conflict"].includes(dto.outcome) && d.mode !== "apply") return false;
50
+ if (a.exitCode !== null && a.terminal !== "observed") return false;
51
+ if (["preflight", "unsupported", "failed"].includes(dto.outcome) && !["not-attempted", "not-started"].includes(a.state)) return false;
52
+ if (["completed", "no-op", "conflict", "analyzed"].includes(dto.outcome) && (a.state !== "started" || a.terminal !== "observed" || a.exitCode === null || a.aborted || a.timedOut || !d.capture.merge?.complete || !p || p.malformed || p.unknownOperations)) return false;
53
+ if (dto.outcome === "preflight" && (d.capture.help !== null || d.capture.merge !== null || d.capability.state !== "not-checked" || !d.command || !d.requestedIdentity || !d.wouldRun)) return false;
54
+ if (d.requestedIdentity) {
55
+ const s = splitServerBranch(d.requestedIdentity.source), t = splitServerBranch(d.requestedIdentity.target);
56
+ if (!s || !t || s.repository !== t.repository || s.server !== t.server || t.branch !== d.requestedIdentity.branch || t.repository !== d.requestedIdentity.repository || t.server !== d.requestedIdentity.server || s.branch === t.branch) return false;
57
+ }
58
+ const c = d.counts;
59
+ if (c.changesetsReturned + c.conflictsReturned > 100) return false;
60
+ if (c.changesetsReturned !== d.observedChangesets.length || c.conflictsReturned !== d.conflictPaths.length || p && (c.changesetsReturned + c.changesetsOmitted !== p.changesetsObserved || c.conflictsReturned + c.conflictsOmitted !== p.conflictsObserved)) return false;
61
+ if ((c.changesetsOmitted || c.conflictsOmitted) && dto.completeness.projection) return false;
62
+ if (dto.outcome === "completed" && (a.exitCode !== 0 || !d.requestedIdentity || !d.createdChangeset || d.createdChangeset.branch !== d.requestedIdentity.branch || d.createdChangeset.repository !== d.requestedIdentity.repository || d.createdChangeset.mount !== "/" || p?.changesetsObserved !== 1 || p.conflictsObserved !== 0)) return false;
63
+ if (dto.outcome === "no-op" && (a.exitCode !== 0 || p?.changesetsObserved !== 0 || p.conflictsObserved !== 0)) return false;
64
+ if (dto.outcome === "conflict" && (a.exitCode === 0 || p?.changesetsObserved !== 0 || !p.conflictsObserved)) return false;
65
+ for (const cap of [d.capture.help, d.capture.merge]) if (cap && (cap.stdoutRetainedBytes > cap.stdoutBytes || cap.stderrRetainedBytes > cap.stderrBytes || cap.complete && (!cap.validUtf8 || cap.truncated))) return false;
66
+ return true;
67
+ }
68
+ export async function executeServerMergeOutput(args: unknown) {
69
+ let dto = await assembleServerMergeReceipt(args);
70
+ const valid: boolean = validateServerMergeOutput(dto);
71
+ if (!valid) {
72
+ const mayHaveStarted = dto.data.attempt.state === "started" || dto.data.attempt.state === "unknown";
73
+ // Build a minimal bounded failure with no commands; damaged producer fields
74
+ // must not survive as apparently verified identities/capture facts.
75
+ const safe = await assembleServerMergeReceipt({});
76
+ dto = { ...safe, ok: false, outcome: mayHaveStarted ? "uncertain" : "failed", data: { ...safe.data, attempt: { ...safe.data.attempt, state: mayHaveStarted ? "unknown" : "not-attempted" }, effect: mayHaveStarted ? "uncertain" : "not-attempted" }, completeness: { capture: "unknown", projection: false }, error: { code: "producer_failed", stage: "producer", message: "The bounded server merge receipt could not be verified; do not retry automatically." } } as MergeReceipt;
77
+ if (!validateServerMergeOutput(dto)) throw new Error("The safe server merge receipt could not be constructed.");
78
+ }
79
+ const json = (args as { format?: unknown })?.format === "json";
80
+ let text = json ? JSON.stringify(dto) : await presentServerMergeReceipt(dto);
81
+ if (text.length > 24000) text = JSON.stringify({ action: dto.action, outcome: dto.outcome, ok: dto.ok, note: "Presentation omitted; inspect structuredContent for the bounded receipt." });
82
+ return { content: [{ type: "text" as const, text }], details: {}, structuredContent: dto, isError: !dto.ok };
83
+ }
@@ -0,0 +1,50 @@
1
+ import { Type, type Static, type TSchema } from "typebox";
2
+ import { Check } from "typebox/value";
3
+ import { assembleShelvesetListObservation, presentShelvesetListObservation, shelvesetListPayload, ShelvesetListError, validShelvesetId, validShelvesetQueryString, SHELVESET_LIST_MAX_BYTES, type ShelvesetListArgs } from "../operations/shelveset-list";
4
+ import { ShelvesetListCommandError } from "../execution/shelveset-list-command";
5
+ import { getActiveAbortSignal } from "../execution/context";
6
+ const object = <T extends Record<string, TSchema>>(fields: T) => Type.Object(fields, { additionalProperties: false });
7
+ const nullable = <T extends TSchema>(schema: T) => Type.Union([schema, Type.Null()]);
8
+ const integer = (maximum: number) => Type.Integer({ minimum: 0, maximum });
9
+ const queryString = nullable(Type.String({ maxLength: 4096, pattern: "^[^\\u0000-\\u001f\\u007f-\\u009f]*$" }));
10
+ const query = object({ owner: queryString, commentLike: queryString, dateFrom: queryString, limit: nullable(Type.Integer({ minimum: 1, maximum: Number.MAX_SAFE_INTEGER })) });
11
+ const common = { schemaVersion: Type.Literal(1), action: Type.Literal("shelveset-list"), provenance: object({ source: Type.Literal("plastic"), producer: Type.Literal("@aefree/pi-plastic"), contentTrust: Type.Literal("external") }) };
12
+ const dataCommon = { scope: Type.Literal("workspace_repository"), qualifierVerified: Type.Literal(false), query, limits: object({ maxItems: Type.Integer({ minimum: 1, maximum: 500 }), idDigits: Type.Literal(20), queryCodeUnits: Type.Literal(4096), compactUtf8Bytes: Type.Literal(131072) }) };
13
+ const errorCode = Type.Union([Type.Literal("command_failed"), Type.Literal("aborted"), Type.Literal("capture_incomplete"), Type.Literal("malformed_output"), Type.Literal("invalid_identity"), Type.Literal("unsupported_query"), Type.Literal("invalid_producer_data"), Type.Literal("output_overflow")]);
14
+ export const shelvesetListOutputSchema = Type.Union([
15
+ object({ ...common, ok: Type.Literal(true), completeness: object({ read: Type.Literal("complete"), capture: Type.Literal("complete"), projection: Type.Boolean() }), data: object({ ...dataCommon, mode: Type.Literal("ids"), basis: Type.Literal("find_shelve_id_utf8"), diagnostics: object({ duplicateRecords: integer(19999) }), rows: Type.Array(object({ id: Type.String({ minLength: 1, maxLength: 20, pattern: "^(?:0|[1-9][0-9]*)$" }), shelveset: Type.String({ minLength: 4, maxLength: 23, pattern: "^sh:(?:0|[1-9][0-9]*)$" }) }), { maxItems: 500 }), countBasis: Type.Literal("observed_query_rows"), counts: object({ observed: integer(20000), returned: integer(500), omitted: integer(20000), excluded: Type.Literal(0) }) }) }),
16
+ object({ ...common, ok: Type.Literal(true), completeness: object({ read: Type.Literal("unknown"), capture: Type.Literal("unknown"), projection: Type.Literal(false) }), data: object({ ...dataCommon, mode: Type.Literal("native"), basis: Type.Literal("find_shelve_native"), rows: Type.Null(), counts: Type.Null(), countBasis: Type.Literal("unavailable") }) }),
17
+ object({ ...common, ok: Type.Literal(false), completeness: object({ read: Type.Literal("incomplete"), capture: Type.Literal("unknown"), projection: Type.Literal(false) }), error: object({ code: errorCode, message: Type.String({ minLength: 1, maxLength: 256 }) }) }),
18
+ ]);
19
+ export type ShelvesetListOutput = Static<typeof shelvesetListOutputSchema>;
20
+ export type ShelvesetListErrorCode = Extract<ShelvesetListOutput, { ok: false }>["error"]["code"];
21
+ const failure = (code: ShelvesetListErrorCode): ShelvesetListOutput => ({ schemaVersion: 1, action: "shelveset-list", provenance: { source: "plastic", producer: "@aefree/pi-plastic", contentTrust: "external" }, ok: false, completeness: { read: "incomplete", capture: "unknown", projection: false }, error: { code, message: `Shelveset list read failed (${code}); no reusable rows are available.` } });
22
+ export function validateShelvesetListOutput(value: unknown): ShelvesetListOutput {
23
+ if (!Check(shelvesetListOutputSchema, value)) return failure("invalid_producer_data");
24
+ const dto = value as ShelvesetListOutput;
25
+ if (dto.ok) {
26
+ if ([dto.data.query.owner, dto.data.query.commentLike, dto.data.query.dateFrom].some(value => value !== null && !validShelvesetQueryString(value))) return failure("invalid_producer_data");
27
+ if (dto.data.mode === "ids") {
28
+ const { rows, counts, diagnostics, limits, query } = dto.data;
29
+ const repeats = rows.length - new Set(rows.map(row => row.id)).size;
30
+ if (rows.some(row => !validShelvesetId(row.id) || row.shelveset !== `sh:${row.id}`)
31
+ || counts.returned !== rows.length || counts.observed !== counts.returned + counts.omitted || counts.returned !== Math.min(counts.observed, limits.maxItems)
32
+ || (query.limit !== null && counts.observed > query.limit) || dto.completeness.projection !== (counts.omitted === 0)
33
+ || diagnostics.duplicateRecords < repeats || diagnostics.duplicateRecords > repeats + counts.omitted || diagnostics.duplicateRecords > Math.max(0, counts.observed - 1)) return failure("invalid_producer_data");
34
+ }
35
+ }
36
+ if (Buffer.byteLength(JSON.stringify(dto), "utf8") > SHELVESET_LIST_MAX_BYTES) return failure("output_overflow");
37
+ return dto;
38
+ }
39
+ export async function executeShelvesetListOutput(args: ShelvesetListArgs) {
40
+ let dto: ShelvesetListOutput, rawResult: string | undefined;
41
+ try {
42
+ const observation = await assembleShelvesetListObservation(args, true);
43
+ if (getActiveAbortSignal()?.aborted) throw new ShelvesetListCommandError("aborted");
44
+ dto = validateShelvesetListOutput(shelvesetListPayload(observation));
45
+ if (dto.ok) rawResult = args.output === "json" ? JSON.stringify(dto) : presentShelvesetListObservation(observation);
46
+ if (getActiveAbortSignal()?.aborted) throw new ShelvesetListCommandError("aborted");
47
+ } catch (error) { dto = failure(error instanceof ShelvesetListError || error instanceof ShelvesetListCommandError ? error.code : "invalid_producer_data"); }
48
+ dto = validateShelvesetListOutput(dto);
49
+ return { content: [{ type: "text" as const, text: dto.ok ? rawResult! : dto.error.message }], structuredContent: dto, isError: !dto.ok, details: { exportName: "shelvesetList", rawResult: dto.ok ? rawResult : undefined, ...(args.workdir ? { workdir: args.workdir } : {}) } };
50
+ }