@fro.bot/systematic 3.5.1 → 3.5.3

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/ATTRIBUTIONS.md CHANGED
@@ -71,10 +71,10 @@ SOFTWARE.
71
71
 
72
72
  ## pbakaus/impeccable — Apache 2.0
73
73
 
74
- **Source repository:** [https://github.com/pbakaus/impeccable](https://github.com/pbakaus/impeccable)
75
- **Pinned commit:** `642f03d5a10eb3deb91bd511241e387e23b9aa39`
76
- **License:** Apache 2.0
77
- **Copyright:** Paul Bakaus
74
+ **Source repository:** [https://github.com/pbakaus/impeccable](https://github.com/pbakaus/impeccable)
75
+ **Pinned commit:** `642f03d5a10eb3deb91bd511241e387e23b9aa39`
76
+ **License:** Apache 2.0
77
+ **Copyright:** Paul Bakaus
78
78
 
79
79
  ### Files derived
80
80
 
@@ -272,7 +272,7 @@ The following is the full Apache License, Version 2.0 text, reproduced here in c
272
272
  ## vercel-labs/agent-browser — Apache-2.0
273
273
 
274
274
  **Source repository:** [`vercel-labs/agent-browser`](https://github.com/vercel-labs/agent-browser)
275
- **Pinned version:** `v0.33.0` (npm `agent-browser@0.33.0`)
275
+ **Pinned version:** `v0.33.1` (npm `agent-browser@0.33.1`)
276
276
  **License:** Apache-2.0
277
277
  **Copyright:** Copyright 2025 Vercel Inc.
278
278
 
@@ -289,14 +289,14 @@ Vendored verbatim from the pinned npm package (`node_modules/agent-browser/skill
289
289
  Refresh is **automated** via the pinned `devDependency` + `agent-browser:build` regeneration + `agent-browser:drift` CI gate:
290
290
 
291
291
  1. Renovate opens a version-bump PR when upstream releases a new `agent-browser` version.
292
- 2. The `postUpgradeTasks` in `renovate.json5` runs `bun run agent-browser:build`, which regenerates `skills/agent-browser/SKILL.md` from the newly pinned version and lands the updated stub in the same PR.
292
+ 2. The `postUpgradeTasks` in `renovate.json5` runs `bun run postupgrade` (which runs `bun run agent-browser:build`), which regenerates `skills/agent-browser/SKILL.md` from the newly pinned version and lands the updated stub in the same PR.
293
293
  3. The CI `agent-browser:drift` step verifies the committed file matches the pinned package and fails the PR if it does not.
294
294
 
295
295
  This is fully automated — no human adaptation pass is required, in contrast to `obra/superpowers` which is manually refreshed because its content is hand-adapted on each bump.
296
296
 
297
297
  ### Upstream Apache-2.0 license text
298
298
 
299
- The following is the full Apache License, Version 2.0 text from `agent-browser@0.33.0`, reproduced here in compliance with the license's notice requirements:
299
+ The following is the full Apache License, Version 2.0 text from `agent-browser@0.33.1`, reproduced here in compliance with the license's notice requirements:
300
300
 
301
301
  ```
302
302
  Apache License
package/dist/index.js CHANGED
@@ -1309,6 +1309,15 @@ var DEFAULT_LIMITS = {
1309
1309
  maxTotalFileBytes: 16777216,
1310
1310
  maxPathBytes: 4096
1311
1311
  };
1312
+ var UNTRACKED_FILES_ARGS = [
1313
+ "ls-files",
1314
+ "--others",
1315
+ "--exclude-standard",
1316
+ "--full-name",
1317
+ "-z",
1318
+ "--",
1319
+ ":/"
1320
+ ];
1312
1321
  function unavailable(result) {
1313
1322
  return { status: "unavailable", reasonCode: result.reasonCode };
1314
1323
  }
@@ -1420,15 +1429,15 @@ function runCommand(runner, args2, cwd, limits) {
1420
1429
  }
1421
1430
  return { status: "ok", output: { stdout: result.stdout } };
1422
1431
  }
1423
- function readRevision(runner, root, limits) {
1424
- const branch = runCommand(runner, ["symbolic-ref", "--short", "HEAD"], root, limits);
1432
+ function readRevision(runner, context, limits) {
1433
+ const branch = runCommand(runner, ["symbolic-ref", "--short", "HEAD"], context.commandDirectory, limits);
1425
1434
  if (branch.status === "error" && branch.reasonCode !== "command-failed") {
1426
1435
  return branch;
1427
1436
  }
1428
1437
  const branchValue = branch.status === "ok" ? branch.output.stdout.trim() : "detached";
1429
1438
  let head;
1430
1439
  try {
1431
- head = runner(["rev-parse", "--verify", "HEAD"], root, limits.maxCommandOutputBytes, limits.maxCommandTimeoutMs);
1440
+ head = runner(["rev-parse", "--verify", "HEAD"], context.commandDirectory, limits.maxCommandOutputBytes, limits.maxCommandTimeoutMs);
1432
1441
  } catch {
1433
1442
  return { status: "error", reasonCode: "command-failed" };
1434
1443
  }
@@ -1439,7 +1448,7 @@ function readRevision(runner, root, limits) {
1439
1448
  return { status: "error", reasonCode: "command-output-limit" };
1440
1449
  }
1441
1450
  const headValue = head.status === 0 ? head.stdout.trim() : "unborn";
1442
- const inside = runCommand(runner, ["rev-parse", "--is-inside-work-tree"], root, limits);
1451
+ const inside = runCommand(runner, ["rev-parse", "--is-inside-work-tree"], context.commandDirectory, limits);
1443
1452
  if (inside.status === "error")
1444
1453
  return inside;
1445
1454
  return {
@@ -1454,8 +1463,8 @@ function readRevision(runner, root, limits) {
1454
1463
  function isEnoent(error) {
1455
1464
  return error !== null && typeof error === "object" && "code" in error && error.code === "ENOENT";
1456
1465
  }
1457
- function appendSubmoduleFact(hash, runner, root, relativePath, limits, totalBytes) {
1458
- const submodule = runCommand(runner, ["-C", relativePath, "rev-parse", "--verify", "HEAD"], root, limits);
1466
+ function appendSubmoduleFact(hash, runner, submodulePath, relativePath, limits, totalBytes) {
1467
+ const submodule = runCommand(runner, ["rev-parse", "--verify", "HEAD"], submodulePath, limits);
1459
1468
  if (submodule.status === "error")
1460
1469
  return submodule;
1461
1470
  const revision = submodule.output.stdout.trim();
@@ -1512,9 +1521,9 @@ function appendRegularFact(hash, fileReader, absolutePath, relativePath, stat, l
1512
1521
  hash.update("\x00");
1513
1522
  return { status: "ok", totalBytes: totalBytes + content.byteLength };
1514
1523
  }
1515
- function appendFileFactV2(hash, runner, fileReader, symlinkReader, statReader, root, entry, limits, totalBytes) {
1524
+ function appendFileFactV2(hash, runner, fileReader, symlinkReader, statReader, context, entry, limits, totalBytes) {
1516
1525
  const { mode, relativePath } = entry;
1517
- const absolutePath = safePath(root, relativePath);
1526
+ const absolutePath = safePath(context.worktreeRoot, relativePath);
1518
1527
  if (!absolutePath || outputBytes(relativePath) > limits.maxPathBytes) {
1519
1528
  return { status: "error", reasonCode: "file-read-failed" };
1520
1529
  }
@@ -1525,20 +1534,20 @@ function appendFileFactV2(hash, runner, fileReader, symlinkReader, statReader, r
1525
1534
  return isEnoent(error) ? { status: "ok", totalBytes } : { status: "error", reasonCode: "file-read-failed" };
1526
1535
  }
1527
1536
  if (mode === "160000")
1528
- return appendSubmoduleFact(hash, runner, root, relativePath, limits, totalBytes);
1537
+ return appendSubmoduleFact(hash, runner, absolutePath, relativePath, limits, totalBytes);
1529
1538
  if (stat.isSymbolicLink) {
1530
1539
  return appendSymlinkFact(hash, symlinkReader, absolutePath, relativePath, limits, totalBytes);
1531
1540
  }
1532
1541
  return appendRegularFact(hash, fileReader, absolutePath, relativePath, stat, limits, totalBytes);
1533
1542
  }
1534
- function readWorktreeRevisionV2(runner, fileReader, symlinkReader, statReader, root, limits) {
1535
- const tracked = runCommand(runner, ["ls-files", "-z"], root, limits);
1543
+ function readWorktreeRevisionV2(runner, fileReader, symlinkReader, statReader, context, limits) {
1544
+ const tracked = runCommand(runner, ["ls-files", "--full-name", "-z", "--", ":/"], context.commandDirectory, limits);
1536
1545
  if (tracked.status === "error")
1537
1546
  return tracked;
1538
- const staged = runCommand(runner, ["ls-files", "--stage", "-z"], root, limits);
1547
+ const staged = runCommand(runner, ["ls-files", "--stage", "--full-name", "-z", "--", ":/"], context.commandDirectory, limits);
1539
1548
  if (staged.status === "error")
1540
1549
  return staged;
1541
- const untracked = runCommand(runner, ["ls-files", "--others", "--exclude-standard", "-z"], root, limits);
1550
+ const untracked = runCommand(runner, UNTRACKED_FILES_ARGS, context.commandDirectory, limits);
1542
1551
  if (untracked.status === "error")
1543
1552
  return untracked;
1544
1553
  const stageEntries = parseStageEntries(staged.output.stdout, limits.maxPathBytes);
@@ -1558,17 +1567,17 @@ function readWorktreeRevisionV2(runner, fileReader, symlinkReader, statReader, r
1558
1567
  hash.update("systematic:opencode-observer:worktree:v2\x00");
1559
1568
  let totalBytes = 0;
1560
1569
  for (const relativePath of paths) {
1561
- const result = appendFileFactV2(hash, runner, fileReader, symlinkReader, statReader, root, { relativePath, mode: modes.get(relativePath) ?? "" }, limits, totalBytes);
1570
+ const result = appendFileFactV2(hash, runner, fileReader, symlinkReader, statReader, context, { relativePath, mode: modes.get(relativePath) ?? "" }, limits, totalBytes);
1562
1571
  if (result.status === "error")
1563
1572
  return result;
1564
1573
  totalBytes = result.totalBytes;
1565
1574
  }
1566
1575
  return { status: "ok", digest: hash.digest("hex") };
1567
1576
  }
1568
- function readCommitClosure(runner, root, limits) {
1577
+ function readCommitClosure(runner, context, limits) {
1569
1578
  let diff;
1570
1579
  try {
1571
- diff = runner(["diff", "--quiet", "HEAD", "--", "."], root, limits.maxCommandOutputBytes, limits.maxCommandTimeoutMs);
1580
+ diff = runner(["diff", "--quiet", "HEAD", "--", ":/"], context.commandDirectory, limits.maxCommandOutputBytes, limits.maxCommandTimeoutMs);
1572
1581
  } catch {
1573
1582
  return { status: "error", reasonCode: "command-failed" };
1574
1583
  }
@@ -1581,7 +1590,7 @@ function readCommitClosure(runner, root, limits) {
1581
1590
  if (diff.status !== 0 && diff.status !== 1) {
1582
1591
  return { status: "error", reasonCode: "command-failed" };
1583
1592
  }
1584
- const untracked = runCommand(runner, ["ls-files", "--others", "--exclude-standard", "-z"], root, limits);
1593
+ const untracked = runCommand(runner, UNTRACKED_FILES_ARGS, context.commandDirectory, limits);
1585
1594
  if (untracked.status === "error")
1586
1595
  return untracked;
1587
1596
  return {
@@ -1765,8 +1774,8 @@ function readCheckReadback(runner, root, limits, pullRequest) {
1765
1774
  const state = checkState(parsedChecks.value, limits);
1766
1775
  return state === undefined ? { status: "unavailable", reasonCode: "remote-missing-field" } : remoteResultFromPullRequest(pullRequest, { checkState: state });
1767
1776
  }
1768
- function readRemoteSnapshot(runner, operation, phase, root, limits) {
1769
- return operation === "push" ? readPushRemoteSnapshot(runner, phase, root, limits) : readRemotePullRequest(runner, operation, root, limits);
1777
+ function readRemoteSnapshot(runner, operation, phase, commandDirectory, limits) {
1778
+ return operation === "push" ? readPushRemoteSnapshot(runner, phase, commandDirectory, limits) : readRemotePullRequest(runner, operation, commandDirectory, limits);
1770
1779
  }
1771
1780
  function createOpencodeOperationObserver(options) {
1772
1781
  const limits = mergeLimits(options.limits);
@@ -1801,13 +1810,17 @@ function createOpencodeOperationObserver(options) {
1801
1810
  if (root.length === 0 || outputBytes(root) > limits.maxPathBytes) {
1802
1811
  return { status: "unavailable", reasonCode: "target-unavailable" };
1803
1812
  }
1804
- const repository = readRevision(runner, root, limits);
1813
+ const context = {
1814
+ commandDirectory: targetDirectory,
1815
+ worktreeRoot: root
1816
+ };
1817
+ const repository = readRevision(runner, context, limits);
1805
1818
  if (repository.status === "error")
1806
1819
  return unavailable(repository);
1807
- const worktree = readWorktreeRevisionV2(runner, fileReader, symlinkReader, statReader, root, limits);
1820
+ const worktree = readWorktreeRevisionV2(runner, fileReader, symlinkReader, statReader, context, limits);
1808
1821
  if (worktree.status === "error")
1809
1822
  return unavailable(worktree);
1810
- const closure = readCommitClosure(runner, root, limits);
1823
+ const closure = readCommitClosure(runner, context, limits);
1811
1824
  if (closure.status === "error")
1812
1825
  return unavailable(closure);
1813
1826
  return {
@@ -1829,7 +1842,7 @@ function createOpencodeOperationObserver(options) {
1829
1842
  if (root.length === 0 || outputBytes(root) > limits.maxPathBytes) {
1830
1843
  return { status: "unavailable", reasonCode: "target-unavailable" };
1831
1844
  }
1832
- return readRemoteSnapshot(remoteRunner, operation, phase, root, limits);
1845
+ return readRemoteSnapshot(remoteRunner, operation, phase, targetDirectory, limits);
1833
1846
  }
1834
1847
  };
1835
1848
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fro.bot/systematic",
3
- "version": "3.5.1",
3
+ "version": "3.5.3",
4
4
  "description": "Compound-engineering loops for OpenCode, Pi, and Claude Code",
5
5
  "type": "module",
6
6
  "homepage": "https://fro.bot/systematic",
@@ -55,6 +55,7 @@
55
55
  "registry:drift": "bun scripts/generate-registry.ts --check",
56
56
  "registry:validate": "bun scripts/build-registry.ts --validate-only",
57
57
  "claude-code:build": "bun scripts/build-claude-code-plugin.ts",
58
+ "postupgrade": "bun run build && bun run agent-browser:build",
58
59
  "prepublishOnly": "bun run build && bun run schema:generate"
59
60
  },
60
61
  "keywords": [
@@ -92,14 +93,14 @@
92
93
  "devDependencies": {
93
94
  "@biomejs/biome": "2.5.6",
94
95
  "@earendil-works/pi-coding-agent": "0.80.10",
95
- "@opencode-ai/plugin": "1.18.8",
96
- "@opencode-ai/sdk": "1.18.8",
96
+ "@opencode-ai/plugin": "1.18.9",
97
+ "@opencode-ai/sdk": "1.18.9",
97
98
  "@semantic-release/exec": "7.1.0",
98
99
  "@tintinweb/pi-subagents": "0.14.3",
99
100
  "@types/bun": "latest",
100
101
  "@types/js-yaml": "4.0.9",
101
102
  "@types/node": "24.13.3",
102
- "agent-browser": "0.33.0",
103
+ "agent-browser": "0.33.1",
103
104
  "ajv": "8.20.0",
104
105
  "ajv-formats": "3.0.1",
105
106
  "conventional-changelog-conventionalcommits": "9.3.1",
@@ -17,6 +17,7 @@ Every subagent dispatch should carry the same bounded brief, expressed through t
17
17
  Specialist: systematic-implementer
18
18
  Objective: Implement the auth module
19
19
  Scope: Make only the changes needed for the auth module.
20
+ Workspace: /absolute/path/to/expected-repository-or-worktree
20
21
  Return: List the changed files and summarize the implementation.
21
22
  ```
22
23
 
@@ -24,6 +25,7 @@ Return: List the changed files and summarize the implementation.
24
25
  - **Specialist** — the registered agent or persona best suited to the work (for example, an implementer or researcher)
25
26
  - **Objective** — a concise statement of the outcome
26
27
  - **Scope** — full instructions, boundaries, dependencies, and files in scope
28
+ - **Workspace** — for any write-capable dispatch, the explicit expected repository or worktree root
27
29
  - **Return** — the result format expected from the specialist
28
30
 
29
31
  **Optional brief elements:**
@@ -61,6 +63,7 @@ After the first result, dispatch:
61
63
  Objective: Implement caching
62
64
  Scope: Implement caching based on the research result below.
63
65
  Input: Include the first specialist's returned research.
66
+ Workspace: /absolute/path/to/expected-repository-or-worktree
64
67
  Return: List changed files and summarize the implementation.
65
68
  ```
66
69
 
@@ -110,6 +113,12 @@ When dispatching units in parallel, include these instructions in each specialis
110
113
 
111
114
  > Do not stage files (`git add`), create commits, or run the project test suite. Leave staging and committing to the orchestrator or caller.
112
115
 
116
+ ## Workspace Attestation
117
+
118
+ For every write-capable dispatch, the specialist must verify and return its actual repository or worktree root using available repository tooling (for example, `git rev-parse --show-toplevel` when Git is available) and an inventory of changed files. The orchestrator must independently verify the expected worktree's status and diff, plus the source or primary checkout's status, before accepting the result; a path in the brief or a specialist's self-report is not evidence.
119
+
120
+ If writes landed in the wrong checkout, stop dependent dispatches, inspect both diffs, transfer only intended changes deliberately, and restore only confirmed accidental agent edits. Do not blindly copy or reset.
121
+
113
122
  ## Result Synthesis
114
123
 
115
124
  After specialists complete, the orchestrator synthesizes results:
@@ -138,6 +147,7 @@ After specialists complete, the orchestrator synthesizes results:
138
147
  | Background unavailable | Foreground only — serial or batched |
139
148
  | Specialist fails | Diagnose, correct the brief, then re-dispatch or resume the prior session where supported |
140
149
  | File collision detected post-parallel | Stage non-colliding files (if workflow owns git ops), re-run colliding units serially |
150
+ | Write-capable dispatch | Include the expected root; require actual-root and changed-file attestation; independently verify both worktrees before acceptance |
141
151
 
142
152
  ## Common Mistakes
143
153