@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,85 @@
1
+ import { resolve } from "node:path";
2
+ import { captureSwitchCommand, emptySwitchAttempt, type SwitchCommandObservation } from "../execution/switch-command";
3
+ import { compareSwitchTarget, parseSwitchTarget, parseSwitchLoadedBranch, parseSwitchPending, switchPendingArgv, safeSwitchValue, admittedSwitchNoChanges, type SwitchReceipt, type SwitchRequest, type SwitchData, type SwitchStep, type SwitchStepName } from "../domain/switch-contract";
4
+ import { buildSwitchPendingProfile, canSwitchDirectWithPrivateOnlyPending, isSwitchBringBlockedForUnattended } from "../domain/switch-policy";
5
+ export { presentSwitchReceipt } from "../presentation/switch-results";
6
+ export { safeSwitchValue, compareSwitchTarget, parseSwitchTarget } from "../domain/switch-contract";
7
+ export type { SwitchReceipt, SwitchStep, SwitchData, SwitchAttempt, SwitchCapture, SwitchSummary, SwitchBranchObservation } from "../domain/switch-contract";
8
+ export function validateSwitchRequest(input: unknown): asserts input is SwitchRequest {
9
+ if (!input || typeof input !== "object" || Array.isArray(input)) throw Error("Switch request must be an object.");
10
+ const r = input as Record<string, unknown>;
11
+ if (Object.keys(r).some(k => !["branch", "pendingChanges", "preflight", "format", "workdir"].includes(k)) || !safeSwitchValue(r.branch) || !parseSwitchTarget(r.branch)) throw Error("Provide a bounded hierarchical branch selector with preserved complete qualification.");
12
+ if (r.workdir !== undefined && !safeSwitchValue(r.workdir) || r.pendingChanges !== undefined && !["shelve", "bring", "cancel"].includes(r.pendingChanges as string) || r.format !== undefined && !["text", "json"].includes(r.format as string) || r.preflight !== undefined && typeof r.preflight !== "boolean") throw Error("Switch options are unsupported or invalid.");
13
+ }
14
+ export async function assembleSwitchReceipt(input: unknown): Promise<SwitchReceipt> {
15
+ const data: SwitchData = { requestedTarget: null, workdir: null, pendingPolicy: "cancel", defaultedPolicy: true, preflight: false, strategy: "unresolved", wouldRun: false, plannedMutations: [], unattemptedMutations: [], steps: [], branchBefore: null, branchAfter: null, pendingBefore: null, pendingRecovery: null, observedShelveset: null, usedNoChangesShelveRecovery: false, targetVerification: "unverified", serverAliasEquivalence: "unverified", filePreservation: "unverified", xlinkEffects: "unverified", effect: "not-attempted" };
16
+ let stage: "input" | SwitchStepName | "policy" | "producer" = "input";
17
+ const finish = (outcome: SwitchReceipt["outcome"], code: Extract<SwitchReceipt, {ok:false}>["error"]["code"] = "observation_failed", message = "Switch evidence is unavailable; do not retry automatically."): SwitchReceipt => {
18
+ const mutations = data.steps.filter(s => s.name === "shelve" || s.name === "switch");
19
+ data.effect = mutations.some(s => s.effect === "uncertain") ? "uncertain" : mutations.some(s => s.effect === "command-completed") ? "command-completed" : "not-attempted";
20
+ data.unattemptedMutations = data.plannedMutations.filter(name => !mutations.some(s => s.name === name && s.attempt.state !== "not-attempted"));
21
+ const reads = data.steps.filter(s => s.name !== "shelve" && s.name !== "switch");
22
+ return { schemaVersion: 1, action: "switch-branch", provenance: { source: "plastic", producer: "@aefree/pi-plastic", contentTrust: "external" }, sourceAdmission: "windows-cm11.0.16.10371-observed", completeness: { read: reads.length ? reads.every(s => s.evidence === "admitted") ? "complete" : "incomplete" : "unknown", capture: data.steps.length ? data.steps.every(s => s.capture.complete) ? "complete" : "incomplete" : "unknown", projection: true }, data, ok: ["preflight", "already-loaded", "switched"].includes(outcome), outcome, ...(["preflight", "already-loaded", "switched"].includes(outcome) ? {} : {error:{code,stage,message}}) } as SwitchReceipt;
23
+ };
24
+ const normal = (o: SwitchCommandObservation) => !o.failed && o.capture.complete && o.attempt.state === "started" && o.attempt.terminal === "observed" && !o.attempt.aborted && !o.attempt.timedOut;
25
+ const observe = async (name: SwitchStepName, argv: string[]) => {
26
+ stage = name;
27
+ const mutation = name === "shelve" || name === "switch";
28
+ const s: SwitchStep = { name, argv: [...argv], attempt: { ...emptySwitchAttempt(), state:"unknown" }, capture: { stdoutBytes:0,stderrBytes:0,stdoutRetainedBytes:0,stderrRetainedBytes:0,truncated:false,complete:false,validUtf8:false }, evidence:"unadmitted", effect:mutation?"uncertain":"not-attempted", branch:null,summary:null,records:0 };
29
+ data.steps.push(s);
30
+ const o = await captureSwitchCommand(argv, data.workdir!);
31
+ s.attempt = o.attempt; s.capture = o.capture;
32
+ if (mutation) {
33
+ s.effect = ["not-attempted","not-started"].includes(o.attempt.state) ? "not-attempted" : "uncertain";
34
+ if (normal(o) && o.attempt.exitCode === 0 && !o.capture.stderrBytes) { s.effect="command-completed";s.evidence="terminal-only"; }
35
+ } else if (normal(o) && o.attempt.exitCode === 0 && !o.capture.stderrBytes) {
36
+ if (name === "branch-before" || name === "branch-after") { s.branch=parseSwitchLoadedBranch(o.stdout!);s.records=s.branch?1:0; }
37
+ else { const p=parseSwitchPending(o.stdout!,data.workdir!);s.summary=p.summary;s.records=p.records; }
38
+ if(s.branch || s.summary) { s.evidence="admitted";s.effect="read-observed"; }
39
+ }
40
+ return { o,s };
41
+ };
42
+ try {
43
+ validateSwitchRequest(input);
44
+ const r=input;
45
+ const cwd=resolve(r.workdir??process.cwd());if(!safeSwitchValue(cwd))return finish("failed","invalid_request","Resolved workdir exceeds the bound.");
46
+ Object.assign(data,{requestedTarget:r.branch,workdir:cwd,pendingPolicy:r.pendingChanges??"cancel",defaultedPolicy:r.pendingChanges===undefined,preflight:r.preflight??false});
47
+ if(process.platform!=="win32")return finish("failed","unsupported_source","Switch source is admitted only on observed Windows/client configuration.");
48
+ const before=await observe("branch-before",["status"]);data.branchBefore=before.s.branch;
49
+ if(!data.branchBefore)return finish("failed");
50
+ const pending=await observe("pending-before",switchPendingArgv);data.pendingBefore=pending.s.summary;
51
+ if(!data.pendingBefore)return finish("failed");
52
+ stage="policy";
53
+ const verification=compareSwitchTarget(data.branchBefore,r.branch);
54
+ if(verification!=="unverified") { data.strategy="already-on-target-branch";data.targetVerification=verification;return finish(data.preflight?"preflight":"already-loaded"); }
55
+ const profile=buildSwitchPendingProfile({...data.pendingBefore,privatePaths:[]});
56
+ if(!profile.hasPendingChanges)data.strategy="silent-noinput";
57
+ else if(canSwitchDirectWithPrivateOnlyPending(data.pendingPolicy,data.defaultedPolicy,profile))data.strategy="direct-switch-private-only";
58
+ else if(isSwitchBringBlockedForUnattended(data.pendingPolicy,profile)) { data.strategy="blocked-bring-tracked-pending";return finish(data.preflight?"preflight":"blocked","policy_blocked","Tracked pending bring requires interactive prompts and is blocked."); }
59
+ else if(data.pendingPolicy==="cancel") { data.strategy="cancel-with-pending";return finish(data.preflight?"preflight":"canceled","policy_canceled","Switch canceled by pending-change policy; no mutation was attempted."); }
60
+ else data.strategy="shelve-then-switch-noinput";
61
+ data.plannedMutations=data.strategy==="shelve-then-switch-noinput"?["shelve","switch"]:["switch"];data.wouldRun=true;
62
+ if(data.preflight)return finish("preflight");
63
+ if(data.strategy==="shelve-then-switch-noinput") {
64
+ const shelf=await observe("shelve",["shelveset","create","--all",`-c=Auto-shelve before switch to ${r.branch}`]);
65
+ if(shelf.s.effect!=="command-completed") {
66
+ if(!normal(shelf.o) || shelf.o.attempt.exitCode!==1 || !admittedSwitchNoChanges(shelf.o.stdout!,shelf.o.stderr!,cwd))return finish(data.effect==="not-attempted"&&["not-attempted","not-started"].includes(shelf.s.attempt.state)?"failed":"uncertain","mutation_failed");
67
+ shelf.s.evidence="eligible-no-changes";
68
+ const recovery=await observe("pending-recovery",switchPendingArgv);data.pendingRecovery=recovery.s.summary;
69
+ if(!data.pendingRecovery || data.pendingRecovery.tracked!==0)return finish("uncertain","observation_failed","Shelve failed; recovery did not prove absence of tracked pending. No switch was attempted.");
70
+ data.usedNoChangesShelveRecovery=true;
71
+ }
72
+ }
73
+ const switched=await observe("switch",["switch","--silent","--noinput",r.branch]);
74
+ if(switched.s.effect!=="command-completed")return finish(["not-attempted","not-started"].includes(switched.s.attempt.state)?"failed":"uncertain","mutation_failed");
75
+ const after=await observe("branch-after",["status"]);data.branchAfter=after.s.branch;
76
+ if(!data.branchAfter)return finish("command-completed-unverified","target_unverified","Switch command completed but loaded target could not be observed. Earlier effects remain; do not retry automatically.");
77
+ data.targetVerification=compareSwitchTarget(data.branchAfter,r.branch);
78
+ if(data.targetVerification==="unverified")return finish("command-completed-unverified","target_unverified","Switch command completed; requested target identity or full qualifier spelling remains unverified. Alias equivalence is not inferred.");
79
+ // Eligible failed shelving remains uncertain preservation, even after a verified switch.
80
+ if(data.usedNoChangesShelveRecovery) { stage="shelve";return finish("uncertain","mutation_failed","Loaded target observed after eligible recovery, but failed shelving did not prove preservation or absence of earlier effects."); }
81
+ return finish("switched");
82
+ } catch {
83
+ return finish(data.steps.some(s=>(s.name==="shelve"||s.name==="switch")&&!["not-attempted","not-started"].includes(s.attempt.state))?"uncertain":"failed",stage==="input"?"invalid_request":"producer_failed",stage==="input"?"Switch request could not be admitted.":"Switch producer failed; inspect recorded earlier effects before another attempt.");
84
+ }
85
+ }
@@ -0,0 +1,25 @@
1
+ import { isSameBranchSpec, normalizeBranchSpecForComparison, assertWorkspaceOnBranch } from "../domain/branches";
2
+ import { toLegacyPendingSummary, buildSwitchPendingProfile, isSwitchBringBlockedForUnattended, canSwitchDirectWithPrivateOnlyPending } from "../domain/switch-policy";
3
+ export { toLegacyPendingSummary, createCanceledSwitchOutcome, buildSwitchPendingProfile, isSwitchBringBlockedForUnattended, canSwitchDirectWithPrivateOnlyPending } from "../domain/switch-policy";
4
+ export type { SwitchPendingProfile, CanceledSwitchOutcome } from "../domain/switch-policy";
5
+ import { assembleSwitchReceipt, presentSwitchReceipt } from "./switch-receipt";
6
+ import { tool } from "../tool-definition";
7
+ import { outputFormatArg } from "../presentation/results";
8
+ import { workdirArg } from "./arguments";
9
+ export const __plasticSwitchInternals = { toLegacyPendingSummary, normalizeBranchSpecForComparison, isSameBranchSpec, assertWorkspaceOnBranch, buildSwitchPendingProfile, isSwitchBringBlockedForUnattended, canSwitchDirectWithPrivateOnlyPending };
10
+ export const switchBranch = tool({
11
+ description: "Switch the Plastic SCM workspace to a branch (cm switch), handling pending changes when needed.",
12
+ args: {
13
+ branch: tool.schema.string().min(1).describe("Branch name or spec selected from the current repository convention."),
14
+ pendingChanges: tool.schema.enum(["shelve", "bring", "cancel"]).optional().describe("How to handle pending changes when switching branches. Defaults to cancel. In unattended mode, bring is blocked only when tracked pending changes exist."),
15
+ preflight: tool.schema.boolean().optional().describe("Preview switch strategy without executing switch."),
16
+ format: outputFormatArg,
17
+ workdir: workdirArg,
18
+ },
19
+ async execute(args) {
20
+ const dto = await assembleSwitchReceipt(args);
21
+ // Policy cancellation also rejects here: a compound caller must not continue into target-side mutations.
22
+ if (!dto.ok && !args.preflight) throw new Error(dto.error.message);
23
+ return presentSwitchReceipt(dto,args.format);
24
+ },
25
+ });
@@ -0,0 +1,37 @@
1
+ import { captureUndoCommand, emptyUndoAttempt } from "../execution/undo-command";
2
+ import { UNDO_COMMAND, safeUndoText, validUndoOperands, type UndoData, type UndoError, type UndoReceipt } from "../domain/undo-contract";
3
+ export { UNDO_COMMAND, safeUndoText, validUndoOperands } from "../domain/undo-contract";
4
+ export type { UndoData, UndoError, UndoReceipt, UndoAttempt, UndoCapture } from "../domain/undo-contract";
5
+ export { presentUndoReceipt } from "../presentation/undo-results";
6
+ export function emptyUndoData(): UndoData {
7
+ return { intendedArgv: [UNDO_COMMAND], requestedOperandCount: null, workingDirectory: null, attempt: emptyUndoAttempt(), capture: null, observedWorkspaceIdentity: null, observedUndoneItems: null, verification: "unverified", effect: "not-attempted" };
8
+ }
9
+ function undoHeader(data: UndoData) {
10
+ return { schemaVersion: 1 as const, action: "undo" as const, provenance: { source: "plastic" as const, producer: "@aefree/pi-plastic" as const, contentTrust: "external" as const }, completeness: { capture: data.capture ? data.capture.complete ? "complete" as const : "incomplete" as const : "unknown" as const, projection: true as const }, data };
11
+ }
12
+ export function undoFailure(data: UndoData, code: UndoError["code"], message: string): UndoReceipt {
13
+ return { ...undoHeader(data), ok: false, outcome: data.effect === "uncertain" ? "uncertain" : "failed", error: { code, message } };
14
+ }
15
+ export async function assembleUndoReceipt(args: unknown): Promise<UndoReceipt> {
16
+ const data = emptyUndoData(); let dispatched = false;
17
+ try {
18
+ if (!args || typeof args !== "object" || Array.isArray(args) || Object.keys(args).some(key => key !== "workdir" && key !== "paths")) return undoFailure(data, "invalid_request", "Undo accepts paths and workdir only; preview and extra options are unsupported.");
19
+ const input = args as Record<string, unknown>;
20
+ const cwd = input.workdir === undefined ? process.cwd() : input.workdir;
21
+ if (!safeUndoText(cwd)) return undoFailure(data, "invalid_request", "Working directory must be nonblank bounded text without controls or invalid Unicode.");
22
+ const paths = Array.isArray(input.paths) && input.paths.length <= 256 ? Array.from(input.paths) : input.paths;
23
+ if (!validUndoOperands(paths)) return undoFailure(data, "invalid_request", "Provide 1–256 bounded path operands; CLI options, filters and hyphen-leading sentinels are rejected, aggregate UTF8 limit 32768 bytes.");
24
+ data.workingDirectory = cwd; data.intendedArgv = [UNDO_COMMAND, ...paths]; data.requestedOperandCount = paths.length; dispatched = true;
25
+ const observation = await captureUndoCommand([...data.intendedArgv], cwd);
26
+ data.attempt = observation.attempt; data.capture = observation.capture;
27
+ if (observation.capture.complete && !observation.failed && observation.attempt.state === "started" && observation.attempt.exitCode === 0) {
28
+ data.effect = "not-proven";
29
+ return { ...undoHeader(data), ok: true, outcome: "command-completed" };
30
+ }
31
+ data.effect = ["started", "unknown"].includes(data.attempt.state) ? "uncertain" : "not-attempted";
32
+ return undoFailure(data, data.attempt.aborted ? "aborted" : data.effect === "uncertain" ? "uncertain" : "launch_failed", data.effect === "uncertain" ? "Undo completion is not proven; partial undone-item or workspace effects may exist." : "Undo did not establish a process start.");
33
+ } catch {
34
+ if (dispatched) { data.attempt = { ...emptyUndoAttempt(), state: "unknown" }; data.capture = null; data.effect = "uncertain"; }
35
+ return undoFailure(data, "producer_failed", "Undo observation failed; no rollback or safe retry is proven.");
36
+ }
37
+ }
@@ -0,0 +1,35 @@
1
+ import { captureUpdateCommand, emptyUpdateAttempt } from "../execution/update-command";
2
+ import { UPDATE_ARGV, safeUpdateText, type UpdateData, type UpdateError, type UpdateReceipt } from "../domain/update-contract";
3
+ export { UPDATE_ARGV, safeUpdateText } from "../domain/update-contract";
4
+ export type { UpdateData, UpdateError, UpdateReceipt, UpdateAttempt, UpdateCapture } from "../domain/update-contract";
5
+ export { presentUpdateReceipt } from "../presentation/update-results";
6
+ export function emptyUpdateData(): UpdateData {
7
+ return { intendedArgv: [...UPDATE_ARGV], workingDirectory: null, attempt: emptyUpdateAttempt(), capture: null, observedWorkspaceIdentity: null, observedChanges: null, verification: "unverified", effect: "not-attempted" };
8
+ }
9
+ function updateHeader(data: UpdateData) {
10
+ return { schemaVersion: 1 as const, action: "update" as const, provenance: { source: "plastic" as const, producer: "@aefree/pi-plastic" as const, contentTrust: "external" as const }, completeness: { capture: data.capture ? data.capture.complete ? "complete" as const : "incomplete" as const : "unknown" as const, projection: true as const }, data };
11
+ }
12
+ export function updateFailure(data: UpdateData, code: UpdateError["code"], message: string): UpdateReceipt {
13
+ return { ...updateHeader(data), ok: false, outcome: data.effect === "uncertain" ? "uncertain" : "failed", error: { code, message } };
14
+ }
15
+ export async function assembleUpdateReceipt(args: unknown): Promise<UpdateReceipt> {
16
+ const data = emptyUpdateData(); let dispatched = false;
17
+ try {
18
+ if (!args || typeof args !== "object" || Array.isArray(args) || Object.keys(args).some(key => key !== "workdir")) return updateFailure(data, "invalid_request", "Update accepts workdir only; preview and extra options are unsupported.");
19
+ const input = args as Record<string, unknown>;
20
+ const cwd = input.workdir === undefined ? process.cwd() : input.workdir;
21
+ if (!safeUpdateText(cwd)) return updateFailure(data, "invalid_request", "Working directory must be nonblank bounded text without controls or invalid Unicode.");
22
+ data.workingDirectory = cwd; dispatched = true;
23
+ const observation = await captureUpdateCommand([...UPDATE_ARGV], cwd);
24
+ data.attempt = observation.attempt; data.capture = observation.capture;
25
+ if (observation.capture.complete && !observation.failed && observation.attempt.state === "started" && observation.attempt.exitCode === 0) {
26
+ data.effect = "not-proven";
27
+ return { ...updateHeader(data), ok: true, outcome: "command-completed" };
28
+ }
29
+ data.effect = ["started", "unknown"].includes(data.attempt.state) ? "uncertain" : "not-attempted";
30
+ return updateFailure(data, data.attempt.aborted ? "aborted" : data.effect === "uncertain" ? "uncertain" : "launch_failed", data.effect === "uncertain" ? "Update completion is not proven; partial workspace effects may exist." : "Update did not establish a process start.");
31
+ } catch {
32
+ if (dispatched) { data.attempt = { ...emptyUpdateAttempt(), state: "unknown" }; data.capture = null; data.effect = "uncertain"; }
33
+ return updateFailure(data, "producer_failed", "Update observation failed; no rollback or safe retry is proven.");
34
+ }
35
+ }
@@ -0,0 +1,92 @@
1
+ import { runCm, normalizeFindOutputLines } from "../execution/cm";
2
+ import { spawnAndCollect, getCmExecutable } from "../execution/process";
3
+ import { commandExecutionStorage, getActiveAbortSignal } from "../execution/context";
4
+ import { runWorkspaceListFieldsCommand, WorkspaceListCommandError, WORKSPACE_LIST_CAPTURE_LIMITS } from "../execution/workspace-list-command";
5
+ import { toStructuredResult } from "../presentation/results";
6
+
7
+ export type WorkspaceListArgs = { source?: "native" | "fields"; format?: string; output?: "text" | "json"; maxItems?: number; workdir?: string };
8
+ export type WorkspaceRow = { name: string; path: string; guid: string };
9
+ export class WorkspaceListError extends Error {
10
+ constructor(readonly code: "invalid_identity" | "malformed_output" | "unsupported_query" | "invalid_producer_data" | "output_overflow") { super(`Workspace list read failed (${code}); no reusable rows are available.`); }
11
+ }
12
+ export const WORKSPACE_LIST_MAX_BYTES = 131072;
13
+ export const WORKSPACE_FIELDS_FORMAT = "{wkname}{tab}{machine}{tab}{path}{tab}{wkid}";
14
+ export const validWorkspaceTemplate = (s: string) => s.length <= 4096 && !/[\u0000\uD800-\uDFFF]/u.test(s);
15
+ // ASCII output is not independent proof that original identities were ASCII:
16
+ // this source requires an independently known ASCII-name/path configuration.
17
+ export const validWorkspaceIdentity = (s: string) => s.length > 0 && s.length <= 4096 && /^[\x20-\x7e]+$/.test(s) && !s.includes("?");
18
+ export const validWorkspaceName = (s: string) => validWorkspaceIdentity(s) && !/[\\/@]/.test(s) && s.trim() === s;
19
+ export const validWorkspacePath = (s: string) => validWorkspaceIdentity(s) && (s.startsWith("/") || /^[A-Za-z]:[\\/]/.test(s) || /^\\\\[^\\]+\\[^\\]+/.test(s));
20
+ export const validWorkspaceGuid = (s: string) => /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(s);
21
+ export function workspaceListRequest(args: WorkspaceListArgs) {
22
+ if ((args.source !== undefined && args.source !== "native" && args.source !== "fields") || (args.output !== undefined && args.output !== "text" && args.output !== "json")
23
+ || (args.maxItems !== undefined && (!Number.isSafeInteger(args.maxItems) || args.maxItems < 1 || args.maxItems > 500))
24
+ || (args.format !== undefined && (typeof args.format !== "string" || !validWorkspaceTemplate(args.format)))) throw new WorkspaceListError("invalid_producer_data");
25
+ const source = args.source ?? "native";
26
+ if (source === "fields" && args.format) throw new WorkspaceListError("unsupported_query");
27
+ return { source, maxItems: args.maxItems ?? 100 };
28
+ }
29
+ export function workspaceListCommand(args: WorkspaceListArgs): string[] {
30
+ const request = workspaceListRequest(args);
31
+ return ["workspace", "list", ...(request.source === "fields" ? [`--format=${WORKSPACE_FIELDS_FORMAT}`] : args.format ? [`--format=${args.format}`] : [])];
32
+ }
33
+ export function parseWorkspaceListFields(bytes: Uint8Array): { rows: WorkspaceRow[]; duplicateRecords: number } {
34
+ if (bytes.length > WORKSPACE_LIST_CAPTURE_LIMITS.stdoutBytes) throw new WorkspaceListCommandError("capture_incomplete");
35
+ if (bytes.some(byte => byte > 127)) throw new WorkspaceListError("invalid_identity");
36
+ if (!bytes.length) return { rows: [], duplicateRecords: 0 };
37
+ const lines = Buffer.from(bytes).toString("ascii").split(/\r?\n/);
38
+ if (lines.at(-1) === "") lines.pop();
39
+ if (lines.length > WORKSPACE_LIST_CAPTURE_LIMITS.records) throw new WorkspaceListCommandError("capture_incomplete");
40
+ const rows: WorkspaceRow[] = [], guids = new Map<string, string>(), names = new Map<string, string>(), paths = new Map<string, string>();
41
+ let duplicateRecords = 0;
42
+ for (const line of lines) {
43
+ const fields = line.split("\t");
44
+ if (fields.length !== 4) throw new WorkspaceListError("malformed_output");
45
+ const [name, machine, path, guid] = fields;
46
+ if (!validWorkspaceName(name) || !validWorkspaceIdentity(machine) || !validWorkspacePath(path) || !validWorkspaceGuid(guid)) throw new WorkspaceListError("invalid_identity");
47
+ // Exact repeated full records are observed again, not silently deduplicated.
48
+ // Machine remains unpublished, but conflicts in it still fail the whole read.
49
+ const prior = [guids.get(guid.toLowerCase()), names.get(name), paths.get(path)].filter(value => value !== undefined);
50
+ if (prior.some(value => value !== line)) throw new WorkspaceListError("malformed_output");
51
+ if (prior.length) duplicateRecords++;
52
+ guids.set(guid.toLowerCase(), line); names.set(name, line); paths.set(path, line); rows.push({ name, path, guid });
53
+ }
54
+ return { rows, duplicateRecords };
55
+ }
56
+ export type WorkspaceListObservation = { maxItems: number } & ({ source: "native"; rawResult: string; command: string[] } | { source: "fields"; rows: WorkspaceRow[]; duplicateRecords: number });
57
+ export async function assembleWorkspaceListObservation(args: WorkspaceListArgs, typedNative = false): Promise<WorkspaceListObservation> {
58
+ const request = workspaceListRequest(args), command = workspaceListCommand(args);
59
+ if (request.source === "fields") {
60
+ const bytes = await runWorkspaceListFieldsCommand(command, args.workdir);
61
+ if (getActiveAbortSignal()?.aborted) throw new WorkspaceListCommandError("aborted");
62
+ const parsed = parseWorkspaceListFields(bytes);
63
+ if (getActiveAbortSignal()?.aborted) throw new WorkspaceListCommandError("aborted");
64
+ return { ...request, source: "fields", ...parsed };
65
+ }
66
+ if (!typedNative) return { ...request, source: "native", command, rawResult: await runCm(command, args.workdir) };
67
+ const signal = getActiveAbortSignal();
68
+ if (signal?.aborted) throw new WorkspaceListCommandError("aborted");
69
+ let result;
70
+ try { result = await spawnAndCollect(getCmExecutable(), command, args.workdir ?? process.cwd(), undefined, signal, commandExecutionStorage.getStore()); }
71
+ catch { throw new WorkspaceListCommandError(signal?.aborted ? "aborted" : "command_failed"); }
72
+ if (result.aborted || signal?.aborted) throw new WorkspaceListCommandError("aborted");
73
+ if (result.timedOut || result.stdoutTruncated || result.stderrTruncated) throw new WorkspaceListCommandError("capture_incomplete");
74
+ if (result.exitCode !== 0) throw new WorkspaceListCommandError("command_failed");
75
+ return { ...request, source: "native", command, rawResult: [result.stdout, result.stderr].filter(Boolean).join("\n").trim() || "(no output)" };
76
+ }
77
+ export const workspaceCapability = { identities: "ascii_names_and_paths_only", prerequisite: "original_names_and_paths_are_ascii", originalVerification: "not_performed", formatterLoss: "non_ascii_may_be_replaced_or_best_fit_to_ascii" } as const;
78
+ export function workspaceListPayload(observation: WorkspaceListObservation) {
79
+ const common = { schemaVersion: 1, action: "workspace-list", provenance: { source: "plastic", producer: "@aefree/pi-plastic", contentTrust: "external" }, ok: true } as const;
80
+ const data = { scope: "local_client_inventory", limits: { maxItems: observation.maxItems, identityCodeUnits: 4096, compactUtf8Bytes: WORKSPACE_LIST_MAX_BYTES } } as const;
81
+ const payload = observation.source === "native"
82
+ ? { ...common, completeness: { read: "unknown", capture: "unknown", projection: false }, data: { ...data, mode: "native", basis: "workspace_list_native", capability: "noncanonical", pathBasis: "unavailable", rows: null, counts: null, countBasis: "unavailable" } }
83
+ : { ...common, completeness: { read: "complete_under_ascii_prerequisite", capture: "complete", projection: observation.rows.length <= observation.maxItems }, data: { ...data, mode: "fields", basis: "workspace_list_tab_fields", capability: workspaceCapability, pathBasis: "producer_absolute", diagnostics: { duplicateRecords: observation.duplicateRecords }, rows: observation.rows.slice(0, observation.maxItems), countBasis: "observed_response_rows", counts: { observed: observation.rows.length, returned: Math.min(observation.rows.length, observation.maxItems), omitted: Math.max(0, observation.rows.length - observation.maxItems), excluded: 0 } } };
84
+ if (Buffer.byteLength(JSON.stringify(payload), "utf8") > WORKSPACE_LIST_MAX_BYTES) throw new WorkspaceListError("output_overflow");
85
+ return payload;
86
+ }
87
+ export async function presentWorkspaceListObservation(observation: WorkspaceListObservation, args: WorkspaceListArgs): Promise<string> {
88
+ if (observation.source === "native") return toStructuredResult("workspace-list", args.output ?? "text", observation.rawResult, { command: ["cm", ...observation.command], rawOutput: observation.rawResult, resultCount: normalizeFindOutputLines(observation.rawResult).length }, args.workdir);
89
+ if (args.output === "json") return JSON.stringify(workspaceListPayload(observation));
90
+ const rows = observation.rows.slice(0, observation.maxItems), omitted = observation.rows.length - rows.length;
91
+ return ["ASCII-only workspace names/paths required; originals are not independently verified by this read.", ...(rows.length ? rows.map(row => `${row.name}\t${row.path}\t${row.guid}`) : ["(no observed inventory rows)"]), ...(omitted ? [`(${omitted} observed rows omitted from presentation.)`] : [])].join("\n");
92
+ }
@@ -0,0 +1,50 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import { win32 } from "node:path";
3
+ import { parseCopiedPending, admitCopiedStandardStatus, copiedPendingArgv, copiedPathKey } from "../domain/copied-merge";
4
+ import { captureWorkspaceMergeCommand, emptyWorkspaceMergeAttempt } from "../execution/workspace-merge-command";
5
+ import { admitCleanStandardStatus, admitWorkspaceMergeOutput, buildWorkspaceMergeArgv, safeMergeText, type MergeAction, type MergeStage, type WorkspaceMergeData, type WorkspaceMergeReceipt, type MergeStrategy } from "../domain/workspace-merge-contract";
6
+ export type { WorkspaceMergeReceipt, MergeAction } from "../domain/workspace-merge-contract";
7
+ export { safeMergeText, buildWorkspaceMergeArgv } from "../domain/workspace-merge-contract";
8
+ export { presentWorkspaceMergeReceipt } from "../presentation/workspace-merge-results";
9
+ export const emptyMergeStage = (): MergeStage => ({attempt:emptyWorkspaceMergeAttempt(),capture:null,admission:"not-observed"});
10
+ export function emptyWorkspaceMergeData(): WorkspaceMergeData { return {intendedArgv:["merge"],workingDirectory:null,requestedSource:null,strategy:null,cherrypicking:null,forced:null,previewRequested:null,outputFormatRequested:null,apply:emptyMergeStage(),shortStatus:emptyMergeStage(),fullStatus:emptyMergeStage(),pendingReadMode:"short",commandCompleted:false,observedWorkspaceIdentity:null,observedSourceIdentity:null,observedFinalizedMetadata:null,observedPreservation:null,observedPendingItems:null,fileConflictCount:null,protocol:"not-observed",checkinReadiness:"unknown",verification:"unverified",effect:"not-attempted"}; }
11
+ function header(action:MergeAction,data:WorkspaceMergeData) {const stages=[data.apply,data.shortStatus,data.fullStatus],observed=stages.filter(s=>s.capture);return {schemaVersion:1 as const,action,provenance:{source:"plastic" as const,producer:"@aefree/pi-plastic" as const,contentTrust:"external" as const},completeness:{capture:!observed.length?"unknown" as const:observed.every(s=>s.capture!.complete)?"complete" as const:"incomplete" as const,projection:true as const},data};}
12
+ export function workspaceMergeFailure(action:MergeAction,data:WorkspaceMergeData,code:WorkspaceMergeReceipt extends infer T ? T extends {error:{code:infer C}} ? C : never : never,message:string):WorkspaceMergeReceipt {return {...header(action,data),ok:false,outcome:data.effect==="not-attempted"?"failed":data.checkinReadiness==="blocked"?"blocked":"uncertain",error:{code,message}};}
13
+ export async function assembleWorkspaceMergeReceipt(action:MergeAction,args:unknown):Promise<WorkspaceMergeReceipt>{
14
+ const data=emptyWorkspaceMergeData();let active: "apply"|"shortStatus"|"fullStatus"|null=null;
15
+ try{
16
+ const keys=action==="merge"?["source","strategy","cherrypicking","forced","preflight","format","workdir"]:["source","strategy","preflight","format","workdir"];
17
+ if(!args||typeof args!=="object"||Array.isArray(args)||Object.keys(args).some(k=>!keys.includes(k)))return workspaceMergeFailure(action,data,"invalid_request","Use only declared workspace merge controls.");
18
+ const i=args as Record<string,unknown>,source=i.source,cwd=i.workdir===undefined?process.cwd():i.workdir,strategy=i.strategy===undefined?(action==="merge"?"auto":"destination"):i.strategy,cherry=i.cherrypicking===undefined?false:i.cherrypicking,forced=i.forced===undefined?false:i.forced,preview=i.preflight===undefined?false:i.preflight,format=i.format===undefined?"text":i.format;
19
+ if(!safeMergeText(source)||source.trimStart().startsWith("-")||!safeMergeText(cwd)||!(action==="merge"?["auto","source","destination"]:["source","destination"]).includes(strategy as string)||[cherry,forced,preview].some(v=>typeof v!=="boolean")||!["text","json"].includes(format as string))return workspaceMergeFailure(action,data,"invalid_request","Use a bounded source operand, strict controls and text/json; source options/stdin are rejected.");
20
+ const token="__WM_"+randomBytes(12).toString("hex")+"__";data.intendedArgv=buildWorkspaceMergeArgv(source,strategy as MergeStrategy,cherry as boolean,forced as boolean,token);data.workingDirectory=cwd;data.requestedSource=source;data.strategy=strategy as MergeStrategy;data.cherrypicking=cherry as boolean;data.forced=forced as boolean;data.previewRequested=preview as boolean;data.outputFormatRequested=format as "text"|"json";
21
+ if(preview)return {...header(action,data),ok:true,outcome:"preflight"};
22
+ active="apply";const apply=await captureWorkspaceMergeCommand([...data.intendedArgv],cwd);data.apply={attempt:apply.attempt,capture:apply.capture,admission:"unsupported"};active=null;data.effect=["started","unknown"].includes(apply.attempt.state)?"uncertain":"not-attempted";
23
+ if(!apply.capture.complete||apply.failed||apply.attempt.state!=="started")return workspaceMergeFailure(action,data,apply.attempt.aborted?"aborted":data.effect==="not-attempted"?"launch_failed":"uncertain","Merge capture/start/completion is not established; do not retry automatically.");
24
+ data.commandCompleted=apply.attempt.exitCode===0;
25
+ const parsed=admitWorkspaceMergeOutput(apply.stdout!,token);data.protocol=parsed.protocol;data.pendingReadMode=parsed.protocol==="apply-add-copied"?"copied-machine":"short";data.fileConflictCount=parsed.fileConflictCount;data.apply.admission=parsed.protocol==="unsupported"?"unsupported":"admitted";
26
+ if(parsed.protocol==="file-conflict"){data.checkinReadiness="blocked";return workspaceMergeFailure(action,data,"conflict","Observed file-conflict records; checkin blocked. Earlier effects are not disproven.");}
27
+ if(!data.commandCompleted)return workspaceMergeFailure(action,data,"uncertain","Merge exited nonzero; workspace effects may already exist.");
28
+ if(parsed.protocol==="unsupported")return workspaceMergeFailure(action,data,"unsupported_readiness","Merge completed; applying profile unadmitted. Inspect apply.admission/protocol; verify loaded package revision and retain sanitized producer evidence. Readiness unknown; do not reapply automatically or infer rollback.");
29
+ const copied=parsed.protocol==="apply-add-copied";data.pendingReadMode=copied?"copied-machine":"short";
30
+ if(copied&&process.platform!=="win32")return workspaceMergeFailure(action,data,"unsupported_readiness","Copied readiness is sourced only on the observed Windows client.");
31
+ active="shortStatus";const short=await captureWorkspaceMergeCommand(copied?[...copiedPendingArgv]:["status","--short"],cwd);data.shortStatus={attempt:short.attempt,capture:short.capture,admission:"unsupported"};active=null;
32
+ if(!short.capture.complete||short.failed||short.attempt.exitCode!==0)return workspaceMergeFailure(action,data,"unsupported_readiness","Merge command completed; pending read failed/incomplete. Checkin readiness unknown.");
33
+ const pending=copied?parseCopiedPending(short.stdout!,cwd):null;
34
+ const copiedKeys=pending?.paths.map(copiedPathKey)??[];
35
+ const coherent=!!pending&&parsed.copiedPaths.length===copiedKeys.length&&new Set(parsed.copiedPaths.map(copiedPathKey)).size===copiedKeys.length&&parsed.copiedPaths.every(p=>copiedKeys.includes(copiedPathKey(p)))&&new Set(parsed.addedPaths.map(p=>copiedPathKey(win32.join(cwd,p)))).size===copiedKeys.length&&parsed.addedPaths.every(p=>p.startsWith("/")&&!p.includes("\\")&&p.slice(1).split("/").every(v=>v!==""&&v!=="."&&v!==".."&&!/[?\uFFFD]/u.test(v))&&copiedKeys.includes(copiedPathKey(win32.join(cwd,p))));
36
+ data.shortStatus.admission=(copied?coherent:short.stdout==="")&&short.stderr===""?"admitted":"unsupported";
37
+ active="fullStatus";const full=await captureWorkspaceMergeCommand(["status"],cwd);data.fullStatus={attempt:full.attempt,capture:full.capture,admission:"unsupported"};active=null;
38
+ if(!full.capture.complete||full.failed||full.attempt.exitCode!==0)return workspaceMergeFailure(action,data,"unsupported_readiness","Merge command completed; merge-state read failed/incomplete. Checkin readiness unknown.");
39
+ data.fullStatus.admission=(copied?!!pending&&admitCopiedStandardStatus(full.stdout!,pending,source,cwd):admitCleanStandardStatus(full.stdout!))&&full.stderr===""?"admitted":"unsupported";
40
+ // No warning/unknown stderr can authorize compound checkin.
41
+ if(apply.stderr!=="")return workspaceMergeFailure(action,data,"unsupported_readiness","Merge command completed; applying stderr is outside the admitted warning profile. Checkin paused; prior effects remain possible. Do not reapply automatically.");
42
+ if(data.shortStatus.admission!=="admitted")return workspaceMergeFailure(action,data,"unsupported_readiness",copied?"Merge command completed; copied pending/ADD/path cardinality correlation is unadmitted (shortStatus). Checkin paused; inspect separately authorized pending evidence without reapplying merge.":"Merge command completed; shortStatus pending/warning evidence is unadmitted. Checkin paused; inspect separately authorized pending evidence without reapplying merge.");
43
+ if(data.fullStatus.admission!=="admitted")return workspaceMergeFailure(action,data,"unsupported_readiness","Merge command completed; fullStatus namespace/link/copied or clean-status evidence is unadmitted. Checkin paused; prior effects remain possible. Inspect separately authorized status evidence without reapplying merge.");
44
+ data.effect="not-proven";data.checkinReadiness="supported-no-unresolved-signals";return {...header(action,data),ok:true,outcome:"command-completed"};
45
+ }catch{
46
+ if(active){data[active]={attempt:{...emptyWorkspaceMergeAttempt(),state:"unknown"},capture:null,admission:"unsupported"};}
47
+ if(active==="apply"||["started","unknown"].includes(data.apply.attempt.state))data.effect="uncertain";
48
+ return workspaceMergeFailure(action,data,"producer_failed","Merge evidence collection failed; prior effects remain possible and checkin readiness unknown.");
49
+ }
50
+ }
@@ -0,0 +1,120 @@
1
+ import { tool } from "../tool-definition";
2
+ import { assembleWorkspaceListObservation, presentWorkspaceListObservation } from "./workspace-list";
3
+ import { workdirArg } from "./arguments";
4
+ import { assembleUpdateReceipt, presentUpdateReceipt } from "./update-receipt";
5
+ import { assembleAddReceipt, presentAddReceipt } from "./add-receipt";
6
+ import { assembleUndoReceipt, presentUndoReceipt } from "./undo-receipt";
7
+ import { runCm } from "../execution/cm";
8
+ import { outputFormatArg } from "../presentation/results";
9
+ import { assembleRemovalReceipt, presentRemovalReceipt } from "./removal-receipt";
10
+
11
+ export const update = tool({
12
+ description: "Update workspace safely without launching interactive merge (cm update --dontmerge --noinput).",
13
+ args: {
14
+ workdir: workdirArg,
15
+ },
16
+ async execute(args)
17
+ {
18
+ const receipt = await assembleUpdateReceipt(args);
19
+ const text = presentUpdateReceipt(receipt);
20
+ if (!receipt.ok) throw new Error(text);
21
+ return text;
22
+ },
23
+ });
24
+
25
+ export const add = tool({
26
+ description: "Add items to Plastic SCM (cm add).",
27
+ args: {
28
+ paths: tool.schema.array(tool.schema.string()).min(1).describe("Paths to add."),
29
+ workdir: workdirArg,
30
+ },
31
+ async execute(args)
32
+ {
33
+ const receipt = await assembleAddReceipt(args);
34
+ const text = presentAddReceipt(receipt);
35
+ if (!receipt.ok) throw new Error(text);
36
+ return text;
37
+ },
38
+ });
39
+
40
+ export const undo = tool({
41
+ description: "Undo changes for specified items (cm undo).",
42
+ args: {
43
+ paths: tool.schema.array(tool.schema.string()).min(1).describe("Paths to undo."),
44
+ workdir: workdirArg,
45
+ },
46
+ async execute(args)
47
+ {
48
+ const receipt = await assembleUndoReceipt(args);
49
+ const text = presentUndoReceipt(receipt);
50
+ if (!receipt.ok) throw new Error(text);
51
+ return text;
52
+ },
53
+ });
54
+
55
+ export const resolveDeleteChangeConflict = tool({
56
+ description: "Request source-side deletion for a Plastic SCM delete/change conflict via cm remove; actual resolution remains unverified.",
57
+ args: {
58
+ paths: tool.schema.array(tool.schema.string()).min(1).describe("Controlled workspace paths to resolve by accepting the source-side deletion."),
59
+ keepOnDisk: tool.schema.boolean().optional().describe("Request --nodisk to retain items on disk. Defaults to true; preservation remains unverified."),
60
+ preflight: tool.schema.boolean().optional().describe("Preview the resolution command without executing it."),
61
+ format: outputFormatArg,
62
+ workdir: workdirArg,
63
+ },
64
+ async execute(args)
65
+ {
66
+
67
+ const receipt = await assembleRemovalReceipt(args);
68
+ const text = presentRemovalReceipt(receipt);
69
+ if (!receipt.ok) throw new Error(text);
70
+ return text;
71
+ },
72
+ });
73
+
74
+ export const workspaceCreate = tool({
75
+ description: "Create a Plastic SCM workspace (cm workspace create).",
76
+ args: {
77
+ name: tool.schema.string().min(1).describe("Workspace name."),
78
+ path: tool.schema.string().min(1).describe("Workspace path."),
79
+ repositorySpec: tool.schema.string().optional().describe("Optional repository specification for the new workspace."),
80
+ selectorFile: tool.schema.string().optional().describe("Optional selector file path for the new workspace."),
81
+ workdir: workdirArg,
82
+ },
83
+ async execute(args)
84
+ {
85
+ if (args.repositorySpec && args.selectorFile)
86
+ {
87
+ throw new Error("Provide either repositorySpec or selectorFile, not both.");
88
+ }
89
+
90
+ const cmdArgs: string[] = ["workspace", "create", args.name, args.path];
91
+
92
+ if (args.repositorySpec)
93
+ {
94
+ cmdArgs.push(args.repositorySpec);
95
+ }
96
+
97
+ if (args.selectorFile)
98
+ {
99
+ cmdArgs.push(`--selector=${args.selectorFile}`);
100
+ }
101
+
102
+ return runCm(cmdArgs, args.workdir);
103
+ },
104
+ });
105
+
106
+ export const workspaceList = tool({
107
+ description: "List Plastic SCM workspaces (cm workspace list).",
108
+ args: {
109
+ format: tool.schema.string().optional().describe("Native CLI template (up to 4096 code units), not a presentation enum. Conflicts with fields source unless empty."),
110
+ source: tool.schema.enum(["native", "fields"]).optional().describe("Defaults to native. Fields requires independently known ASCII-only original workspace names and paths; formatter may silently best-fit non-ASCII to ASCII."),
111
+ maxItems: tool.schema.number().int().min(1).max(500).optional().describe("Fields projection limit, default 100; not a CLI query limit."),
112
+ output: outputFormatArg,
113
+ workdir: workdirArg,
114
+ },
115
+ async execute(args)
116
+ {
117
+ const observation = await assembleWorkspaceListObservation(args);
118
+ return presentWorkspaceListObservation(observation, args);
119
+ },
120
+ });
@@ -0,0 +1,52 @@
1
+ import { Type } from "typebox";
2
+ import { Check } from "typebox/value";
3
+ import { assembleAddReceipt, emptyAddData, addFailure, presentAddReceipt, safeAddText, ADD_COMMAND, validAddOperands, type AddReceipt } from "../operations/add-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: 257 }), 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(), observedAddedItems: Type.Null(), verification: Type.Literal("unverified") };
10
+ const header = { schemaVersion: Type.Literal(1), action: Type.Literal("add"), 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 addOutputSchema = Type.Unsafe<AddReceipt>(Type.Union([
13
+ object({ ...header, ok: Type.Literal(true), outcome: Type.Literal("command-completed"), data: object({ ...data, effect: Type.Literal("not-proven") }) }),
14
+ object({ ...header, ok: Type.Literal(false), outcome: Type.Literal("failed"), data: object({ ...data, effect: Type.Literal("not-attempted") }), error }),
15
+ object({ ...header, ok: Type.Literal(false), outcome: Type.Literal("uncertain"), data: object({ ...data, effect: Type.Literal("uncertain") }), error }),
16
+ ]));
17
+ export function validateAddOutput(value: unknown): value is AddReceipt {
18
+ if (!Check(addOutputSchema, value) || Buffer.byteLength(JSON.stringify(value), "utf8") > 131072) return false;
19
+ const dto = value as AddReceipt, d = dto.data, a = d.attempt, c = d.capture;
20
+ if (d.intendedArgv[0] !== ADD_COMMAND) return false;
21
+ if (d.requestedOperandCount === null) {
22
+ if (d.intendedArgv.length !== 1 || d.workingDirectory !== null || c || !["not-attempted", "unknown"].includes(a.state)) return false;
23
+ } else if (!validAddOperands(d.intendedArgv.slice(1)) || d.requestedOperandCount !== d.intendedArgv.length - 1 || !d.workingDirectory) return false;
24
+ if (d.workingDirectory !== null && !safeAddText(d.workingDirectory)) return false;
25
+ if (!dto.ok && !safeAddText(dto.error.message)) return false;
26
+ 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;
27
+ if (c) {
28
+ if (!d.workingDirectory || c.stdoutRetainedBytes > c.stdoutBytes || c.stderrRetainedBytes > c.stderrBytes) return false;
29
+ 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;
30
+ if (a.state === "not-attempted" && (!a.aborted || c.stdoutBytes || c.stderrBytes || c.validUtf8 || c.complete)) return false;
31
+ }
32
+ if (dto.completeness.capture !== (c ? c.complete ? "complete" : "incomplete" : "unknown")) return false;
33
+ if (dto.ok && (d.requestedOperandCount === null || !d.workingDirectory || !c?.complete || a.state !== "started" || a.terminal !== "observed" || a.exitCode !== 0 || a.aborted || a.timedOut)) return false;
34
+ if (!dto.ok && dto.outcome === "uncertain" && !["started", "unknown"].includes(a.state)) return false;
35
+ if (!dto.ok && dto.outcome === "failed" && !["not-attempted", "not-started"].includes(a.state)) return false;
36
+ if (!dto.ok && dto.error.code === "invalid_request" && (d.workingDirectory !== null || c || a.state !== "not-attempted" || a.aborted)) return false;
37
+ if (!dto.ok && dto.error.code === "aborted" && !a.aborted) return false;
38
+ if (!dto.ok && dto.error.code === "launch_failed" && !["not-attempted", "not-started"].includes(a.state)) return false;
39
+ return true;
40
+ }
41
+ export async function executeAddOutput(args: unknown) {
42
+ let dto = await assembleAddReceipt(args);
43
+ const valid: boolean = validateAddOutput(dto);
44
+ if (!valid) {
45
+ const possibleStart = !["not-attempted", "not-started"].includes(dto.data.attempt.state);
46
+ const data = emptyAddData();
47
+ if (possibleStart) { data.attempt.state = "unknown"; data.effect = "uncertain"; }
48
+ dto = addFailure(data, "producer_failed", "Add receipt validation failed; do not retry automatically.");
49
+ if (!validateAddOutput(dto)) throw new Error("Safe add receipt could not be constructed.");
50
+ }
51
+ return { content: [{ type: "text" as const, text: presentAddReceipt(dto) }], details: {}, structuredContent: dto, isError: !dto.ok };
52
+ }