@awebai/oats 0.29.0 → 0.29.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.
package/bin/oats.mjs CHANGED
@@ -958,7 +958,8 @@ function instanceCmd() {
958
958
  console.log(` upstream: ${observed.upstream.ref ? `${observed.upstream.ref} +${observed.upstream.ahead} -${observed.upstream.behind}` : "none (ahead/behind unknown)"}`);
959
959
  console.log(` base: ${observed.base.ref ? `${observed.base.ref} +${observed.base.ahead} -${observed.base.behind} (merge-base ${observed.base.mergeBase?.slice(0, 12)})` : "unknown"}`);
960
960
  console.log(` files: ${observed.files.length} (${Object.entries(observed.summary).filter(([, n]) => n).map(([k, n]) => `${n} ${k}`).join(", ") || "clean"})`);
961
- for (const f of observed.files) console.log(` ${f.xy} ${f.origPath ? `${f.origPath} -> ` : ""}${f.path} [${f.id}]`);
961
+ const lines = (f) => (f.binary ? " binary" : Number.isInteger(f.additions) ? ` +${f.additions} -${f.deletions}` : "");
962
+ for (const f of observed.files) console.log(` ${f.xy} ${f.origPath ? `${f.origPath} -> ` : ""}${f.path}${lines(f)} [${f.id}]`);
962
963
  for (const n of observed.notes) console.log(` note: ${n}`);
963
964
  return;
964
965
  }
@@ -19,9 +19,12 @@ prints exactly one JSON object on stdout:
19
19
 
20
20
  `version` is the installed package's exact semver (e.g. `0.20.0`).
21
21
  The Desktop accepts `desktopApi === 1` and gates on the kernel feature
22
- `packages-no-approval` (semver range `>=0.25.8 <0.27.0`: the floor admits the
23
- main-branch kernel before 0.26.0 is tagged; the feature fence is the real gate).
24
- Earlier bands were `>=0.22.0 <0.26.0` (Desktop 0.25) and `>=0.22.0 <0.24.0`
22
+ `packages-no-approval` (semver range `>=0.25.8 <0.30.0`, spelled once in
23
+ `packages/desktop/cli-locator.mjs` `ACCEPT_RANGE`: the floor admits the
24
+ main-branch kernel before 0.26.0 was tagged; the feature fences are the real gate,
25
+ and 0.29's reads are gated on `automations` and `desktop-facts`).
26
+ Earlier bands were `>=0.25.8 <0.29.0` (Desktop 0.28), `<0.28.0` (Desktop 0.27),
27
+ `<0.27.0` (Desktop 0.26), `>=0.22.0 <0.26.0` (Desktop 0.25) and `>=0.22.0 <0.24.0`
25
28
  (Desktop 0.23). It does not establish complete
26
29
  UI, backend, plugin, retirement or recovery parity; capability checks and explicit
27
30
  refusals below remain authoritative.
@@ -483,7 +486,7 @@ un-materialized tree → `E_NO_WORKTREE`.
483
486
  "base":{"ref":"origin/main","source":"origin/HEAD","mergeBase":"<oid>","ahead":2,"behind":0},
484
487
  "remote":{"name":"origin","url":"git@github.com:acme/one.git","host":"github.com","path":"acme/one","source":"branch-upstream|origin"},
485
488
  "summary":{"changed":1,"renamed":1,"copied":0,"unmerged":0,"untracked":1},
486
- "files":[{"id":"<24 hex>","kind":"renamed","xy":"R.","submodule":false,"score":"R100","path":"src/new.txt","origPath":"src/old.txt"}],
489
+ "files":[{"id":"<24 hex>","kind":"renamed","xy":"R.","submodule":false,"score":"R100","path":"src/new.txt","origPath":"src/old.txt","additions":84,"deletions":3,"binary":false}],
487
490
  "notes":[]}
488
491
  ```
489
492
 
@@ -496,6 +499,19 @@ un-materialized tree → `E_NO_WORKTREE`.
496
499
  unmerged | untracked. Ignored files are not listed.
497
500
  - `files[].id` is **opaque**, minted under (`revision`, `indexRevision`). It is
498
501
  the only way to ask for a diff.
502
+ - `files[].additions`, `deletions` and `binary` (0.29.1, additive;
503
+ `instanceGitApi` stays 1) are each file's line counts.
504
+ - They come from one `git diff <revision> --numstat -z -M` per observation:
505
+ the working tree against the observed commit, **staged and unstaged
506
+ combined**. That is the baseline of the status letters and of
507
+ `oats instance diff`.
508
+ - An unborn tree counts against the empty tree. A rename counts on its new
509
+ `path`.
510
+ - A binary file is `additions: null, deletions: null, binary: true`. An
511
+ untracked file (no baseline; its contents are not read) and a submodule
512
+ are all `null`.
513
+ - If the count itself fails, every entry is `null` and `notes` says line
514
+ counts are unavailable. `null` means unknown, never zero.
499
515
  - `remote` (0.24.8+): the branch's configured remote (`source: branch-upstream`),
500
516
  else `origin`, else `null` — never invented. `host`/`path` are **parsed** from
501
517
  the URL (ssh/https forms; `.git` stripped) so an ADE can choose a forge backend
@@ -0,0 +1,79 @@
1
+ # OATS 0.29.1
2
+
3
+ ## Added
4
+
5
+ - **Per-file line counts in `oats instance git`.** Each tracked file entry has
6
+ `additions` and `deletions` (integers) and `binary`, so the Desktop can show
7
+ `+84 −3` beside a changed file. They come from one `git diff --numstat`
8
+ against the observed commit, counting staged and unstaged changes together,
9
+ which is the same baseline as the status letters and `oats instance diff`.
10
+ - A binary file has `additions: null, deletions: null, binary: true`.
11
+ - An untracked file or a submodule has all three `null`.
12
+ - A rename is counted on its new path.
13
+ - `instanceGitApi` stays 1: the fields are additive. The text output shows
14
+ `+N -M` after each path. See
15
+ [desktop-cli-api.md](../desktop-cli-api.md#instance-git-state-oats-instance-gitdiff-instancegitapi-1-oats-0247).
16
+
17
+ ## Changed
18
+
19
+ - **The `oats-setup-admin` soul is never harvested.** Its `soul.yaml` opts out
20
+ of OKF harvest with `knowledge: { harvest: off }`: its sessions work on the
21
+ deployment's own config and carry deployment specifics that must not reach
22
+ the shared knowledge base. oats.okf 4.0.0 makes a soul's opt-out absolute, so
23
+ a deployment that switches harvest on (`settings.oats.okf.harvest: on`)
24
+ still registers no source, captures nothing and takes no custody for its
25
+ instances. `oats okf harvest-status` in one of its homes reports
26
+ `harvest: off` with the soul's reason.
27
+
28
+ ## Desktop
29
+
30
+ The Desktop now shows the kernel's 0.29 Desktop facts and the Workspace v4
31
+ design's Git & GitHub section. On an older kernel, each fact is absent and its
32
+ view shows nothing new.
33
+
34
+ - **Souls say when a spawn here would refuse** (#228, #231). A soul the kernel
35
+ reports as not spawnable shows "Can't spawn here" with the kernel's reason on
36
+ its card and page, and its Spawn and Schedule… are disabled with that reason.
37
+ A soul page offers Open file for a hosted soul file, and shows the file's path
38
+ when it has no web address.
39
+ - **The server passes the kernel's Desktop facts through** (#230). Its souls,
40
+ capabilities, workspace status and instance projections keep those fields,
41
+ bounded, instead of dropping them. A soul's spawn default is nested as
42
+ `spawnDefault`, so it is never read as the soul's own harness or model.
43
+ - **Capability pages** (#232) show the description, what the capability
44
+ provides (skills, commands and hooks by name, or "Not listable"), its file and
45
+ its fingerprint.
46
+ - **Setup names the files and this computer's state** (#233). It names the
47
+ workspace and membership files, and links them when they are hosted. A member
48
+ panel has Open repository and its clone on this computer. This computer lists
49
+ the lock file, disabled souls and each member's clone, or "not cloned here".
50
+ Packages with a newer version say "X available".
51
+ - **Setup shows the workspace defaults** (#237): each slot and its source ("not
52
+ set" or "none" when empty), the default capabilities, and what each team adds
53
+ or turns off.
54
+ - **The instance page** (#235) says when the session started, where its model
55
+ came from (the soul, the spawn, the start, the launch configuration or the
56
+ harness default), and its messaging address.
57
+ - **Each local instance's pull request on its roster row** (#234, #236). A new
58
+ `POST /api/forge-roster` maps each instance to its member's clone from the
59
+ kernel's workspace status and reads the branch's PR with `gh`, cached for a
60
+ minute. The row shows `#N`, with draft, merged or closed in words. Remote
61
+ instances, and hosts without a signed-in `gh`, show no badge.
62
+ - **W6 Git & GitHub** (#239, #240, #241, #242, #243). The instance's Git panel
63
+ follows the design:
64
+ - **Branch** shows the work mode, the distance from the default branch and
65
+ whether the tree is clean, with the rest behind Details.
66
+ - **Changes** is a list of status letters and paths, with Open diff.
67
+ - **Pull request** shows the title and state, the issues it closes and the
68
+ checks: failing first, with how long each took. It also shows the review
69
+ decision with the count of unresolved review threads, and has Open on
70
+ GitHub.
71
+ - Check marks are icons. The thread count is read only for the open
72
+ instance's PR, never for the roster.
73
+ - **Automations through the app proxy** (#224). `/api/automations` has its own
74
+ proxy route, pinned to the verified workspace like the other workspace
75
+ endpoints. It has a 90 s deadline, so `trigger test` (which polls the forge)
76
+ is no longer cut off in the packaged app. The automations docs describe the
77
+ API and the proxy.
78
+ - **Docs** (#227): the documented Desktop band is `>=0.25.8 <0.30.0`.
79
+ - The Desktop still accepts OATS CLIs `>=0.25.8 <0.30.0`.
@@ -138,6 +138,33 @@ function remoteOf(work, branch) {
138
138
  if (!url) return null;
139
139
  return { name, url, ...parseRemoteUrl(url), source: configured && configured !== "." ? "branch-upstream" : "origin" };
140
140
  }
141
+ /** Per-file line counts (0.29.1), from ONE `git diff <captured HEAD> --numstat -z -M`: the working tree
142
+ * against the observed commit, staged and unstaged combined. That is the same baseline as the status letters
143
+ * and `oats instance diff`. An unborn tree counts against the empty tree (`hash-object` without -w writes nothing).
144
+ * → Map path → { additions, deletions, binary }, keyed by the NEW path for a rename; null when git failed.
145
+ * -z records: "A\tD\tpath\0", or "A\tD\t\0old\0new\0" for a rename; "-\t-" is a binary file. */
146
+ function lineCounts(work, headOid) {
147
+ const against = headOid ?? trim(git(work, ["hash-object", "-t", "tree", "/dev/null"], { allowFail: true }));
148
+ if (!against) return null;
149
+ const raw = git(work, ["diff", against, "--numstat", "-z", "-M", "--"], { allowFail: true });
150
+ if (raw === null) return null;
151
+ const counts = new Map(), recs = raw.split("\0");
152
+ for (let i = 0; i < recs.length;) {
153
+ const m = /^(-|\d+)\t(-|\d+)\t([\s\S]*)$/.exec(recs[i++]);
154
+ if (!m) continue;
155
+ let path = m[3];
156
+ if (path === "") { i++; path = recs[i++]; } // a rename: skip the old path, key the new one
157
+ if (typeof path !== "string" || !path) continue;
158
+ counts.set(path, m[1] === "-" ? { additions: null, deletions: null, binary: true } : { additions: Number(m[1]), deletions: Number(m[2]), binary: false });
159
+ }
160
+ return counts;
161
+ }
162
+ /** An entry's counts: untracked (no baseline; contents are not read) and submodules have none (all null). */
163
+ function countsOf(counts, entry) {
164
+ if (entry.kind === "untracked" || entry.submodule) return { additions: null, deletions: null, binary: null };
165
+ return counts?.get(entry.path) ?? { additions: null, deletions: null, binary: null };
166
+ }
167
+
141
168
  function fileId(revision, indexOid, entry) {
142
169
  return createHash("sha256").update(`${revision}\0${indexOid}\0${entry.kind}\0${entry.path}\0${entry.origPath ?? ""}`).digest("hex").slice(0, 24);
143
170
  }
@@ -164,7 +191,8 @@ export function observeInstanceGit(home) {
164
191
  const counts = mergeBase ? countRange(work, `${base.ref}...HEAD`) : null;
165
192
  baseComparison = { ref: base.ref, source: base.source, mergeBase, ahead: counts ? counts.right : null, behind: counts ? counts.left : null };
166
193
  }
167
- const files = entries.filter((e) => e.kind !== "ignored").map((e) => ({ id: fileId(revision, indexOid, e), ...e }));
194
+ const counts = lineCounts(work, headOid);
195
+ const files = entries.filter((e) => e.kind !== "ignored").map((e) => ({ id: fileId(revision, indexOid, e), ...e, ...countsOf(counts, e) }));
168
196
  const summary = { changed: 0, renamed: 0, copied: 0, unmerged: 0, untracked: 0 };
169
197
  for (const f of files) summary[f.kind]++;
170
198
  return {
@@ -178,6 +206,7 @@ export function observeInstanceGit(home) {
178
206
  notes: [
179
207
  ...(upstream.ref === null ? ["no upstream configured: upstream ahead/behind are unknown, not zero"] : []),
180
208
  ...(baseComparison.ref === null ? ["no default branch found (origin/HEAD, origin/main, origin/master, main, master): base comparison unknown"] : []),
209
+ ...(counts === null ? ["line counts unavailable (git diff --numstat failed): additions/deletions are unknown, not zero"] : []),
181
210
  ],
182
211
  };
183
212
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@awebai/oats",
3
- "version": "0.29.0",
3
+ "version": "0.29.1",
4
4
  "description": "OATS (Open Agent Team Specification) — durable souls, disposable instances, targetable capability packages, and the runtime-neutral oats CLI/kernel.",
5
5
  "keywords": [
6
6
  "agents",