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 +1 -1
- package/skills/using-dreamteamer/SKILL.md +5 -1
- package/src/cli.js +7 -2
- package/src/commit.js +58 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dreamteamer",
|
|
3
|
-
"version": "0.
|
|
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.
|
|
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.
|
|
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
|
|
212
|
+
// bare args are TARGETS — a 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
|
-
|
|
102
|
-
|
|
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; }
|