dreamteamer 0.12.1 → 0.13.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dreamteamer",
3
- "version": "0.12.1",
3
+ "version": "0.13.0",
4
4
  "description": "A workspace compiler for coding agents — schema-validated records as plain files over git, compiled into every harness",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Gilad Khen <giladkhen@gmail.com>",
@@ -75,7 +75,11 @@ and deliberately nothing else — including nothing about people. There is no `u
75
75
  from git's own status letters), and a multi-record commit says what it swept.
76
76
  - **one commit per REPO.** a module can own its records (`owns-data` in its package.json), and git
77
77
  has no cross-repo commit — so a rename whose inbound refs live in another repo is TWO commits.
78
- `dt commit` prints both. `dt commit <collection> …` scopes it; `--dry-run` shows the set first.
78
+ `dt commit` prints both. `--dry-run` shows the set first.
79
+ - **scope the commit to what YOU wrote: `dt commit <collection>/<id>`.** any number of targets, each
80
+ either a whole `<collection>` or one record — bare `dt commit` publishes everything pending, and
81
+ `dt commit <collection>` publishes every dirty record under it *whoever wrote it*, which is the
82
+ same sweep as the blanket add below when a second session shares the tree.
79
83
  - **never `git add -A`, `git add .`, or `git commit -a`.** stage explicit paths. more than one agent
80
84
  can be working in a tree, and a blanket add silently commits whatever another session has
81
85
  uncommitted right now — under your subject, leaving `git status` clean and the damage invisible.
package/src/cli.js CHANGED
@@ -100,7 +100,10 @@ workspace verbs:
100
100
  [--since <sha|YYYY-MM-DD>] (default: the last commit) [--json]
101
101
  commit publish records already written to disk: samples git status over every
102
102
  collection's record dirs, one commit PER REPO, subject composed from the
103
- status letters. [<collection> …] to scope, [-m <subject>], [--dry-run]
103
+ status letters. Scope it with any number of targets, each either a whole
104
+ <collection> or one <collection>/<id> — the record form is what keeps a
105
+ concurrent session's pending records out of your commit.
106
+ [<collection>|<collection>/<id> …] [-m <subject>] [--dry-run] [--json]
104
107
  help this text
105
108
  `;
106
109
 
@@ -206,7 +209,9 @@ export function run(argv) {
206
209
  const store = new Store(ws);
207
210
  const mi = rest.indexOf('-m');
208
211
  const message = mi > -1 ? rest[mi + 1] : undefined;
209
- // bare args are collection names minus the token `-m` consumed as its subject
212
+ // bare args are TARGETSa whole collection or a `<collection>/<id>` reference,
213
+ // told apart in commit.js against the declared collections — minus the token `-m`
214
+ // consumed as its subject
210
215
  const only = rest.filter((a, i) => !a.startsWith('-') && (mi === -1 || i !== mi + 1));
211
216
  const results = commitPending(store, { only, message, dryRun: rest.includes('--dry-run') });
212
217
  if (rest.includes('--json')) { emit(JSON.stringify(results, null, 2)); process.exit(0); }
package/src/commit.js CHANGED
@@ -5,6 +5,7 @@ import { execFileSync } from 'node:child_process';
5
5
  import fs from 'node:fs';
6
6
  import path from 'node:path';
7
7
  import { pathToRecord } from './events.js';
8
+ import { splitRef } from './ref.js';
8
9
 
9
10
  // git calls whose failure we CATCH must not print git's own error: execFileSync forwards the
10
11
  // child's stderr to ours unless told otherwise, so a handled "not a git repository" still
@@ -14,6 +15,28 @@ const QUIET = ['ignore', 'pipe', 'ignore'];
14
15
 
15
16
  const VERB = { A: 'add', M: 'set', D: 'rm', R: 'rename', '?': 'add' };
16
17
 
18
+ /** What the caller asked to publish. A target is EITHER a collection name or a `<collection>/<id>`
19
+ * reference — the same either-shape `move` and `commands` accept, and after 0.12.0 the shape every
20
+ * other verb's target has. Which one it is cannot be guessed from the string (an id may contain
21
+ * slashes and so may a namespaced collection name), so it is decided against the DECLARED
22
+ * collections: a key of `descriptors` is a collection, anything else goes to splitRef — which
23
+ * throws, naming the known collections, rather than letting a typo mean "no scope".
24
+ *
25
+ * Returns `scope` (collections to sample, so the git pathspec stays as narrow as it was), `whole`
26
+ * (collections asked for entire) and `records` (ref → {collection, id}). */
27
+ function parseTargets(descriptors, only) {
28
+ const scope = new Set();
29
+ const whole = new Set();
30
+ const records = new Map();
31
+ for (const target of only) {
32
+ if (descriptors.has(target)) { whole.add(target); scope.add(target); continue; }
33
+ const { collection, id } = splitRef(descriptors, target);
34
+ records.set(`${collection}/${id}`, { collection, id });
35
+ scope.add(collection);
36
+ }
37
+ return { scoped: only.length > 0, scope: [...scope], whole, records };
38
+ }
39
+
17
40
  /** Record directories to watch, grouped by owning repo. System-stored collections are excluded:
18
41
  * they live in the gitignored runtime and their sources are module files — the same exclusion
19
42
  * pathToRecord already applies. */
@@ -97,11 +120,44 @@ export function composeSubject(rows) {
97
120
  return `dreamteamer: ${rows.length} changes across ${collections.join(', ')}`;
98
121
  }
99
122
 
123
+ /** A requested reference that matched no pending row is one of two very different things: a record
124
+ * that is already published (nothing to do, and fine) or a MISTYPED id — which must not pass as
125
+ * "nothing pending", the one report that looks like success. Only the store can tell them apart,
126
+ * and it is only asked in this branch: a pending DELETION matched a row above, so a record whose
127
+ * file is legitimately gone never reaches here. */
128
+ function assertResolvable(store, records, matched) {
129
+ for (const [ref, { collection, id }] of records) {
130
+ if (matched.has(ref) || store.ids(collection).has(id)) continue;
131
+ throw new Error(`${ref}: no such record — nothing pending under that reference`);
132
+ }
133
+ }
134
+
100
135
  export function commitPending(store, { only = [], message, dryRun = false } = {}) {
101
- const byRepo = scopeByRepo(store.descriptors, only);
102
- const results = [];
136
+ // Targets are resolved BEFORE anything is committed, so one bad target in a list of good ones
137
+ // leaves the whole tree untouched rather than committing a prefix of what was asked for.
138
+ const targets = parseTargets(store.descriptors, only);
139
+ const byRepo = scopeByRepo(store.descriptors, targets.scope);
140
+ // ⚠ Sample every repo FIRST, then check the references, then commit. The unknown-reference test
141
+ // below can only be answered once every repo has been sampled — a record lives in exactly one
142
+ // repo, and which one is not known in advance.
143
+ const sampled = [];
144
+ const matched = new Set();
103
145
  for (const [repo, dirs] of byRepo) {
104
146
  const { cwd, rows } = sample(store.root, repo, dirs, store.descriptors);
147
+ // The SAMPLER is deliberately left alone — it is what makes a hand-edited record
148
+ // indistinguishable from one the store wrote. Narrowing happens here, on the sampled rows,
149
+ // so `dt commit <collection>/<id>` publishes that record and leaves a sibling written by
150
+ // another session exactly as pending as it found it.
151
+ const wanted = !targets.scoped ? rows : rows.filter((r) => {
152
+ const ref = `${r.collection}/${r.id}`;
153
+ if (targets.records.has(ref)) { matched.add(ref); return true; }
154
+ return targets.whole.has(r.collection);
155
+ });
156
+ sampled.push({ repo, cwd, rows: wanted });
157
+ }
158
+ assertResolvable(store, targets.records, matched);
159
+ const results = [];
160
+ for (const { repo, cwd, rows } of sampled) {
105
161
  if (!rows.length) continue;
106
162
  const blocked = inProgress(cwd);
107
163
  if (blocked) { results.push({ repo, rows, blocked }); continue; }