dreamteamer 0.19.1 → 0.21.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.
@@ -1,4 +1,7 @@
1
1
  name: docs
2
+ description: >-
3
+ title / content / tags, under a dated slug id (`2026-07-23--<title>`) — the shape of a thing that
4
+ HAPPENED or was written on a day: a note, a report, a meeting write-up. For a thing that IS, use `entity`.
2
5
  template:
3
6
  storage: { codec: md, shape: file }
4
7
  id:
@@ -2,6 +2,10 @@
2
2
  # no date prefix (docs-style dated ids read wrong for things that ARE, not happen).
3
3
  # storage is left to the template mechanism's standard defaults (data/<name>, md, file).
4
4
  name: entity
5
+ description: >-
6
+ name / notes / tags, under a bare slug id (`<name>`) — the shape of a thing that IS: a person, a
7
+ supplier, a tool. No date in the id, because an entity does not happen on a day. For a dated
8
+ document, use `docs`.
5
9
  template:
6
10
  id:
7
11
  generate: "{{ name | slug }}"
@@ -10,18 +10,23 @@ schema:
10
10
  description: The collection id — must equal the filename, and must be unique across every installed module.
11
11
  description:
12
12
  type: string
13
- description: What kind of thing this collection holds, in one line.
13
+ description: >-
14
+ What one record IS, in one line — and the neighbour it is NOT, when a confusable one exists
15
+ ("the person, never the org — that is `companies`"). Rendered into the orientation block
16
+ every agent session loads, so it is retrieval surface, not documentation.
14
17
  use_when:
15
18
  type: string
16
19
  description: >-
17
- WHEN to reach for this collection, in one clause — rendered into the orientation block every
18
- agent session loads. OPTIONAL and usually absent: for most collections the trigger IS the
19
- description (`contacts` = a person; you use it when you have a person), and a restatement
20
- there costs every session tokens while diluting the few that carry real information. The
21
- test: author this only when an agent that FULLY UNDERSTANDS the description would still not
22
- know to reach for the collection. It spans both acts read ("you are about to diagnose a
23
- defect search here first") and write ("a day needs planning"). NOT a place for procedure:
24
- a `how` belongs in the module's skill, this field holds a `when`.
20
+ The SITUATIONS that should bring a session here, in one clause — rendered under the
21
+ description in the orientation block. A description says what a record is; this says when
22
+ to reach for the collection, in both directions: to READ ("about to diagnose a defect search
23
+ here first, filtered by repo") and to WRITE ("a thought arrives that has no home yet"). Author
24
+ it for every collection whose trigger is not literally "you have one of these": a schema is
25
+ designed to be used, and the measured failure is a session inventing a new state or field while
26
+ the collection that already modelled it sat in its context. Two rules: it names a situation,
27
+ never a procedure (a `how` belongs in the module's skill); and it must not restate the
28
+ description — a paraphrase costs every session tokens and dilutes the clauses that carry
29
+ signal. Omit only where the noun is the whole trigger.
25
30
  title:
26
31
  type: string
27
32
  description: What to call this collection in the nav and page headers. DERIVED from `name` by title-casing when absent — author it only when that is wrong (`ui-views` → `UI Views`).
@@ -66,6 +66,15 @@ schema:
66
66
  path:
67
67
  type: string
68
68
  description: Workspace-relative root of the module's sources.
69
+ bin:
70
+ type: array
71
+ description: >-
72
+ The module's runnable entry points — every file under its `bin/`, module-relative, projected by
73
+ compile. Rendered into the orientation block beside the module's skills and commands as the
74
+ POINTER that a procedure is a script here (how to run it stays in the module's skill); a module
75
+ with no `bin/` has no field.
76
+ items:
77
+ type: string
69
78
  owns_data:
70
79
  type: boolean
71
80
  description: >-
@@ -2,6 +2,11 @@ name: repos
2
2
  description: >-
3
3
  A git repository this workspace knows about — where it lives and how a working tree is
4
4
  materialized on demand.
5
+ use_when: >-
6
+ a repository this workspace depends on is named — its own engine or extension clone, a client's
7
+ repo — read the record here for WHERE it lives (`path`, a `${env:…}` template `dt resolve` renders
8
+ per machine) before assuming a path or concluding a clone is unreachable; `dt status` reports
9
+ each one and `dt ensure` materializes it
5
10
  # An external git repo attached to this workspace. Owns CLONE LIFECYCLE ONLY — a repo record
6
11
  # never contributes schema, skills or UI (that is what a module is, declared in package.json
7
12
  # `dreamteamer.git-modules`, because modules must be restorable BEFORE compile can run).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dreamteamer",
3
- "version": "0.19.1",
3
+ "version": "0.21.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>",
@@ -40,7 +40,7 @@ install. The looking still matters; the taking changed shape.
40
40
 
41
41
  | # | look at | how, concretely | what you get |
42
42
  |---|---|---|---|
43
- | 1 | **this workspace's own modules** | the orientation block's COLLECTIONS list is already in your context — reread it with the concept's *synonyms* in mind; `.dreamteamer/manifest.yaml` names every module and entry; `dt help` shows the verbs and `dt commands <collection>` the bound ones; the skills index shows the techniques | the thing may already exist under a name you didn't guess. This is where misses actually happen, because it is the place you assume you already know |
43
+ | 1 | **this workspace's own modules** | the orientation block's MODULES AND THEIR COLLECTIONS list is already in your context — find the module whose sentence owns the concept, then reread its collections with the concept's *synonyms* in mind, `use when` clauses included; then read the candidate's DESCRIPTOR, not its records; `dt help` shows the verbs and `dt commands <collection>` the bound ones; the module's skills line names the techniques | the thing may already exist under a name you didn't guess. This is where misses actually happen, because it is the place you assume you already know |
44
44
  | 2 | **a recipes repo — where this workspace's own CLAUDE.md names one** (reference modules maintained to be **copied and adapted**) | read that repo's own instructions first; they ARE the adoption procedure. no recipes repo declared → skip this row | a working module with its reasoning attached — descriptors, skills, and the trap notes that each cost someone a day |
45
45
  | 3 | **a sibling workspace — where the operator has named one** | read-only; grep its `modules/` for the concept. none named → skip | another vault may have solved it concretely. That is a *reference*, not a source — it holds real personal data, so read the shape, never lift the content |
46
46
  | 4 | **the engine's own surface** | `dt help` — the complete verb surface, system and field verbs included; read it rather than recalling it | the capability may already be a verb (`relations rebuild`, `resolve`, `ensure`) rather than a missing module |
@@ -227,6 +227,45 @@ Two gates around it:
227
227
  - ⚠ **An overlay can add fields but cannot remove an inherited one.** If the shape is wrong for
228
228
  the module rather than just for this workspace, fix the base.
229
229
 
230
+ ## `x-choices` — what an enum VALUE looks like
231
+
232
+ An enum value carries a label and nothing else by default: a surface gets `{ text, value }` and
233
+ draws the value. `x-choices` is an OPTIONAL sparse map, keyed by the value, that gives a surface
234
+ more to draw with — a board grouping by the field, a dropdown in a form, anything reading
235
+ `edit_options.choices`.
236
+
237
+ ```yaml
238
+ lane:
239
+ type: string
240
+ enum: [alpha, bravo, charlie]
241
+ x-choices:
242
+ alpha:
243
+ label: Alpha team # what a surface shows; the stored VALUE is still `alpha`
244
+ description: the one that ships
245
+ icon: rocket # a codicon name …
246
+ color: charts.blue # a theme colour id — the accent
247
+ background: charts.blue # … and the fill
248
+ bravo:
249
+ icon: assets/icons/lucide/anchor # … OR a reference to a `codec: file` record
250
+ ```
251
+
252
+ - **Sparse and additive.** Decorate one value, or none. A value with no entry projects exactly as it
253
+ did before this keyword existed, so adding it changes nothing that already works.
254
+ - **`enum` still owns the value set AND its order.** A map key cannot add, remove or reorder a
255
+ value — which matters, because a grouped view takes its band order from the enum.
256
+ - **Five keys, and only five** — `label` · `description` · `icon` · `color` · `background`, each an
257
+ optional string. Anything else in an entry is dropped: the projection copies by name, so a
258
+ descriptor cannot inject keys into a contract every surface reads.
259
+ - **`label` becomes `text`.** So a workspace can relabel a value without touching the value, and no
260
+ stored record moves.
261
+ - **`icon` is a codicon name or a reference to a record of a `codec: file` collection.** A codicon
262
+ name never contains a slash and a record reference always does, so the surface decides which
263
+ without a second keyword.
264
+ - **Colours are theme colour ids, not hex.** A hex is authored against one theme and wrong in the
265
+ other.
266
+ - **Both mistakes warn rather than fail** — a key that is not one of the enum's values, and the
267
+ keyword on a field with no enum. See the message catalog below.
268
+
230
269
  ## the reference contract — `x-reference` across the module graph
231
270
 
232
271
  Every `x-reference` target must be one of: a **core** collection (the entity kinds plus `repos`)
@@ -291,6 +330,8 @@ collection author actually meets. (⚠ = warning: it compiled, and you should st
291
330
  | `cyclic module dependencies: a → b → a` | concept-level links declared as module deps | the collection belongs in `peerDependencies` |
292
331
  | relation refusals (`stamps a mirror onto…`, `declared on both sides…`) | the relation rules | `data-modeling.md` Part VI |
293
332
  | ⚠ `x-unique on "f" is inert` | a relation keyword with no relation — nothing enforces it | declare the inverse, or drop it |
333
+ | ⚠ `x-choices on "f" has an entry for "k"` | it decorates enum VALUES and `k` is not one — a typo, or a value since removed | fix the spelling, or drop the entry |
334
+ | ⚠ `x-choices on "f" is inert` | the keyword on a field that declares no enum — nothing reads it | give the field an enum, or drop the keyword |
294
335
  | ⚠ `collection … has no description` | it renders as a bare name in the orientation block every session loads | write the sentence (`data-modeling.md` §18) |
295
336
  | ⚠ `module "…" contributed no recognised sources` | its folders match no kind and it ships no UI bundle | usually a layout or naming mistake |
296
337
  | ⚠ `module X: <channel> copy shadows <channel> copy` | the same module delivered twice — the more local wins (npm-link semantics) | intended for dev; otherwise remove one |
@@ -420,23 +420,43 @@ step downstream.
420
420
  ### 18. Descriptions and `use_when` — the model is also the prompt
421
421
 
422
422
  In an agent-operated workspace, descriptor prose is not documentation; it is **retrieval surface**.
423
- Collection descriptions are compiled into the orientation block every session loads, and field
424
- descriptions are what an agent reads before writing a value. So:
425
-
426
- - A collection's `description` carries the question it answers **and the neighbour it is NOT**:
423
+ The orientation block every session loads is rendered from it, top-down: each **module's** sentence
424
+ heads its group (with the namespaces, skills and commands it ships), each **collection's**
425
+ `description` and `use_when` follow, and field descriptions are what an agent reads before writing a
426
+ value. Three layers, three questions *what is this area for* · *what is this thing, and when do I
427
+ reach for it* · *what does this value mean*. So:
428
+
429
+ - A **module's** `description` (its package.json — `dreamteamer.description`, or npm's own
430
+ top-level `description`) says what the AREA is for and what it deliberately is not: the domain,
431
+ its boundary with a neighbouring module, anything a session must know before working in it
432
+ (private data, a machine-bound account). It is the top of the tree a session reads first.
433
+ - A **collection's** `description` says what one record IS, and names the neighbour it is NOT:
427
434
  "the person, never the org — that is `companies`". Confusable pairs each point at the other.
428
- - A field's `description` says what the value MEANS, names the source when the value is copied
435
+ - A collection's **`use_when`** names the SITUATIONS that should bring a session here, in both
436
+ directions — read ("about to diagnose a defect — search here first, filtered by repo") and write
437
+ ("a thought arrives that has no home yet"). **Author it for every collection whose trigger is not
438
+ literally "you have one of these."** A schema is designed to be used, and the measured failure
439
+ mode is a session inventing a new state, field or terminal condition while the collection that
440
+ already modelled it sat in its context under a description it had read. Two rules: it names a
441
+ situation, never a procedure (a `how` belongs in the module's skill); and it must not paraphrase
442
+ the description — that costs every session tokens and dilutes the clauses that carry signal.
443
+ ⚠ It is prose; nothing fires on it. It raises the odds a session looks; it does not make it.
444
+ - A **field's** `description` says what the value MEANS, names the source when the value is copied
429
445
  from elsewhere, and states the convention an agent must follow ("empty means unmatched — the
430
446
  matching command's queue").
431
447
  - A field whose valid value has a non-obvious SHAPE carries an `examples:` annotation — standard
432
- JSON Schema, passed through to the compiled descriptor so the canonical value lives in the
433
- contract, not in whichever record a writer happens to open.
434
- - `use_when` is authored **only** when an agent that fully understood the description would still
435
- not reach for the collection a search-here-first trigger, a write-here-when situation. It is
436
- prose; nothing fires on it; and a `use_when` restating the description costs every session
437
- tokens while diluting the few that carry real signal.
448
+ JSON Schema, passed through to the compiled descriptor unchanged, and its first value rendered
449
+ into the collection's `write:` line in the orientation block beside the required fields and closed
450
+ enums so the canonical value lives in the contract, not in whichever record a writer happens to
451
+ open, and a session sees it before its first write. Two or three, real-looking
452
+ and synthetic: a `${env:…}` path template, a `key:value` tag, an RRULE, a composite id.
438
453
  - Descriptions are the cheapest UX in the system: the same line is the tooltip in every surface,
439
454
  the agent's guidance, and the future maintainer's note. Budget a real sentence per field.
455
+ - Compile WARNS on a module, collection or collection-template with no description — each renders
456
+ as a bare name in the block otherwise — and on a `use_when` that restates its description (content
457
+ words shared ≥ 0.5): name the situation, do not delete the clause. It does not warn on a missing
458
+ `use_when`, because it cannot tell a considered omission from a forgotten one; that judgement is
459
+ yours, per collection.
440
460
 
441
461
  ---
442
462
 
package/src/compile.js CHANGED
@@ -421,6 +421,28 @@ function bothLayouts(root, kind) {
421
421
  * (`dreamteamer.ignore`). That is real per-module variance — `services` has `dashboard/`, `agentlog`
422
422
  * has `data/` — not a layout knob every module would set identically.
423
423
  */
424
+ /** `bin/*` files of a module root, module-relative and sorted — dotfiles and subfolders (lib/,
425
+ * parsers/) excluded. Empty when there is no bin/. */
426
+ function binEntries(moduleRoot) {
427
+ const dir = path.join(moduleRoot, 'bin');
428
+ if (!fs.existsSync(dir)) return [];
429
+ return fs.readdirSync(dir, { withFileTypes: true })
430
+ .filter((e) => e.isFile() && !e.name.startsWith('.'))
431
+ .map((e) => `bin/${e.name}`).sort();
432
+ }
433
+
434
+ /** Content-word Jaccard between two clauses, thresholded. Short words and the join words of
435
+ * English carry no signal here; ≥ 0.5 of the remaining vocabulary shared is a paraphrase. */
436
+ function paraphrases(description, useWhen) {
437
+ const STOP = new Set(['this', 'that', 'with', 'from', 'into', 'here', 'when', 'what', 'never', 'every', 'their', 'there', 'them', 'they', 'have', 'been', 'each', 'than', 'then', 'only', 'also', 'before', 'after', 'about']);
438
+ const words = (t) => new Set(String(t ?? '').toLowerCase().replace(/[`'"“”‘’()[\],.;:—–-]/g, ' ').split(/\s+/).filter((w) => w.length > 3 && !STOP.has(w)));
439
+ const a = words(description), b = words(useWhen);
440
+ if (!a.size || !b.size) return false;
441
+ let shared = 0;
442
+ for (const w of b) if (a.has(w)) shared++;
443
+ return shared / (a.size + b.size - shared) >= 0.5;
444
+ }
445
+
424
446
  const NON_SOURCE_DIRS = new Set([
425
447
  'node_modules', 'data', 'state', 'media', 'bin', 'src', 'lib', 'scripts',
426
448
  'ui', 'studio', // the module's UI bundle — 'studio' is the pre-archive name, kept as a fallback
@@ -900,6 +922,9 @@ export function compile({ root, pkg }) {
900
922
  if (!m) continue;
901
923
  const doc = loadSource(entry.bytes.toString('utf8'), entry.sources[0].path);
902
924
  templateDocs.set(m[1], { template: doc?.template ?? {}, src: entry.sources[0] });
925
+ // The orientation block lists every template by its own sentence (harnesses.templatesSection);
926
+ // the engine's two shipped ones rendered as bare names for a month before anyone noticed.
927
+ if (!String(doc?.description ?? '').trim()) console.warn(`⚠ collection-template ${m[1]} has no description — it renders as a bare name in the orientation block every session loads`);
903
928
  }
904
929
 
905
930
  // ---- namespaces: the UNION of every module's declaration plus the workspace's (§8) ----------
@@ -1290,6 +1315,25 @@ export function compile({ root, pkg }) {
1290
1315
  if (h['x-unique'] === true && h['x-inverse'] === undefined && h['x-inverse-of'] === undefined) {
1291
1316
  console.warn(`⚠ collection ${name}: x-unique on "${fieldName}" is inert — it is a RELATION keyword, enforced only while the store maintains a mirror, and this field declares no x-inverse. Nothing constrains the value. Declare the relation (dreamteamer update-field ${name} --name ${fieldName} --inverse) or drop x-unique.`);
1292
1317
  }
1318
+ // `x-choices` decorates ENUM VALUES (presentation.js#choiceRow, 0.21.0), and both ways of
1319
+ // getting it wrong are SILENT: a key that is not a value decorates nothing, and the keyword
1320
+ // on a non-enum field is read by no one at all. Either way the author sees no error and no
1321
+ // decoration — on a surface they are probably not looking at while editing the descriptor.
1322
+ // ⚠ WARNINGS, not failures, for the same reason as x-unique directly above: neither breaks
1323
+ // anything today, and a descriptor mid-edit must stay compilable.
1324
+ const choices = h['x-choices'];
1325
+ if (choices && typeof choices === 'object' && !Array.isArray(choices)) {
1326
+ const values = Array.isArray(h.enum) ? h.enum.map(String) : null;
1327
+ if (!values) {
1328
+ console.warn(`⚠ collection ${name}: x-choices on "${fieldName}" is inert — it decorates the values of an enum, and this field declares no enum. Nothing reads it.`);
1329
+ } else {
1330
+ // Named per offending key rather than "some keys are wrong", and the legal values are
1331
+ // quoted so the fix needs no second lookup — the same shape as every other warning here.
1332
+ for (const k of Object.keys(choices)) {
1333
+ if (!values.includes(k)) console.warn(`⚠ collection ${name}: x-choices on "${fieldName}" has an entry for "${k}", which is not one of its enum values (${values.join(', ')}) — it decorates nothing. Fix the spelling, or drop the entry.`);
1334
+ }
1335
+ }
1336
+ }
1293
1337
  }
1294
1338
  const rt = path.join('collections', `${name}.collection.yaml`);
1295
1339
  entries.set(rt, { sources: descriptorSources, bytes: Buffer.from(dump(merged)) });
@@ -1298,12 +1342,20 @@ export function compile({ root, pkg }) {
1298
1342
  // rather than a heroic backfill pass, and the same shape as the per-missing-env-key warning:
1299
1343
  // non-blocking, named per offender, so the gap converges instead of being rediscovered.
1300
1344
  //
1301
- // Deliberately NO equivalent warning for `use_when`. That field is optional and correct to
1302
- // omit on most collections warning on it would invert its authoring test and manufacture a
1303
- // restatement of the description on every collection that does not need one.
1345
+ // No equivalent warning for `use_when`, though the field is WANTED on most collections
1346
+ // (data-modeling §18: a description says what a record IS, `use_when` says which situations
1347
+ // should lead a session here). A warning cannot tell a considered omission from a forgotten
1348
+ // one, and a manufactured restatement of the description is worse than an absent clause.
1304
1349
  if (merged.storage.base !== 'runtime' && !String(merged.description ?? '').trim()) {
1305
1350
  console.warn(`⚠ collection ${name} has no description — it renders as a bare name in the orientation block every session loads`);
1306
1351
  }
1352
+ // A `use_when` that restates the description costs every session tokens and dilutes the clauses
1353
+ // that carry signal (data-modeling §18); a reader catches it, nothing else did. Word overlap is
1354
+ // the mechanical proxy: the clauses that earned their place in a blind evaluation encoded an
1355
+ // ORDER or a REFUSAL, and shared few content words with their description.
1356
+ if (merged.use_when && paraphrases(merged.description, merged.use_when)) {
1357
+ console.warn(`⚠ collection ${name}: use_when restates its description — name the SITUATION that brings a session here (search here first · capture here when), not the noun again`);
1358
+ }
1307
1359
  counts.collections++;
1308
1360
  }
1309
1361
 
@@ -1332,18 +1384,26 @@ export function compile({ root, pkg }) {
1332
1384
  }
1333
1385
  for (const source of sources) {
1334
1386
  const id = moduleId(source.name);
1335
- let mpkg = {};
1336
- try { mpkg = JSON.parse(fs.readFileSync(path.join(source.root, 'package.json'), 'utf8')).dreamteamer ?? {}; } catch { /* inline workspace source */ }
1387
+ let pkg = {};
1388
+ try { pkg = JSON.parse(fs.readFileSync(path.join(source.root, 'package.json'), 'utf8')); } catch { /* inline workspace source */ }
1389
+ const mpkg = pkg.dreamteamer ?? {};
1390
+ // What this module is FOR, in one line. `dreamteamer.description` wins; npm's own top-level
1391
+ // `description` is the fallback, because it is the one place a package author already writes
1392
+ // that sentence — six modules on one dogfood workspace had authored it THERE and every
1393
+ // `dt list modules` row read `-` for description. There is no derivation past that and there
1394
+ // should not be: a module is the only place that knows.
1395
+ const description = [mpkg.description, pkg.description].find((s) => typeof s === 'string' && s.trim());
1396
+ // A module with no sentence renders as a bare title at the head of its section in the
1397
+ // orientation block — the one place a session learns what an AREA of the workspace is for.
1398
+ // Same shape as the per-collection warning: non-blocking, named per offender.
1399
+ if (!description) console.warn(`⚠ module ${source.name} has no description — set "description" in its package.json; it heads its own section in the orientation block every session loads`);
1337
1400
  const record = {
1338
1401
  name: source.name,
1339
1402
  // Authored wins; the derived fallback title-cases the id the same way a collection's
1340
1403
  // `title` is derived. `@dreamteamer/crm` -> "Crm" until crm declares "CRM" — which is
1341
1404
  // the point: the module is the only place that knows.
1342
1405
  title: typeof mpkg.title === 'string' && mpkg.title ? mpkg.title : titleCase(id),
1343
- // What this module is FOR, in one line — authored as `dreamteamer.description` in its
1344
- // package.json. There is no derivation for it and there should not be: a module is the only
1345
- // place that knows, and an absent one renders as a bare name in every listing.
1346
- ...(typeof mpkg.description === 'string' && mpkg.description ? { description: mpkg.description } : {}),
1406
+ ...(description ? { description } : {}),
1347
1407
  // §8: the namespaces THIS module declares — projected from its package.json, so
1348
1408
  // `dt list modules` answers "who owns hr?" without reading seven package.json files.
1349
1409
  ...(normalizeNamespaces(mpkg.namespaces).length ? { namespaces: normalizeNamespaces(mpkg.namespaces) } : {}),
@@ -1351,6 +1411,11 @@ export function compile({ root, pkg }) {
1351
1411
  // `hr git_modules @ 3f2a1c (dirty)` and needs no legend.
1352
1412
  location: locationOf(source, root),
1353
1413
  path: rel(source.root) || '.',
1414
+ // The module's RUNNABLE entry points — `bin/<file>`, the folder NON_SOURCE_DIRS waves through.
1415
+ // Skills and commands render into the orientation block; a module whose procedure is a
1416
+ // script did not, so a session planned `dt add` writes the tooling forbids. This is the
1417
+ // POINTER that the procedure exists, never its arguments — those stay in the skill.
1418
+ ...(binEntries(source.root).length ? { bin: binEntries(source.root) } : {}),
1354
1419
  ...(mpkg['owns-data'] === true ? { owns_data: true } : {}),
1355
1420
  // Declared module names become record IDS here, because that is what an x-reference
1356
1421
  // resolves against. An undeclared/unknown name would dangle, and `check` would say so —
@@ -1500,7 +1565,7 @@ export function compile({ root, pkg }) {
1500
1565
  const anyFlat = sources.some((s) => KINDS.some((k) => fs.existsSync(path.join(s.root, k))));
1501
1566
  const anyNested = sources.some((s) => KINDS.some((k) => fs.existsSync(path.join(s.root, 'system', k))));
1502
1567
  const sourceLayout = anyFlat && anyNested ? 'mixed' : anyNested ? 'nested' : 'flat';
1503
- const { outputs: adapterOutputs, summary: harnessSummary } = runHarnessAdapters({ root, entries, harnesses, prevManifest, sourceLayout, namespaces, version: engineVer });
1568
+ const { outputs: adapterOutputs, blocks: adapterBlocks, summary: harnessSummary } = runHarnessAdapters({ root, entries, harnesses, prevManifest, sourceLayout, namespaces, version: engineVer, workspaceModule: config['workspace-module'] ?? '' });
1504
1569
 
1505
1570
  // ---- provenance manifest ------------------------------------------------------
1506
1571
  const manifest = {
@@ -1535,6 +1600,9 @@ export function compile({ root, pkg }) {
1535
1600
  })),
1536
1601
  ui: uiModules.sort(),
1537
1602
  'adapter-outputs': adapterOutputs.sort(),
1603
+ // the root files whose managed BLOCK this compile rewrote — never pruned, but committed with a
1604
+ // schema write so the block and the schema it names land together (schema-ops.regeneratedOutputs)
1605
+ 'adapter-blocks': adapterBlocks.sort(),
1538
1606
  entries: Object.fromEntries(
1539
1607
  [...entries].map(([rt, e]) => [rt, { sources: e.sources, hash: sha256(e.bytes) }]) // sources: [{path, hash}] — per-SOURCE hashes power staleness
1540
1608
  ),
package/src/harnesses.js CHANGED
@@ -21,10 +21,17 @@ export const STAMP = '<!-- generated by dreamteamer compile — do not edit; sou
21
21
  const BEGIN = '<!-- dreamteamer:begin (generated — do not edit inside this block) -->';
22
22
  const END = '<!-- dreamteamer:end -->';
23
23
 
24
- export function runHarnessAdapters({ root, entries, harnesses, prevManifest, sourceLayout = 'flat', namespaces = [], version = 'unknown' }) {
24
+ export function runHarnessAdapters({ root, entries, harnesses, prevManifest, sourceLayout = 'flat', namespaces = [], version = 'unknown', workspaceModule = '' }) {
25
25
  const outputs = [];
26
+ // ⚠ SEPARATE from `outputs`: these are USER-OWNED root files carrying a managed block, and the
27
+ // prune loop below DELETES anything in a previous manifest's `adapter-outputs` that this compile
28
+ // did not produce — listing CLAUDE.md there would delete the operator's own instructions the day
29
+ // a harness is switched off. They are reported so a schema write can COMMIT the regenerated block
30
+ // beside the source it changed (schema-ops.regeneratedOutputs), which is the only other reader.
31
+ const blocks = [];
26
32
  const summary = [];
27
33
  const rel = (p) => path.relative(root, p);
34
+ const block = (file, content) => { writeBlock(root, file, content); if (content != null) blocks.push(file); };
28
35
  const write = (out, bytes) => {
29
36
  const dest = path.join(root, out);
30
37
  fs.mkdirSync(path.dirname(dest), { recursive: true });
@@ -56,7 +63,7 @@ export function runHarnessAdapters({ root, entries, harnesses, prevManifest, sou
56
63
  }
57
64
  summary.push(`claude-code → .claude (${n} files)`);
58
65
  }
59
- writeBlock(root, 'CLAUDE.md', on('claude-code') ? orientationBlock('claude-code', skillsIndex, sourceLayout, namespaces, version, entries) : null);
66
+ block('CLAUDE.md', on('claude-code') ? orientationBlock('claude-code', skillsIndex, sourceLayout, namespaces, version, entries, workspaceModule) : null);
60
67
 
61
68
  // ---- shared cross-agent skills mirror (.agents/skills) — codex/pi discover it,
62
69
  // cursor/gemini blocks point at it. written once no matter how many harnesses use it.
@@ -73,17 +80,17 @@ export function runHarnessAdapters({ root, entries, harnesses, prevManifest, sou
73
80
  }
74
81
 
75
82
  // ---- codex + pi: both read root AGENTS.md; one block serves both ----------------
76
- writeBlock(root, 'AGENTS.md', on('codex') || on('pi') ? orientationBlock('agents-md', skillsIndex, sourceLayout, namespaces, version, entries) : null);
83
+ block('AGENTS.md', on('codex') || on('pi') ? orientationBlock('agents-md', skillsIndex, sourceLayout, namespaces, version, entries, workspaceModule) : null);
77
84
  if (on('codex')) summary.push('codex → AGENTS.md block');
78
85
  if (on('pi')) summary.push('pi → AGENTS.md block + .agents/skills');
79
86
 
80
87
  // ---- gemini-cli: GEMINI.md is its context file -----------------------------------
81
- writeBlock(root, 'GEMINI.md', on('gemini-cli') ? orientationBlock('gemini', skillsIndex, sourceLayout, namespaces, version, entries) : null);
88
+ block('GEMINI.md', on('gemini-cli') ? orientationBlock('gemini', skillsIndex, sourceLayout, namespaces, version, entries, workspaceModule) : null);
82
89
  if (on('gemini-cli')) summary.push('gemini-cli → GEMINI.md block');
83
90
 
84
91
  // ---- cursor: native .mdc rule (alwaysApply) ---------------------------------------
85
92
  if (on('cursor')) {
86
- const mdc = `---\ndescription: dreamteamer workspace orientation (generated)\nalwaysApply: true\n---\n\n${orientationBlock('cursor', skillsIndex, sourceLayout, namespaces, version, entries)}\n\n${STAMP}\n`;
93
+ const mdc = `---\ndescription: dreamteamer workspace orientation (generated)\nalwaysApply: true\n---\n\n${orientationBlock('cursor', skillsIndex, sourceLayout, namespaces, version, entries, workspaceModule)}\n\n${STAMP}\n`;
87
94
  write('.cursor/rules/dreamteamer.mdc', Buffer.from(mdc));
88
95
  summary.push('cursor → .cursor/rules/dreamteamer.mdc');
89
96
  }
@@ -105,7 +112,7 @@ export function runHarnessAdapters({ root, entries, harnesses, prevManifest, sou
105
112
  // pruning leaves empty skill folders behind — sweep them (and the roots when hollow)
106
113
  for (const dir of ['.claude', '.agents', '.cursor']) pruneEmptyDirs(path.join(root, dir));
107
114
 
108
- return { outputs, summary };
115
+ return { outputs, blocks, summary };
109
116
  }
110
117
 
111
118
  // skill id → description one-liners from each SKILL.md's frontmatter; the orientation
@@ -144,11 +151,54 @@ function buildCollectionsIndex(entries) {
144
151
  system: d.storage?.base === 'runtime',
145
152
  description: flat(d.description),
146
153
  useWhen: flat(d.use_when),
154
+ module: d.module ?? '',
155
+ write: writeLine(d.schema),
147
156
  });
148
157
  }
149
158
  return index.sort((a, b) => a.name.localeCompare(b.name));
150
159
  }
151
160
 
161
+ /** What can REFUSE a write, in one clause — nothing else about the fields. Required fields with no
162
+ * default (the CLI materializes defaults, so those never refuse), closed enums with their size, and
163
+ * the first `examples:` value where one is authored. A blind session's plan was correct until its
164
+ * first `dt add`, which the store refused on a 100-value enum and a required field the block never
165
+ * showed; the descriptor stays the authority, this is the line that keeps the first write from
166
+ * bouncing. Empty string when there is nothing that refuses. */
167
+ function writeLine(schema) {
168
+ const props = schema?.properties ?? {};
169
+ const required = (schema?.required ?? []).filter((f) => props[f] && props[f].default === undefined);
170
+ const enums = Object.entries(props).flatMap(([f, p]) => { const e = p?.enum ?? p?.items?.enum; return Array.isArray(e) && e.length ? [`${f} enum(${e.length})`] : []; });
171
+ const egs = Object.entries(props).flatMap(([f, p]) => Array.isArray(p?.examples) && p.examples.length ? [`${f}='${p.examples[0]}'`] : []);
172
+ const parts = [...(required.length ? [`required ${required.join(' · ')}`] : []), ...(enums.length ? [enums.join(' · ')] : []), ...(egs.length ? [`e.g. ${egs.join(', ')}`] : [])];
173
+ return parts.join('; ');
174
+ }
175
+
176
+ /** module id -> title / description / namespaces / source path, plus the skills and commands whose
177
+ * SOURCE lives under that path — read from the module records compile projected, never from
178
+ * package.json again. A skill or command belongs to the module with the longest source-path prefix;
179
+ * the root layout (`path: .`) is the empty prefix and so matches last. */
180
+ function buildModulesIndex(entries) {
181
+ const mods = [];
182
+ for (const [rt, e] of entries) {
183
+ const m = /^modules\/(.+)\.module\.yaml$/.exec(rt);
184
+ if (!m) continue;
185
+ let d = {};
186
+ try { d = load(e.bytes.toString('utf8')) ?? {}; } catch { /* unparseable record */ }
187
+ const p = !d.path || d.path === '.' ? '' : `${d.path}/`;
188
+ mods.push({ id: m[1], title: d.title ?? m[1], description: flat(d.description), namespaces: d.namespaces ?? [], path: p, bin: d.bin ?? [], skills: [], commands: [] });
189
+ }
190
+ mods.sort((a, b) => b.path.length - a.path.length); // longest prefix first
191
+ for (const [rt, e] of entries) {
192
+ const kind = /^skills\/([^/]+)\/SKILL\.md$/.exec(rt) ? 'skills' : /^commands\/(.+)\.command\.md$/.exec(rt) ? 'commands' : null;
193
+ if (!kind) continue;
194
+ const src = e.sources?.[0]?.path ?? '';
195
+ const owner = mods.find((mod) => src.startsWith(mod.path));
196
+ if (owner) owner[kind].push(kind === 'skills' ? rt.split('/')[1] : path.basename(rt).replace(/\.command\.md$/, ''));
197
+ }
198
+ for (const mod of mods) { mod.skills.sort(); mod.commands.sort(); }
199
+ return mods;
200
+ }
201
+
152
202
  /** Commands, for the harnesses that do NOT discover them natively. claude-code writes every command
153
203
  * into `.claude/commands` and its harness injects name+description at session start, so an index in
154
204
  * CLAUDE.md would be a second copy of what is already there. codex/pi/gemini-cli/cursor got one
@@ -176,26 +226,42 @@ function buildCommandsIndex(entries) {
176
226
  * GEMINI.md, which are COMMITTED files. A count changes on every write, so every compile after
177
227
  * ordinary data work would re-dirty three tracked files — in trees where more than one agent has
178
228
  * uncommitted work — and the number would be stale the moment it was printed. Ordering is
179
- * namespace-grouped then alphabetical precisely so the block diffs ONLY when the schema changed. */
180
- function collectionsSection(index, namespaces) {
229
+ * module-grouped then alphabetical precisely so the block diffs ONLY when the schema changed.
230
+ *
231
+ * GROUPED BY MODULE, the workspace's own first. A module is the one entity that answers "what is
232
+ * this AREA of the workspace for" — its sentence heads the group, its namespaces, skills and
233
+ * commands ride on the line below it, and its collections follow. Before this the block listed
234
+ * nouns by namespace and the reader had to infer the domains from the prefixes; the hand-written
235
+ * module table one dogfood workspace kept to fill that gap was the same failure the collection list
236
+ * had already been through. A module whose every collection is system-stored (the engine) is not a
237
+ * domain and gets no group; a skills-only module is one and does. */
238
+ function collectionsSection(index, modules, workspaceModule) {
181
239
  if (!index.length) return [];
182
- const nsOf = (name) => {
183
- const i = name.indexOf('/');
184
- const p = i < 0 ? '' : name.slice(0, i);
185
- return namespaces.includes(p) ? p : '';
186
- };
187
240
  const lines = [
188
241
  '',
189
- 'COLLECTIONS — the nouns of this workspace. The descriptor at',
242
+ 'MODULES AND THEIR COLLECTIONS — what this workspace is for, one area at a time. A module ships',
243
+ 'one domain: its collections (the nouns), and the skills and commands that operate on them —',
244
+ 'read its skills before working in its area. The descriptor at',
190
245
  '`.dreamteamer/collections/<name>.collection.yaml` is the authority on fields, id shape and',
191
246
  'defaults — read it before writing a kind you have not written this session. Create records',
192
- 'with `dt add <collection>`: it generates the id and rejects invalid writes before disk.',
247
+ 'with `dt add <collection>`: it generates the id and rejects invalid writes before disk. A',
248
+ 'collection\'s `write:` line names only what the store REFUSES — required fields that have no',
249
+ 'default (a defaulted one is filled in for you), closed enums with their size, one example.',
193
250
  ];
194
251
  const data = index.filter((c) => !c.system);
195
- for (const group of ['', ...namespaces]) {
196
- for (const c of data.filter((c) => nsOf(c.name) === group)) {
252
+ const isWs = (m) => m.path === `modules/${workspaceModule}/`;
253
+ const groups = modules
254
+ .filter((m) => { const own = index.filter((c) => c.module === m.id); return own.some((c) => !c.system) || (!own.length && (m.skills.length || m.commands.length || m.bin.length)); })
255
+ .sort((a, b) => (isWs(b) - isWs(a)) || a.title.localeCompare(b.title));
256
+ for (const m of groups) {
257
+ const where = [`\`${m.id}\``, m.path ? m.path.replace(/\/$/, '') : 'the workspace root', ...(m.namespaces.length ? [`namespaces: ${m.namespaces.join(' · ')}`] : [])];
258
+ lines.push('', `**${m.title}** (${where.join(' · ')})${m.description ? ` — ${m.description}` : ''}`);
259
+ const ships = [...(m.skills.length ? [`skills: ${m.skills.join(' · ')}`] : []), ...(m.commands.length ? [`commands: ${m.commands.map((c) => `/${c}`).join(' · ')}`] : []), ...(m.bin.length ? [`runs: ${m.bin.join(' · ')}`] : [])];
260
+ if (ships.length) lines.push(` ${ships.join(' · ')}`);
261
+ for (const c of data.filter((c) => c.module === m.id)) {
197
262
  lines.push(`- ${c.name}${c.description ? ` — ${c.description}` : ''}`);
198
263
  if (c.useWhen) lines.push(` use when: ${c.useWhen}`);
264
+ if (c.write) lines.push(` write: ${c.write}`);
199
265
  }
200
266
  }
201
267
  const system = index.filter((c) => c.system).map((c) => c.name);
@@ -204,7 +270,7 @@ function collectionsSection(index, namespaces) {
204
270
  // It now names the VERBS and the one policy difference, because an agent that knows the verbs
205
271
  // exist still has to be told that these commit and records do not.
206
272
  if (system.length) {
207
- lines.push(`- system collections — the SAME verbs (add · set · rm · rename · list · get), plus \`dt add-field\`/\`update-field\`/\`remove-field\`/\`rename-field <collection>\`. A system write COMMITS ITSELF, in the repo holding the source; a record write does not (\`dt commit\` publishes). Never hand-edit \`.dreamteamer/\` — it is build output: ${system.join(' · ')}`);
273
+ lines.push('', `- system collections — the SAME verbs (add · set · rm · rename · list · get), plus \`dt add-field\`/\`update-field\`/\`remove-field\`/\`rename-field <collection>\`. A system write COMMITS ITSELF, in the repo holding the source; a record write does not (\`dt commit\` publishes). Never hand-edit \`.dreamteamer/\` — it is build output: ${system.join(' · ')}`);
208
274
  }
209
275
  return lines;
210
276
  }
@@ -295,7 +361,7 @@ function bindingsSection(entries) {
295
361
  * agent that splits `health/doctors/dana-levi` at the first slash reads a collection that does not
296
362
  * exist. Naming the declared list is what makes the grammar decidable from this block alone, without
297
363
  * the agent having to go read the manifest. A workspace with no namespaces gets no extra sentence. */
298
- function orientationBlock(flavor, skillsIndex, sourceLayout = 'flat', namespaces = [], version = 'unknown', entries = new Map()) {
364
+ function orientationBlock(flavor, skillsIndex, sourceLayout = 'flat', namespaces = [], version = 'unknown', entries = new Map(), workspaceModule = '') {
299
365
  const sourcesLine = {
300
366
  flat: '`modules/<module>/<kind>/` — `collections/`, `skills/`, `agents/`, `commands/`,',
301
367
  nested: '`modules/<module>/system/<kind>/` — `collections/`, `skills/`, `agents/`, `commands/`,',
@@ -327,7 +393,7 @@ function orientationBlock(flavor, skillsIndex, sourceLayout = 'flat', namespaces
327
393
  'DECLARED prefix, not at the first slash. collections with no prefix are unaffected.',
328
394
  );
329
395
  }
330
- lines.push(...collectionsSection(buildCollectionsIndex(entries), namespaces));
396
+ lines.push(...collectionsSection(buildCollectionsIndex(entries), buildModulesIndex(entries), workspaceModule));
331
397
  lines.push(...templatesSection(entries));
332
398
  lines.push(...bindingsSection(entries));
333
399
  // claude-code discovers skills natively (Skill tool) — an index in CLAUDE.md is pure
package/src/init.js CHANGED
@@ -33,6 +33,7 @@ media/
33
33
  // output, which reads as a broken install. One starter collection answers both. `notes` is
34
34
  // deliberately the most generic thing a workspace can hold.
35
35
  const STARTER_COLLECTION = `name: notes
36
+ description: A dated note — the starter collection init seeds, to rename or replace with what this workspace actually keeps.
36
37
  storage:
37
38
  path: data/notes
38
39
  codec: md
@@ -112,7 +113,11 @@ export function init({ flags = {} } = {}) {
112
113
  if (!fs.existsSync(modulePkg)) {
113
114
  // `files` is the npm publish surface: every kind a module can ship, since a new one here
114
115
  // is an engine change that would otherwise silently stop being packaged.
115
- fs.writeFileSync(modulePkg, JSON.stringify({ name: wm, private: true, version: '0.0.1', files: [...KINDS], dreamteamer: {} }, null, '\t') + '\n');
116
+ // `description` is npm's own key and the one compile falls back to for the module's sentence
117
+ // — the line that heads this module's group in the orientation block. Seeded so a fresh
118
+ // workspace does not open with a warning about the module init just made; the operator
119
+ // replaces it with what THIS workspace is for.
120
+ fs.writeFileSync(modulePkg, JSON.stringify({ name: wm, private: true, version: '0.0.1', description: "This workspace's own collections, skills and commands.", files: [...KINDS], dreamteamer: {} }, null, '\t') + '\n');
116
121
  }
117
122
  }
118
123
  // one starter collection, so `compile` has something to report instead of warning about the
@@ -5,7 +5,9 @@
5
5
  // the client adapter shrinks to a thin path translator. shapes deliberately match what
6
6
  // the studio components already speak (field {field,type,meta,schema}).
7
7
 
8
- import { sourceHint } from './runtime.js';
8
+ // `sourceHint` is deliberately NOT imported here any more — see the note beside `system` below.
9
+ // It survives for the store's own refusal (`store.js`) and `revert`'s (`collections-cli.js`), both
10
+ // of which are still true statements about a path that genuinely cannot be written.
9
11
  import { refTargetsOf } from './ref.js';
10
12
 
11
13
  /** the projection for every collection: rows keyed by collection name + collection meta. */
@@ -82,15 +84,34 @@ function collectionRow(d) {
82
84
 
83
85
  if (typeof d.group === 'string') meta.group = d.group;
84
86
  if (typeof d.description === 'string' && d.description.length > 0) meta.description = d.description;
85
- // A compiled collection is READ-ONLY through the record layer, and the UI needs to say so
86
- // BEFORE offering a button an error after the click is a worse answer than a disabled
87
- // control with a reason. The sentence comes from runtime.js so the store's refusal and this
88
- // hint can never disagree.
87
+ // `system` IS NOT `readonly`, AND SAYING SO COST A RELEASE. Until 0.19.0 the two were the
88
+ // same fact: nothing could write a compiled entity, so this projection set `meta.readonly` for
89
+ // every `storage.base === 'runtime'` collection and the UI disabled the form. 0.19.0 gave the
90
+ // record verbs a system write path — `set skills/<id>`, `rename`, `rm`, `set collections/<c>`,
91
+ // `set modules/<id>` — dispatched at the SURFACE, around the store (`collections-cli.js`,
92
+ // `server.js`). This file was never touched by that wave, so the projection kept describing the
93
+ // old world and a skill in the workspace's own `modules/<m>/skills/` rendered padlocked while
94
+ // `dt set` wrote it happily.
95
+ //
96
+ // So no collection-level readonly is emitted for a system kind any more. Three reasons it is a
97
+ // DELETION rather than a narrower predicate:
98
+ //
99
+ // 1. Writability is per RECORD, not per collection. `skills/<one>` authored in an inline
100
+ // module is writable; `skills/<another>` shipped from `node_modules/` is refused, because
101
+ // the next `npm install` erases the edit. One flag on the collection cannot say both, and
102
+ // a flag that is wrong half the time is worse than none.
103
+ // 2. On today's verb set there is no system kind with no writable path at all, so a correct
104
+ // collection-level predicate would be constant-false.
105
+ // 3. The refusals that remain are per record (npm-shipped), per verb (`revert`, and `add` on
106
+ // the hand-authored kinds) or per field (`x-body`) — none of them collection-shaped. Each
107
+ // already answers with its own sentence naming the fix, which is the contract the schema
108
+ // surfaces are built on.
109
+ //
110
+ // `system` STAYS and is unchanged: the CLI and REST dispatch key on it, and it is what puts a
111
+ // kind in the schema surface rather than the data one. A consumer that disables editing must
112
+ // key on `meta.readonly` (per field, as `id`, `last-modified` and relation mirrors do) or on
113
+ // the verb it is about to offer — never on `system`.
89
114
  const system = d.storage?.base === 'runtime';
90
- if (system) {
91
- meta.readonly = true;
92
- meta.readonly_hint = sourceHint(d);
93
- }
94
115
  return { collection: d.name, meta, system };
95
116
  }
96
117
 
@@ -131,6 +152,33 @@ function titleTemplateOf(prop, descriptors) {
131
152
  return typeof first === 'string' && first.length > 0 && inherited.every((v) => v === first) ? first : undefined;
132
153
  }
133
154
 
155
+ // The keys a descriptor may attach to an enum VALUE through `x-choices` (0.21.0), beyond the label.
156
+ // Copied BY NAME rather than spread: a descriptor is authored data, and spreading whatever it
157
+ // happens to carry would let a workspace inject arbitrary keys into a contract every surface reads.
158
+ // One line is the whole vocabulary, which is also what makes it readable from the consuming side.
159
+ //
160
+ // `icon` is a codicon name OR a reference to a record of a `codec: file` collection — the surface
161
+ // decides which by whether it resolves as a reference, so no second keyword and no set of known
162
+ // collection names. `color`/`background` are theme colour ids for the same reason a hex is not one:
163
+ // a hex is authored against one theme and wrong in the other.
164
+ const CHOICE_KEYS = ['description', 'icon', 'color', 'background'];
165
+
166
+ /**
167
+ * One `choices` row: `{ text, value }` as it has always been, plus whatever `x-choices` declared.
168
+ *
169
+ * ⚠ PURELY ADDITIVE. A value with no entry projects byte-identically to what it projected before
170
+ * this existed, which is the only reason not one of the descriptors already in the wild had to
171
+ * change. `label` arrives as `text` — the key every surface already reads — so a workspace can
172
+ * relabel a value without touching the value, and a stored record never moves.
173
+ */
174
+ function choiceRow(v, entry) {
175
+ const row = { text: String(entry?.label ?? v), value: v };
176
+ if (entry && typeof entry === 'object' && !Array.isArray(entry)) {
177
+ for (const k of CHOICE_KEYS) if (typeof entry[k] === 'string' && entry[k].length > 0) row[k] = entry[k];
178
+ }
179
+ return row;
180
+ }
181
+
134
182
  function fieldRow(d, name, prop, isRequired, descriptors) {
135
183
  const meta = { collection: d.name, field: name };
136
184
  if (isRequired) meta.required = true;
@@ -212,7 +260,7 @@ function fieldRow(d, name, prop, isRequired, descriptors) {
212
260
  }
213
261
 
214
262
  if (Array.isArray(prop.enum) && prop.enum.length > 0) {
215
- meta.edit_options = { choices: prop.enum.map((v) => ({ text: String(v), value: v })) };
263
+ meta.edit_options = { choices: prop.enum.map((v) => choiceRow(v, prop['x-choices']?.[v])) };
216
264
  }
217
265
  const tpl = titleTemplateOf(prop, descriptors);
218
266
  if (typeof tpl === 'string' && tpl.length > 0) meta.view_options = { ...meta.view_options, template: tpl };
package/src/schema-ops.js CHANGED
@@ -1697,7 +1697,7 @@ function isTracked(root, rel) {
1697
1697
  */
1698
1698
  function commitByRepo(ws, store, rels, subject) {
1699
1699
  const byRepo = new Map(); // workspace-relative repo root -> {root, paths relative to THAT repo}
1700
- for (const rel of new Set(rels)) {
1700
+ for (const rel of new Set([...rels, ...regeneratedOutputs(ws)])) {
1701
1701
  const abs = path.join(ws.root, rel);
1702
1702
  // A path that is neither on disk nor in any index cannot be a pathspec, and one bad entry
1703
1703
  // aborts the whole `git add` — the lesson `renameCollection` paid for. ⚠ The filter runs PER
@@ -1730,6 +1730,26 @@ function commitByRepo(ws, store, rels, subject) {
1730
1730
  return out;
1731
1731
  }
1732
1732
 
1733
+ /** The harness files the gate compile just REGENERATED — CLAUDE.md, AGENTS.md, GEMINI.md and whatever
1734
+ * else `dreamteamer.harnesses` writes — narrowed to the ones git already TRACKS. A schema write
1735
+ * committed only the mutated source, so every one left the three committed root files dirty with the
1736
+ * block that names the very change just committed; the next unscoped `git add` in a shared tree swept
1737
+ * them into somebody else's subject. The list is read off the manifest compile has just written
1738
+ * (`adapter-outputs` for the generated dirs, `adapter-blocks` for the root files whose managed block
1739
+ * was rewritten), so no API changes hands. TRACKED ONLY, in one `git ls-files` call: an untracked
1740
+ * root file is the operator's to add (a fresh workspace has not committed its instructions yet, and
1741
+ * a write into a clone must not start tracking files in the workspace repo as a side effect), and an
1742
+ * ignored one (`.claude/`) is a hard error to `git add`, so neither may reach the pathspec. */
1743
+ function regeneratedOutputs(ws) {
1744
+ const m = readManifest(ws.root) ?? {};
1745
+ const outputs = [...(m['adapter-outputs'] ?? []), ...(m['adapter-blocks'] ?? [])].filter((p) => fs.existsSync(path.join(ws.root, p)));
1746
+ if (!outputs.length) return [];
1747
+ try {
1748
+ return execFileSync('git', ['ls-files', '-z', '--', ...outputs], { cwd: ws.root, stdio: ['ignore', 'pipe', 'ignore'] })
1749
+ .toString().split('\0').filter(Boolean);
1750
+ } catch { return []; } // no git here at all — the source commit proceeds without them, as before
1751
+ }
1752
+
1733
1753
  const shortHead = (root) => {
1734
1754
  try { return execFileSync('git', ['rev-parse', '--short', 'HEAD'], { cwd: root, stdio: GIT_QUIET }).toString().trim(); } catch { return null; }
1735
1755
  };