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.
- package/collection-templates/docs.collection-template.yaml +3 -0
- package/collection-templates/entity.collection-template.yaml +4 -0
- package/collections/collections.collection.yaml +14 -9
- package/collections/modules.collection.yaml +9 -0
- package/collections/repos.collection.yaml +5 -0
- package/package.json +1 -1
- package/skills/using-dreamteamer/references/before-you-build.md +1 -1
- package/skills/using-dreamteamer/references/collections.md +41 -0
- package/skills/using-dreamteamer/references/data-modeling.md +31 -11
- package/src/compile.js +78 -10
- package/src/harnesses.js +86 -20
- package/src/init.js +6 -1
- package/src/presentation.js +58 -10
- package/src/schema-ops.js +21 -1
|
@@ -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:
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
a `how` belongs in the module's skill
|
|
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.
|
|
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
|
|
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
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
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
|
|
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
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
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
|
-
//
|
|
1302
|
-
//
|
|
1303
|
-
//
|
|
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
|
|
1336
|
-
try {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
180
|
-
|
|
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 —
|
|
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
|
-
|
|
196
|
-
|
|
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),
|
|
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
|
-
|
|
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
|
package/src/presentation.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
//
|
|
86
|
-
//
|
|
87
|
-
//
|
|
88
|
-
//
|
|
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) => (
|
|
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
|
};
|