@aefree/pi-plastic 0.5.3 → 0.5.5

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.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,21 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project follows semantic versioning for public package releases.
7
7
 
8
+ ## Unreleased
9
+
10
+ ## [0.5.5] - 2026-08-14
11
+
12
+ ### Changed
13
+
14
+ - Made `plastic_status(machineReadable=true, format="json")` return compact parsed pending-item records, aggregate counts, and bounded-item metadata, with package-owned field separation and revision IDs. It returns at most 100 items by default (configurable up to 500); raw, unbounded Plastic output is omitted by default and available with `includeRaw=true` only for diagnostics.
15
+
16
+ ## [0.5.4] - 2026-08-13
17
+
18
+ ### Fixed
19
+
20
+ - Recognize both `smartbranch` and `br` branch forms in Plastic workspace selectors, including quoted values produced by different checkout workflows.
21
+ - Use a valid local selector as the footer's authoritative branch source instead of launching an unnecessary `cm status` process; malformed or missing branch selectors retain the bounded CLI fallback.
22
+
8
23
  ## [0.5.3] - 2026-08-09
9
24
 
10
25
  ### Changed
package/README.md CHANGED
@@ -4,7 +4,7 @@ Pi tools, footer status, and skill guidance for Plastic SCM / Unity Version Cont
4
4
 
5
5
  ## Plastic branch footer status
6
6
 
7
- When Pi starts inside a Plastic workspace, this package adds a themed `Plastic <branch>` status to Pi's built-in footer. It discovers the nearest enclosing `.plastic/plastic.workspace`, reads the local selector as a fast credential-free fallback, and confirms the branch with a bounded `cm status` call. Selector changes and successful same-workspace `plastic_*` tools refresh the status; sibling workspaces are ignored.
7
+ When Pi starts inside a Plastic workspace, this package adds a themed `Plastic <branch>` status to Pi's built-in footer. It discovers the nearest enclosing `.plastic/plastic.workspace` and reads either the selector's `smartbranch` or `br` form as the credential-free branch source. A bounded `cm status` call is used only when the selector has no valid branch. Selector changes and successful same-workspace `plastic_*` tools refresh the status; sibling workspaces are ignored.
8
8
 
9
9
  The extension owns only the `plastic-branch` status key. It does not replace Pi's footer or suppress Pi's Git branch display, so Git and Plastic information can appear together in nested workspaces. If the Plastic marker exists but neither the selector nor `cm` yields a branch, the footer shows `Plastic branch unavailable`. Non-Plastic directories show no Plastic status.
10
10
 
@@ -74,6 +74,7 @@ The Bash guards run only in Pi processes that actually load this package. A proj
74
74
 
75
75
  - `plastic_branchCreate` supports an explicit parent branch independent of the loaded workspace branch, defaults relative names to the current branch when no parent is supplied, and rejects top-level paths unless `allowRootBranch=true` is explicit.
76
76
  - `plastic_diff` remains a disabled alias by design. Use `plastic_status` for changed-path listing; do not diff as routine post-edit validation or checkin preflight. When change-boundary evidence is needed, use `plastic_diffFile` for one exact file, `plastic_workspaceDiff` with selected paths or explicit `allPending=true`, or `plastic_diffRevisions` for an explicit historical pair. Diff responses use small defaults and caller-controlled `maxChars` bounds. Historical and workspace bytes are copied to ASCII-safe package-owned paths before the backend runs, while normalized headers retain the logical Unicode labels.
77
+ - `plastic_status(machineReadable=true, format="json")` returns parsed `items`, aggregate `summary`, and `itemCount` (`total`, `returned`, `omitted`) rather than requiring agents to parse Plastic rows. It returns at most 100 items by default; set `maxItems` (up to 500) when more are intentional. `includeRaw=true` adds the original unbounded Plastic output for diagnostics, so omit it for normal agent use to conserve context tokens.
77
78
  - Preflight is not routine confirmation. Use it for ambiguous or broad mutation scope, moved/deleted path rewriting, compound operations, or explicit preview requests; otherwise rely on exact targets and the tools' runtime guards.
78
79
  - `plastic_patch` generates review patches with `cm patch`, including `clean` and `integration` filters for branch review workflows. It does not expose patch apply. Unqualified `br:/...` selectors are qualified only with the current workspace selector's exact repository; otherwise pass `br:/<branch>@<repository>@<server>`.
79
80
  - Bash safety rails block `cm diff` and unsafe interactive `cm merge --merge` usage.
@@ -107,13 +108,13 @@ pi install npm:@aefree/pi-plastic
107
108
  Install a pinned GitHub release over HTTPS:
108
109
 
109
110
  ```bash
110
- pi install git:github.com/aefreedman/pi-plastic@v0.5.3
111
+ pi install git:github.com/aefreedman/pi-plastic@v0.5.4
111
112
  ```
112
113
 
113
114
  Equivalent SSH install:
114
115
 
115
116
  ```bash
116
- pi install git:git@github.com/aefreedman/pi-plastic@v0.5.3
117
+ pi install git:git@github.com/aefreedman/pi-plastic@v0.5.4
117
118
  ```
118
119
 
119
120
  To intentionally track the moving default branch instead of a release tag:
@@ -219,7 +219,8 @@ export function createPlasticBranchStatusExtension(overrides: Partial<BranchStat
219
219
  if (!active || generation !== ownedGeneration || refreshSequence !== ownedRefresh) return;
220
220
 
221
221
  const hasSelectorBranch = selectorBranch ? displayBranch(selectorBranch) : false;
222
- if (!hasSelectorBranch) displayUnavailable();
222
+ if (hasSelectorBranch) return;
223
+ displayUnavailable();
223
224
 
224
225
  const controller = new AbortController();
225
226
  confirmationController = controller;
package/index.ts CHANGED
@@ -94,6 +94,8 @@ const TOOL_CONFIG: Partial<Record<PlasticExportName, ToolConfig>> = {
94
94
  normalizeWorkdirAliases(input);
95
95
  assignAlias(input, "includeRevId", ["include_rev_id"]);
96
96
  assignAlias(input, "machineReadable", ["machine_readable", "machine"]);
97
+ assignAlias(input, "maxItems", ["max_items"]);
98
+ assignAlias(input, "includeRaw", ["include_raw"]);
97
99
  return input;
98
100
  },
99
101
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aefree/pi-plastic",
3
- "version": "0.5.3",
3
+ "version": "0.5.5",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "node": ">=22.19.0"
@@ -34,7 +34,7 @@
34
34
  ]
35
35
  },
36
36
  "scripts": {
37
- "test": "npx --yes tsx tests/plastic-tool-validation.ts && npx --yes tsx tests/plastic-checkin-path-resolution.ts && npx --yes tsx tests/plastic-diff-workflow.test.ts && npx --yes tsx tests/plastic-workspace-diff.test.ts && npx --yes tsx tests/plastic-process-lifecycle.test.ts && npx --yes tsx tests/plastic-patch-command.test.ts && npx --yes tsx tests/plastic-extension-registration.test.ts && npx --yes tsx tests/plastic-dynamic-tool-loading.test.ts && npx --yes tsx tests/plastic-tool-rendering.test.ts && npx --yes tsx tests/plastic-branch-query.test.ts && npx --yes tsx tests/plastic-workspace.test.ts && npx --yes tsx tests/plastic-mutation-execution.test.ts && npx --yes tsx tests/file-discovery-filter.test.ts && npx --yes tsx tests/file-discovery-filter-registration.test.ts && npx --yes tsx tests/packed-copy-owner-conflict.test.ts && npx --yes tsx tests/plastic-branch-status.test.ts && npx --yes tsx tests/plastic-constrained-sampling.test.ts && npx --yes tsx tests/bash-cm-diff-guard-validation.ts",
37
+ "test": "npx --yes tsx tests/plastic-tool-validation.ts && npx --yes tsx tests/plastic-status-machine-readable.test.ts && npx --yes tsx tests/plastic-checkin-path-resolution.ts && npx --yes tsx tests/plastic-diff-workflow.test.ts && npx --yes tsx tests/plastic-workspace-diff.test.ts && npx --yes tsx tests/plastic-process-lifecycle.test.ts && npx --yes tsx tests/plastic-patch-command.test.ts && npx --yes tsx tests/plastic-extension-registration.test.ts && npx --yes tsx tests/plastic-dynamic-tool-loading.test.ts && npx --yes tsx tests/plastic-tool-rendering.test.ts && npx --yes tsx tests/plastic-branch-query.test.ts && npx --yes tsx tests/plastic-workspace.test.ts && npx --yes tsx tests/plastic-mutation-execution.test.ts && npx --yes tsx tests/file-discovery-filter.test.ts && npx --yes tsx tests/file-discovery-filter-registration.test.ts && npx --yes tsx tests/packed-copy-owner-conflict.test.ts && npx --yes tsx tests/plastic-branch-status.test.ts && npx --yes tsx tests/plastic-constrained-sampling.test.ts && npx --yes tsx tests/bash-cm-diff-guard-validation.ts",
38
38
  "test:live": "npx --yes tsx tests/plastic-live-smoke.test.ts",
39
39
  "test:live:branch-create": "npx --yes tsx tests/plastic-branch-create-live.test.ts",
40
40
  "eval:tool-loading": "npx --yes tsx evals/tool-loading/run-eval.ts"
@@ -326,6 +326,20 @@ type PendingItemSummary = {
326
326
  privatePaths: string[];
327
327
  };
328
328
 
329
+ type MachineReadableStatusItem = {
330
+ statusCode: string;
331
+ kind: PendingItemKind;
332
+ path: string;
333
+ isDirectory: boolean;
334
+ revisionId?: string;
335
+ sourcePath?: string;
336
+ };
337
+
338
+ type MachineReadableStatusSummary = Omit<PendingItemSummary, "privatePaths">;
339
+
340
+ const MACHINE_READABLE_STATUS_DEFAULT_MAX_ITEMS = 100;
341
+ const MACHINE_READABLE_STATUS_MAX_ITEMS = 500;
342
+
329
343
  type PrivateAutoAddSelection = {
330
344
  candidatePaths: string[];
331
345
  blockedPaths: Array<{ path: string; reason: string }>;
@@ -685,6 +699,22 @@ const getMachineReadablePendingItems = async (workdir?: string): Promise<Pending
685
699
  return parseMachineReadablePendingItems(output, cwd);
686
700
  };
687
701
 
702
+ const toMachineReadableStatusItems = (pendingItems: PendingItem[]): MachineReadableStatusItem[] =>
703
+ pendingItems.map((item) => ({
704
+ statusCode: item.statusCode,
705
+ kind: item.kind,
706
+ path: item.workspacePath,
707
+ isDirectory: item.isDirectory,
708
+ ...(item.revisionId ? { revisionId: item.revisionId } : {}),
709
+ ...(item.sourceWorkspacePath ? { sourcePath: item.sourceWorkspacePath } : {}),
710
+ }));
711
+
712
+ const toMachineReadableStatusSummary = (pendingItems: PendingItem[], cwd: string): MachineReadableStatusSummary =>
713
+ {
714
+ const { privatePaths: _privatePaths, ...summary } = summarizePendingItems(pendingItems, cwd);
715
+ return summary;
716
+ };
717
+
688
718
  const summarizePendingItems = (pendingItems: PendingItem[], cwd?: string): PendingItemSummary =>
689
719
  {
690
720
  const summary: PendingItemSummary = {
@@ -2375,7 +2405,9 @@ export const status = tool({
2375
2405
  args: {
2376
2406
  workdir: workdirArg,
2377
2407
  includeRevId: tool.schema.boolean().optional().describe("Include revision IDs in the status output when supported."),
2378
- machineReadable: tool.schema.boolean().optional().describe("Return machine-readable status output when supported."),
2408
+ machineReadable: tool.schema.boolean().optional().describe("Return parsed, machine-readable pending status records when supported."),
2409
+ maxItems: tool.schema.number().int().min(1).max(MACHINE_READABLE_STATUS_MAX_ITEMS).optional().describe(`Maximum parsed items in machine-readable JSON (default ${MACHINE_READABLE_STATUS_DEFAULT_MAX_ITEMS}, maximum ${MACHINE_READABLE_STATUS_MAX_ITEMS}).`),
2410
+ includeRaw: tool.schema.boolean().optional().describe("Include unbounded raw machine-readable Plastic output in JSON diagnostics. Applies only when machineReadable=true."),
2379
2411
  short: tool.schema.boolean().optional().describe("Use short status output."),
2380
2412
  format: outputFormatArg,
2381
2413
  },
@@ -2396,15 +2428,32 @@ export const status = tool({
2396
2428
 
2397
2429
  if (args.machineReadable)
2398
2430
  {
2399
- cmdArgs.push("--machinereadable");
2431
+ // Package-owned separators make paths with whitespace unambiguous, and
2432
+ // revision IDs let agents safely identify the base of moved/deleted items.
2433
+ if (!cmdArgs.includes("--includeRevId"))
2434
+ {
2435
+ cmdArgs.push("--includeRevId");
2436
+ }
2437
+ cmdArgs.push("--machinereadable", `--fieldseparator=${STATUS_FIELD_SEPARATOR}`);
2400
2438
  const machineOutput = await runCmRaw(cmdArgs, args.workdir);
2439
+ const cwd = args.workdir ?? process.cwd();
2440
+ const pendingItems = parseMachineReadablePendingItems(machineOutput, cwd);
2441
+ const maxItems = args.maxItems ?? MACHINE_READABLE_STATUS_DEFAULT_MAX_ITEMS;
2442
+ const returnedItems = pendingItems.slice(0, maxItems);
2401
2443
  return toStructuredResult(
2402
2444
  "status",
2403
2445
  format,
2404
2446
  machineOutput,
2405
2447
  {
2406
- rawOutput: machineOutput,
2407
2448
  machineReadable: true,
2449
+ items: toMachineReadableStatusItems(returnedItems),
2450
+ itemCount: {
2451
+ total: pendingItems.length,
2452
+ returned: returnedItems.length,
2453
+ omitted: pendingItems.length - returnedItems.length,
2454
+ },
2455
+ summary: toMachineReadableStatusSummary(pendingItems, cwd),
2456
+ ...(args.includeRaw ? { rawOutput: machineOutput } : {}),
2408
2457
  },
2409
2458
  args.workdir,
2410
2459
  );
@@ -78,7 +78,8 @@ const parseSelectorValue = (line: string, key: string): string | undefined => {
78
78
  /** Parse the local selector without treating it as proof that a workspace exists. */
79
79
  export function parsePlasticSelector(content: string): ParseOutcome<PlasticSelector> {
80
80
  let repository: string | undefined;
81
- let branch: string | undefined;
81
+ let smartBranch: string | undefined;
82
+ let explicitBranch: string | undefined;
82
83
  let sawBranchKey = false;
83
84
 
84
85
  for (const line of content.split(/\r?\n/)) {
@@ -86,13 +87,18 @@ export function parsePlasticSelector(content: string): ParseOutcome<PlasticSelec
86
87
  if (/^\s*repository(?:\s|$)/i.test(line)) repository = parseSelectorValue(line, "repository");
87
88
  if (/^\s*smartbranch(?:\s|$)/i.test(line)) {
88
89
  sawBranchKey = true;
89
- branch = parseSelectorValue(line, "smartbranch");
90
+ smartBranch = parseSelectorValue(line, "smartbranch");
91
+ }
92
+ if (/^\s*br(?:\s|$)/i.test(line)) {
93
+ sawBranchKey = true;
94
+ explicitBranch = parseSelectorValue(line, "br");
90
95
  }
91
96
  }
92
97
 
93
98
  if (!sawBranchKey) return { kind: "not_found" };
99
+ const branch = smartBranch ?? explicitBranch;
94
100
  if (!branch || /[\u0000-\u001f\u007f]/.test(branch)) {
95
- return { kind: "malformed", reason: "Selector smartbranch is blank or contains control characters." };
101
+ return { kind: "malformed", reason: "Selector branch is blank or contains control characters." };
96
102
  }
97
103
 
98
104
  return {