@agentshouse/skills 0.0.0-stage → 0.1.0-c24b2d26daab4

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.
Files changed (114) hide show
  1. package/README.md +30 -2
  2. package/agent-instructions.json +10 -0
  3. package/app-guide.json +346 -0
  4. package/dist/index.d.ts +33 -0
  5. package/dist/index.js +68 -0
  6. package/guide/get-started.md +9 -0
  7. package/guide/index.md +7 -0
  8. package/guide/reference/app-manifest.md +12 -0
  9. package/guide/reference/read-level.md +9 -0
  10. package/guide/reference/routine.md +14 -0
  11. package/guide/reference/schema.md +28 -0
  12. package/guide/reference/triggers.md +7 -0
  13. package/guide/sites/entry-data-contract.md +25 -0
  14. package/how-we-work-earlier.json +6 -0
  15. package/how-we-work.md +33 -0
  16. package/package.json +28 -4
  17. package/set/ask-matt/LICENSE +21 -0
  18. package/set/ask-matt/PHASE-BOUNDARIES.md +55 -0
  19. package/set/ask-matt/SKILL.md +97 -0
  20. package/set/ask-matt/agents/openai.yaml +5 -0
  21. package/set/build-app/SKILL.md +23 -0
  22. package/set/code-review/LICENSE +21 -0
  23. package/set/code-review/SKILL.md +89 -0
  24. package/set/code-review/agents/openai.yaml +3 -0
  25. package/set/codebase-design/DEEPENING.md +37 -0
  26. package/set/codebase-design/DESIGN-IT-TWICE.md +44 -0
  27. package/set/codebase-design/LICENSE +21 -0
  28. package/set/codebase-design/SKILL.md +114 -0
  29. package/set/codebase-design/agents/openai.yaml +3 -0
  30. package/set/diagnosing-bugs/LICENSE +21 -0
  31. package/set/diagnosing-bugs/SKILL.md +138 -0
  32. package/set/diagnosing-bugs/agents/openai.yaml +3 -0
  33. package/set/diagnosing-bugs/scripts/hitl-loop.template.sh +44 -0
  34. package/set/domain-modeling/ADR-FORMAT.md +47 -0
  35. package/set/domain-modeling/GLOSSARY-FORMAT.md +60 -0
  36. package/set/domain-modeling/LICENSE +21 -0
  37. package/set/domain-modeling/SKILL.md +74 -0
  38. package/set/domain-modeling/agents/openai.yaml +3 -0
  39. package/set/grill-me/LICENSE +21 -0
  40. package/set/grill-me/SKILL.md +7 -0
  41. package/set/grill-me/agents/openai.yaml +5 -0
  42. package/set/grill-with-docs/LICENSE +21 -0
  43. package/set/grill-with-docs/SKILL.md +7 -0
  44. package/set/grill-with-docs/agents/openai.yaml +5 -0
  45. package/set/grilling/LICENSE +21 -0
  46. package/set/grilling/SKILL.md +30 -0
  47. package/set/grilling/agents/openai.yaml +3 -0
  48. package/set/handoff/LICENSE +21 -0
  49. package/set/handoff/SKILL.md +16 -0
  50. package/set/handoff/agents/openai.yaml +5 -0
  51. package/set/implement/LICENSE +21 -0
  52. package/set/implement/SKILL.md +17 -0
  53. package/set/implement/agents/openai.yaml +5 -0
  54. package/set/implement-spec/LICENSE +21 -0
  55. package/set/implement-spec/SKILL.md +40 -0
  56. package/set/implement-spec/agents/openai.yaml +5 -0
  57. package/set/improve-codebase-architecture/HTML-REPORT.md +123 -0
  58. package/set/improve-codebase-architecture/LICENSE +21 -0
  59. package/set/improve-codebase-architecture/SKILL.md +71 -0
  60. package/set/improve-codebase-architecture/agents/openai.yaml +5 -0
  61. package/set/pr/CREDITS.md +3 -0
  62. package/set/pr/LICENSE +21 -0
  63. package/set/pr/SKILL.md +170 -0
  64. package/set/pr/agents/openai.yaml +3 -0
  65. package/set/prototype/LICENSE +21 -0
  66. package/set/prototype/LOGIC.md +67 -0
  67. package/set/prototype/SKILL.md +26 -0
  68. package/set/prototype/UI.md +112 -0
  69. package/set/prototype/agents/openai.yaml +3 -0
  70. package/set/prototype-app/SKILL.md +40 -0
  71. package/set/research/LICENSE +21 -0
  72. package/set/research/SKILL.md +12 -0
  73. package/set/research/agents/openai.yaml +3 -0
  74. package/set/retro/LICENSE +21 -0
  75. package/set/retro/SKILL.md +44 -0
  76. package/set/retro/agents/openai.yaml +5 -0
  77. package/set/tdd/LICENSE +21 -0
  78. package/set/tdd/SKILL.md +38 -0
  79. package/set/tdd/agents/openai.yaml +3 -0
  80. package/set/tdd/mocking.md +59 -0
  81. package/set/tdd/tests.md +77 -0
  82. package/set/teach/GLOSSARY-FORMAT.md +35 -0
  83. package/set/teach/LEARNING-RECORD-FORMAT.md +46 -0
  84. package/set/teach/LICENSE +21 -0
  85. package/set/teach/MISSION-FORMAT.md +31 -0
  86. package/set/teach/RESOURCES-FORMAT.md +32 -0
  87. package/set/teach/SKILL.md +140 -0
  88. package/set/teach/agents/openai.yaml +5 -0
  89. package/set/to-questionnaire/LICENSE +21 -0
  90. package/set/to-questionnaire/SKILL.md +54 -0
  91. package/set/to-questionnaire/agents/openai.yaml +5 -0
  92. package/set/to-spec/LICENSE +21 -0
  93. package/set/to-spec/SKILL.md +75 -0
  94. package/set/to-spec/agents/openai.yaml +5 -0
  95. package/set/to-tickets/LICENSE +21 -0
  96. package/set/to-tickets/SKILL.md +105 -0
  97. package/set/to-tickets/agents/openai.yaml +5 -0
  98. package/set/triage/AGENT-BRIEF.md +207 -0
  99. package/set/triage/LICENSE +21 -0
  100. package/set/triage/OUT-OF-SCOPE.md +105 -0
  101. package/set/triage/SKILL.md +112 -0
  102. package/set/triage/agents/openai.yaml +5 -0
  103. package/set/wait-what/LICENSE +21 -0
  104. package/set/wait-what/SKILL.md +7 -0
  105. package/set/wait-what/agents/openai.yaml +5 -0
  106. package/set/wayfinder/LICENSE +21 -0
  107. package/set/wayfinder/SKILL.md +128 -0
  108. package/set/wayfinder/agents/openai.yaml +5 -0
  109. package/set/wizard/SKILL.md +16 -0
  110. package/set/writing-for-agents/LICENSE +21 -0
  111. package/set/writing-for-agents/SKILL-MECHANICS.md +22 -0
  112. package/set/writing-for-agents/SKILL.md +81 -0
  113. package/set/writing-for-agents/agents/openai.yaml +3 -0
  114. package/upstream.json +32 -0
package/README.md CHANGED
@@ -1,3 +1,31 @@
1
- # Temporary Holding Version
1
+ # `@agentshouse/skills`
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ The Skills agents.house serves its agents, and the texts its agents read.
4
+
5
+ - `set/` holds one directory per Skill, each with its `SKILL.md`: every engineering and
6
+ productivity skill of [mattpocock/skills](https://github.com/mattpocock/skills) but its setup
7
+ skill and `wizard`, file for file beside upstream's `LICENSE`. `upstream.json` names the
8
+ upstream commit, its date and the skills taken from it. Beside them stand agents.house's own
9
+ `prototype-app`, `build-app` and `wizard`.
10
+ - `guide/` is the Guide, the pages agents.house serves its agents by path: `index` is its root, and
11
+ each page opens with its title and one line on what it is for.
12
+ - `how-we-work.md` is the How-we-work document's text, and `how-we-work-earlier.json` the
13
+ SHA-256 of every earlier text.
14
+ - `app-guide.json` is the guide to building an App or a Site, and `agent-instructions.json` the
15
+ lines that open an agent's first prompt.
16
+
17
+ ```ts
18
+ import { HOW_WE_WORK, listSkills, readSkill } from '@agentshouse/skills';
19
+
20
+ for (const { name } of listSkills()) console.log(name, readSkill(name).length);
21
+ console.log(HOW_WE_WORK);
22
+ ```
23
+
24
+ `agentInstructions(houseRef, copies)` returns the first prompt's lines, `GUIDE` every Guide page
25
+ with its path, title, first line and text, `APP_GUIDE` the App guide's lines and
26
+ `EARLIER_HOW_WE_WORK_SHA256` the earlier texts' digests.
27
+
28
+ The skills come from [mattpocock/skills](https://github.com/mattpocock/skills); thank you, Matt
29
+ Pocock.
30
+
31
+ MIT.
@@ -0,0 +1,10 @@
1
+ {
2
+ "lines": [
3
+ "From House:",
4
+ "Work with House through the `house` CLI: run `house --help`, and `house open_guide` opens `index`, the root page of House's guides and formats.",
5
+ "To give the User a file, upload it with `house upload` and link it; for an App, a Site or a website, follow `house find_command '{\"query\":\"app\"}'`. Never start a server.",
6
+ "Optional per-Room instructions: `/rooms/private/rooms/<handle>/AGENTS.md`.",
7
+ "For another Agent's help, `send_routine_request`; its mail comes back into this conversation. If an Agent of your User has no Routine to call, write one in the Private Room and `create_routine_access` with your User as its only caller."
8
+ ],
9
+ "copies": "House Local copies; before work run `house fetch` and rebase onto `<house-ref>`; after it commit, then `house push`:"
10
+ }
package/app-guide.json ADDED
@@ -0,0 +1,346 @@
1
+ {
2
+ "name": "App",
3
+ "aliases": [
4
+ {
5
+ "act": "sites/create",
6
+ "text": "site"
7
+ }
8
+ ],
9
+ "words": [
10
+ "app",
11
+ "apps",
12
+ {
13
+ "act": "sites/create",
14
+ "text": "site"
15
+ },
16
+ {
17
+ "act": "sites/create",
18
+ "text": "website"
19
+ },
20
+ "web",
21
+ "ui",
22
+ "frontend",
23
+ "html",
24
+ "build",
25
+ "create",
26
+ "new",
27
+ "make",
28
+ "deploy",
29
+ "publish"
30
+ ],
31
+ "text": [
32
+ {
33
+ "act": "sites/create",
34
+ "text": "# Build an App or a Site",
35
+ "without": "# Build an App"
36
+ },
37
+ "",
38
+ {
39
+ "act": "sites/create",
40
+ "text": "An App is a web page over your User's Rooms, for your User and whom they share it with (`create_share`); a Site is one at a public address anyone opens without signing in. House serves the built files and gives the page the App runtime, which holds every bound Room in the browser and syncs it with House. There is no server, backend or House API of yours to call.",
41
+ "without": "An App is a web page over your User's Rooms, for your User and whom they share it with (`create_share`). House serves the built files and gives the page the App runtime, which holds every bound Room in the browser and syncs it with House. There is no server, backend or House API of yours to call."
42
+ },
43
+ "",
44
+ "## Start",
45
+ "",
46
+ "- An App with an `a_` reference already exists, even with no release yet, as every App your User makes in the panel does. Clone its `.git` address, `<origin>/app/<app_ref>.git` or its `source_url` in `list_apps`, with `git clone -c 'credential.helper=!kit git-credential' -c user.name=<agent-git-name> -c user.email=<agent-git-email> <address>`, so Git signs in as you and can commit, in every later step too. Run `npm ci --ignore-scripts`, then publish with `npm run publish`, never with `--name`.",
47
+ "- Only when no App exists yet, clone `<origin>/app/new.git` the same way: it gives mdui, a minimal App in plain TypeScript with its test and `npm run publish`. Run `npm ci --ignore-scripts`, then `npm run publish -- --name \"<display name>\"`, which creates the App and prints its address and its `.git` address. Then `git remote set-url origin <that .git address>`; every later `npm run publish` goes there, without `--name`.",
48
+ {
49
+ "act": "sites/create",
50
+ "text": "- A Site starts with `create_site`: `name` is its address name, and the answer's `source_url` is its App's `.git` address to clone the same way. Anything private is an App."
51
+ },
52
+ {
53
+ "act": "sites/create",
54
+ "text": "- A Site shows the Rooms it published, as `published` names them: creating it publishes its bound Rooms, a release keeps the published data, and later Room edits stay private until `publish_site_data`. A publication whose `state` is `preparing` replaces the shown one once its screenshot is taken; `list_sites` names each Site's newest publication and its `state`, and `publish_site_data` with nothing changed since answers that publication. `unpublish_site` stops serving a Site and keeps it; `republish_site` or `publish_site_data` serves it again."
55
+ },
56
+ "- Use any framework and build tool. `npm run build` leaves the whole release in `dist/`: the entry HTML, every file it loads, `app.json` and `schema.yaml`. The entry names files by relative paths (Vite: `base: './'`), carries no inline script and loads nothing from outside the release, so bundle fonts and libraries. A release holds at most <release-files> files and <release-size>.",
57
+ "- Your User sees a change only once it is published, so an ask to change an App is an ask to publish it: end every change with a commit and `npm run publish`, which builds, pushes `main` and the build, and prints `published` or the reason House refused. A `git push` of `main` alone publishes nothing and says so. A refused push changes nothing and the previous release keeps running.",
58
+ "- Without Git, zip the project folder after the build, without `node_modules` and `.env`, `upload` the ZIP to a Room and publish it with `publish_app_zip`.",
59
+ "- Every page registers `bridge.cooperate` in its entry script before its first draw, as mdui's `main.ts` does, even a page that reads no Room: only then does a page your User has open take each release by itself, with no reload. A page that registers none keeps the release it opened on until it is opened again. A page that draws everything at once and has nothing to type or select registers `bridge.cooperate({ checkpoint() {}, persist() {}, capture: () => scrollY, restore(top) { scrollTo(0, Number(top)); }, ready: () => true, settle() {} })` after `const bridge = houseBridge()`, which keeps its scroll position. Every other page keeps its User's task through the release as the example below does: `checkpoint` waits until the page has drawn, `persist` until its Room Draft writes are done, failing while a field it shows was refused its last write so the release waits, `capture` answers the focused field with its cursor and selection, the scroll position and whatever else it holds outside the address, and `restore` puts them back once the page has drawn again; `node_modules/@agentshouse/app-bridge/docs/families.md` describes each member.",
60
+ "",
61
+ "## Data",
62
+ "",
63
+ "- Design the Room schema first. Each thing the App shows is one Markdown document in a Room: its frontmatter `type` names its MD type, the rest of its frontmatter holds the fields, and long text goes in `##` sections. `schema.yaml` declares the types (`open_guide` `reference/schema`).",
64
+ "- Keep each type in its own folder, such as `tasks/`, and read it by `pathPrefix`: a query naming `types` fails with `unknown-type` while no bound Room declares them.",
65
+ "- Link types with `reference` fields instead of copying values; a query follows them with `hop` and `backlinks`.",
66
+ {
67
+ "act": "sites/create",
68
+ "text": "- `app.json` `room_slots` names the Rooms the App needs, each with `access` `read` or `write` and the `types` it uses (`open_guide` `reference/app-manifest`). Bind Rooms with `set_app_bindings`, a Site's with `set_site_bindings`; App code cannot bind and reaches only bound Rooms, so it opens `bridge.ceremony('/bind/<app_ref>?slot=<name>')`, the House page where your User binds Rooms and connects account slots, which returns to the App.",
69
+ "without": "- `app.json` `room_slots` names the Rooms the App needs, each with `access` `read` or `write` and the `types` it uses (`open_guide` `reference/app-manifest`). Bind Rooms with `set_app_bindings`; App code cannot bind and reaches only bound Rooms, so it opens `bridge.ceremony('/bind/<app_ref>?slot=<name>')`, the House page where your User binds Rooms and connects account slots, which returns to the App."
70
+ },
71
+ "- A bound Room must hold each slot type identically in its own `settings/schema.yaml`, beside any other types, or your User is warned. Write it with `edit` before binding; `create_room` makes a new Room.",
72
+ "- Read an existing Room's `settings/schema.yaml` before inventing types. Two Apps over one schema are two interfaces over the same data, and your User's Agents read and write those documents too.",
73
+ "- What no browser can do, such as acting when documents change or at a set time, is a Trigger (`create_trigger`) that notifies or starts a Routine on an Agent; what it writes reaches the App at once.",
74
+ "",
75
+ "## Local first",
76
+ "",
77
+ "- The runtime holds every bound Room on the device and streams in every change, from any device, App or Agent. `query`, `search` and `observe` answer from the device and never wait on the network.",
78
+ "- Draw from `observe` at once, with no loading spinner for Room data and no request of your own; an observation hears each later change by itself, so never poll, refetch or subscribe to House. An answer is `{ pending }` while a Room it reads has not reached this device or been prepared on it, or the search index has not: draw the page without those documents, and keep the empty state for a complete answer that has none.",
79
+ "- Write optimistically: write a Room Draft, then `save`. The change shows in every query at once as a Pending Change and reaches House when it can. `save` refuses at once, with its `code`, a change that is `stale`, `changed-underneath` or against the schema: say so and keep the form.",
80
+ "- Show what the runtime reports: `observe('queue', room)` lists changes House has not confirmed yet, and `observe('sync-status')`'s `attentionRequired` lists each change House refused, returned as a Room Draft with why, to save again or discard.",
81
+ "- Keep what the User types in the Room Draft (`roomDraft.write`) as they type; it survives a new release replacing the page, a reload and another tab. Draw a form again only when its page changes, never on a streamed change.",
82
+ "- A document has one Room Draft, shared by every tab and App, and `save` takes all of it. A write replaces every part and names the revision it read, so write onto the parts it holds; one from an older revision answers `stale` with the current Room Draft: show that one. A field part names as its `base` the value the page showed, `{ value }`, or `null` when the document has no such field.",
83
+ "- Save a change the User does not type, such as a checkbox, with `saveParts(room, [{ path, parts }])`: it writes no Room Draft, so the one another tab holds stays.",
84
+ "- Offline, all of this works and saves wait. `bridge.onStatus` reports `offline`; say so quietly.",
85
+ "- A Room your User reads by Online Read locks once the browser has been offline 2 minutes, and from the moment it opens until House confirms the access. While it is locked, `rooms` lists it `locked: true`, a query or search that does not name it leaves it out, one naming it answers `{ pending }` with `awaits: 'unlock'`, and every other read and write naming it, `save` included, answers the code `room-locked` and keeps nothing. Draw it as locked until it unlocks and keep what the User types in the page; what was saved before stays and is sent then.",
86
+ "- An outside service is reached only at the origins `app.json` lists in `external_origins`; a keyed one through an `account_slots` entry and `bridge.callExternal` naming the slot and a bound Room that connects it, where House adds the key the page never sees.",
87
+ "- The page's own storage is a disposable cache, the Room the only durable home of the App's data; handle any code or value you do not know through `code` and `retryable`, which every refusal and failure carries.",
88
+ "- React: `useObserve` and `useSave` from `@agentshouse/mdruntime/react`. react-admin and Refine: the data providers of `@agentshouse/mdruntime-families`. The whole client is described in `node_modules/@agentshouse/mdruntime/AGENTS.md`.",
89
+ "",
90
+ "## Example: tasks",
91
+ "",
92
+ "`schema.yaml`, also written into the Room's `settings/schema.yaml`:",
93
+ "",
94
+ "```yaml",
95
+ "task:",
96
+ " properties:",
97
+ " title: { type: string }",
98
+ " done: { type: boolean }",
99
+ " required: [title, done]",
100
+ "```",
101
+ "",
102
+ "`app.json`, then `set_app_bindings` with slot `tasks`:",
103
+ "",
104
+ "```json",
105
+ "{ \"entry\": \"index.html\", \"room_slots\": [{ \"name\": \"tasks\", \"access\": \"write\", \"types\": [\"task\"] }] }",
106
+ "```",
107
+ "",
108
+ "`main.ts`: a list at `/` and a form at each task's own address, keeping every rule above.",
109
+ "",
110
+ "```ts",
111
+ "import { houseBridge } from '@agentshouse/app-bridge';",
112
+ "",
113
+ "type Task = { top: number; field: string | null; start: number | null; end: number | null };",
114
+ "",
115
+ "const bridge = houseBridge();",
116
+ "let shown!: () => void;",
117
+ "const showing = new Promise<void>((resolve) => (shown = resolve));",
118
+ "let writing: Promise<unknown> = Promise.resolve();",
119
+ "const unsaved = new Set<HTMLInputElement>();",
120
+ "bridge.cooperate({",
121
+ " checkpoint: () => showing,",
122
+ " async persist() {",
123
+ " await writing;",
124
+ " if ([...unsaved].some((title) => title.isConnected)) throw new Error('unsaved');",
125
+ " },",
126
+ " capture(): Task {",
127
+ " const at = document.activeElement;",
128
+ " const field = at instanceof HTMLInputElement && at.selectionStart !== null ? at : null;",
129
+ " return { top: scrollY, field: field?.ariaLabel ?? null, start: field?.selectionStart ?? null, end: field?.selectionEnd ?? null };",
130
+ " },",
131
+ " async restore(task) {",
132
+ " await showing;",
133
+ " const held = task as Task | null;",
134
+ " if (held === null) return;",
135
+ " const field = Array.from(document.querySelectorAll('input')).find((input) => held.field !== null && input.ariaLabel === held.field);",
136
+ " field?.focus({ preventScroll: true });",
137
+ " field?.setSelectionRange(held.start, held.end);",
138
+ " scrollTo(0, held.top);",
139
+ " },",
140
+ " ready: () => true,",
141
+ " settle() {},",
142
+ "});",
143
+ "const runtime = await bridge.runtime;",
144
+ "const rooms = await runtime.observe('rooms');",
145
+ "const sync = await runtime.observe('sync-status');",
146
+ "const drafts = await runtime.observe('room-drafts');",
147
+ "const tasks = await runtime.observe('query', {",
148
+ " pathPrefix: 'tasks',",
149
+ " select: { fields: ['/title', '/done'] },",
150
+ " orderBy: [{ by: { field: '/title' } }],",
151
+ "});",
152
+ "const main = document.querySelector('main')!;",
153
+ "const list = document.createElement('div');",
154
+ "const note = document.createElement('p');",
155
+ "type Parts = Parameters<typeof runtime.roomDraft.write>[3];",
156
+ "type Draft = Awaited<ReturnType<typeof runtime.roomDraft.read>>;",
157
+ "let drawn: string | undefined;",
158
+ "",
159
+ "async function write(room: string, path: string, held: Draft, parts: Parts) {",
160
+ " const { file: _file, ...kept } = { ...held?.parts };",
161
+ " const onto = { ...kept, ...parts, fields: { ...kept.fields, ...parts.fields } };",
162
+ " const written = await runtime.roomDraft.write(room, path, held?.revision ?? null, onto);",
163
+ " return written.outcome === 'written' ? { draft: written.roomDraft, stale: false } : { draft: written.current, stale: true };",
164
+ "}",
165
+ "",
166
+ "async function save(room: string, draft: Draft): Promise<boolean> {",
167
+ " const saved = draft === null ? null : await runtime.save(room, [{ path: draft.path, revision: draft.revision }]);",
168
+ " note.textContent = saved?.outcome === 'saved' ? '' : `Not saved: ${saved?.code ?? 'stale'}.`;",
169
+ " return saved?.outcome === 'saved';",
170
+ "}",
171
+ "",
172
+ "function queued<T>(work: () => Promise<T>): Promise<T | null> {",
173
+ " const done = writing.then(work).catch((error: { code?: unknown }) => {",
174
+ " note.textContent = `Not saved: ${String(error.code)}.`;",
175
+ " return null;",
176
+ " });",
177
+ " writing = done;",
178
+ " return done;",
179
+ "}",
180
+ "",
181
+ "function taskCreation(draft: Draft) {",
182
+ " const creation = draft?.parts.creation;",
183
+ " return creation !== undefined && 'type' in creation && creation.type === 'task' ? creation : undefined;",
184
+ "}",
185
+ "",
186
+ "function titleOf(draft: Draft): unknown {",
187
+ " return draft?.parts.fields?.title?.value ?? taskCreation(draft)?.fields.title;",
188
+ "}",
189
+ "",
190
+ "function el<K extends keyof HTMLElementTagNameMap>(tag: K, props: object, ...children: (Node | string)[]) {",
191
+ " const node = Object.assign(document.createElement(tag), props);",
192
+ " node.append(...children);",
193
+ " return node;",
194
+ "}",
195
+ "",
196
+ "function titleForm(action: string, room: string, path: () => string, shown: unknown, parts: (title: string, held: Draft) => Parts, done: (title: HTMLInputElement) => void) {",
197
+ " let held = drafts.current().find((draft) => draft.room === room && draft.path === path()) ?? null;",
198
+ " const title = el('input', { value: String(titleOf(held) ?? shown), required: true, ariaLabel: 'Title' });",
199
+ " const keep = () =>",
200
+ " queued(async () => {",
201
+ " unsaved.add(title);",
202
+ " const written = await write(room, path(), held, parts(title.value, held));",
203
+ " unsaved.delete(title);",
204
+ " held = written.draft;",
205
+ " if (written.stale) title.value = String(titleOf(held) ?? shown);",
206
+ " return written.stale;",
207
+ " });",
208
+ " title.oninput = () => void keep();",
209
+ " const form = el('form', {}, title, el('button', {}, action));",
210
+ " form.onsubmit = async (event) => {",
211
+ " event.preventDefault();",
212
+ " const stale = await keep();",
213
+ " if (stale === null) return;",
214
+ " if (stale) return void (note.textContent = 'Not saved: stale.');",
215
+ " if (!(await save(room, held))) return;",
216
+ " held = null;",
217
+ " done(title);",
218
+ " };",
219
+ " return form;",
220
+ "}",
221
+ "",
222
+ "function editForm(room: string, path: string, shown: unknown) {",
223
+ " const parts = (title: string): Parts => ({ fields: { title: { value: title, base: { value: shown } } } });",
224
+ " return titleForm('Save', room, () => path, shown, parts, () => void bridge.push('/'));",
225
+ "}",
226
+ "",
227
+ "function addForm(room: string) {",
228
+ " const parts = (title: string, held: Draft): Parts => {",
229
+ " const kept = taskCreation(held) ?? { type: 'task', fields: { done: false }, sections: {} };",
230
+ " return { creation: { ...kept, fields: { ...kept.fields, title } } };",
231
+ " };",
232
+ " const adding = drafts.current().find((draft) => draft.room === room && draft.path.startsWith('tasks/') && taskCreation(draft));",
233
+ " let path = adding?.path ?? `tasks/${crypto.randomUUID()}.md`;",
234
+ " return titleForm('Add', room, () => path, '', parts, (title) => {",
235
+ " title.value = '';",
236
+ " path = `tasks/${crypto.randomUUID()}.md`;",
237
+ " });",
238
+ "}",
239
+ "",
240
+ "async function toggle(room: string, path: string, done: boolean): Promise<void> {",
241
+ " const parts = { fields: { done: { value: !done, base: { value: done } } } };",
242
+ " const saved = await runtime.saveParts(room, [{ path, parts }]);",
243
+ " note.textContent = saved.outcome === 'saved' ? '' : `Not saved: ${saved.code}.`;",
244
+ " draw();",
245
+ "}",
246
+ "",
247
+ "function draw(): void {",
248
+ " const room = rooms.current()[0];",
249
+ " const answer = tasks.current();",
250
+ " const held = 'pending' in answer ? [] : answer.documents;",
251
+ " const open = held.find((task) => `/${task.path}` === bridge.address());",
252
+ " const page = room === undefined ? '' : `${room.locator}/${open?.path ?? ''}`;",
253
+ " if (page !== drawn) {",
254
+ " drawn = page;",
255
+ " note.textContent = '';",
256
+ " if (room === undefined) main.replaceChildren('This App has no Room yet.');",
257
+ " else if (open !== undefined) main.replaceChildren(editForm(open.room, open.path, open.fields!.title), note);",
258
+ " else main.replaceChildren(list, addForm(room.locator), note);",
259
+ " }",
260
+ " if (!('pending' in answer)) shown();",
261
+ " if (room === undefined) return;",
262
+ " const refused = new Set(sync.current().attentionRequired.flatMap((entry) => ('path' in entry ? [entry.path] : [])));",
263
+ " const rows = held.map((task) => {",
264
+ " const done = task.fields!.done === true;",
265
+ " const box = el('input', { type: 'checkbox', checked: done, ariaLabel: 'Done' });",
266
+ " box.onchange = () => void queued(() => toggle(task.room, task.path, done));",
267
+ " const link = el('a', { href: task.path }, String(task.fields!.title));",
268
+ " link.onclick = (event) => {",
269
+ " event.preventDefault();",
270
+ " void bridge.push(`/${task.path}`);",
271
+ " };",
272
+ " return el('li', {}, box, link, refused.has(task.path) ? ' (refused)' : '');",
273
+ " });",
274
+ " const empty = 'pending' in answer ? '' : 'No tasks yet. Add the first one.';",
275
+ " list.replaceChildren(rows.length > 0 ? el('ul', {}, ...rows) : empty);",
276
+ "}",
277
+ "",
278
+ "for (const observed of [rooms, sync, tasks]) observed.subscribe(draw);",
279
+ "bridge.onAddress(draw);",
280
+ "draw();",
281
+ "```",
282
+ "",
283
+ "## The page",
284
+ "",
285
+ "- The App draws everything; House draws nothing around it. Set the tab's title with `bridge.setTab({ title })`.",
286
+ "- The address bar is the shell's, not your page's: read your place from `bridge.address()` and `bridge.onAddress`, and move with `bridge.push` or `bridge.replace`, so Back, Forward, reload and a shared link keep it. Keep the open item, tab, filter and search in the address. Addresses under `/render`, `/info` and `/tab-icon` are House's.",
287
+ "- Mobile first: lay out from 360 pixels wide up, with no action behind hover alone.",
288
+ "- Every control works from the keyboard, with visible focus and a label.",
289
+ "- An empty list says what it will hold and invites the first document.",
290
+ "- Long names truncate with an ellipsis and show in full on hover.",
291
+ "- The element is the link: a row, card or name opens its item; no \"Open\" buttons.",
292
+ "- Offer installation yourself: `bridge.install()` from a click shows the browser's prompt, or answers `manual` where you show how.",
293
+ "",
294
+ {
295
+ "act": "sites/create",
296
+ "text": "## Sites"
297
+ },
298
+ {
299
+ "act": "sites/create",
300
+ "text": ""
301
+ },
302
+ {
303
+ "act": "sites/create",
304
+ "text": "- A Site is public: anyone opens it, nobody signs in, and every visitor reads its bound Rooms, which must be your User's own and never the Private Room."
305
+ },
306
+ {
307
+ "act": "sites/create",
308
+ "text": "- Everything it shows lives as documents in its Room; put its demo data there, never in code."
309
+ },
310
+ {
311
+ "act": "sites/create",
312
+ "text": "- A visitor's edits stay in their browser and never reach House: write them with `save` as an App does."
313
+ },
314
+ {
315
+ "act": "sites/create",
316
+ "text": "- To receive messages from visitors, name an Intake Room with `set_site_intake_room` and send each with `runtime.call('submit', { content })`, online only."
317
+ },
318
+ {
319
+ "act": "sites/create",
320
+ "text": "- `describe_site` sets the title and description search engines show."
321
+ },
322
+ {
323
+ "act": "sites/create",
324
+ "text": ""
325
+ },
326
+ "## Verify",
327
+ "",
328
+ "Before `npm run publish`:",
329
+ "",
330
+ "- `npm test` passes against the newest `@agentshouse/app-bridge` and `@agentshouse/mdruntime` (`npm install --ignore-scripts <name>@latest`), since every page gets House's newest runtime. Keep mdui's test: it runs the built page on `createMemoryAdapter` from `@agentshouse/mdruntime/memory` and `createHouseBridgeTestHost` from `@agentshouse/app-bridge/test-host`, with no House.",
331
+ "- Every slot type is in `schema.yaml` exactly as in the bound Room's `settings/schema.yaml`.",
332
+ "- The entry script registers `bridge.cooperate` before its first draw.",
333
+ "- The page works 360 pixels wide, offline, and reloaded at a deep address.",
334
+ "",
335
+ "After it:",
336
+ "",
337
+ "- `list_apps` names the App's `published_release`, whose `revision` is the `main` commit it was built from; a clone, a fetch and `git ls-remote` see `main` alone.",
338
+ "- Bind the Room once with `set_app_bindings`.",
339
+ {
340
+ "act": "sites/create",
341
+ "text": "- See it yourself: sign your own browser in with `create_sign_in_code`, then open the App's url. A Site needs none.",
342
+ "without": "- See it yourself: sign your own browser in with `create_sign_in_code`, then open the App's url."
343
+ },
344
+ "- Give your User its address. An open page that registers `bridge.cooperate` takes each release by itself; nothing asks them to update."
345
+ ]
346
+ }
@@ -0,0 +1,33 @@
1
+ export type GuideLine = string | {
2
+ readonly act: string;
3
+ readonly text: string;
4
+ readonly without?: string;
5
+ };
6
+ export interface Guide {
7
+ readonly name: string;
8
+ readonly aliases: readonly GuideLine[];
9
+ readonly words: readonly GuideLine[];
10
+ readonly text: readonly GuideLine[];
11
+ }
12
+ export interface Skill {
13
+ readonly name: string;
14
+ readonly directory: string;
15
+ }
16
+ export interface SkillFile {
17
+ readonly path: string;
18
+ readonly content: string;
19
+ }
20
+ export interface GuidePage {
21
+ readonly path: string;
22
+ readonly title: string;
23
+ readonly line: string;
24
+ readonly text: string;
25
+ }
26
+ export declare const GUIDE_ROOT = "index";
27
+ export declare const GUIDE: readonly GuidePage[];
28
+ export declare const HOW_WE_WORK: string;
29
+ export declare const EARLIER_HOW_WE_WORK_SHA256: readonly string[];
30
+ export declare const APP_GUIDE: Guide;
31
+ export declare function agentInstructions(houseRef: string, copies: readonly string[]): string[];
32
+ export declare function listSkills(): readonly Skill[];
33
+ export declare function readSkill(name: string): readonly SkillFile[];
package/dist/index.js ADDED
@@ -0,0 +1,68 @@
1
+ import { readdirSync, readFileSync } from 'node:fs';
2
+ import { join, relative, sep } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ const PACKAGE = fileURLToPath(new URL('..', import.meta.url));
5
+ const SET = join(PACKAGE, 'set');
6
+ const ENTRY = 'SKILL.md';
7
+ const REF_PLACEHOLDER = '<house-ref>';
8
+ const TREE = join(PACKAGE, 'guide');
9
+ const PAGE = '.md';
10
+ const FRONTMATTER = /^---\n[\s\S]*?\n---\n/u;
11
+ const HEADING = /^# /u;
12
+ export const GUIDE_ROOT = 'index';
13
+ function packaged(name) {
14
+ return readFileSync(join(PACKAGE, name), 'utf8');
15
+ }
16
+ function guidePage(file) {
17
+ const text = readFileSync(join(TREE, file), 'utf8');
18
+ const [heading = '', , line = ''] = text.replace(FRONTMATTER, '').split('\n');
19
+ return {
20
+ path: file.slice(0, -PAGE.length).split(sep).join('/'),
21
+ title: heading.replace(HEADING, ''),
22
+ line,
23
+ text,
24
+ };
25
+ }
26
+ export const GUIDE = readdirSync(TREE, {
27
+ recursive: true,
28
+ withFileTypes: true,
29
+ })
30
+ .filter((entry) => entry.isFile() && entry.name.endsWith(PAGE))
31
+ .map((entry) => guidePage(relative(TREE, join(entry.parentPath, entry.name))))
32
+ .sort((left, right) => (left.path < right.path ? -1 : 1));
33
+ export const HOW_WE_WORK = packaged('how-we-work.md');
34
+ export const EARLIER_HOW_WE_WORK_SHA256 = JSON.parse(packaged('how-we-work-earlier.json'));
35
+ export const APP_GUIDE = JSON.parse(packaged('app-guide.json'));
36
+ const INSTRUCTIONS = JSON.parse(packaged('agent-instructions.json'));
37
+ export function agentInstructions(houseRef, copies) {
38
+ if (copies.length === 0)
39
+ return [...INSTRUCTIONS.lines];
40
+ return [
41
+ ...INSTRUCTIONS.lines,
42
+ INSTRUCTIONS.copies.replaceAll(REF_PLACEHOLDER, houseRef),
43
+ ...copies,
44
+ ];
45
+ }
46
+ export function listSkills() {
47
+ return readdirSync(SET, { withFileTypes: true })
48
+ .filter((entry) => entry.isDirectory())
49
+ .map((entry) => entry.name)
50
+ .sort()
51
+ .map((name) => ({ name, directory: join(SET, name) }));
52
+ }
53
+ function entryFirst(left, right) {
54
+ if (left === ENTRY || right === ENTRY)
55
+ return left === ENTRY ? -1 : 1;
56
+ return left < right ? -1 : 1;
57
+ }
58
+ export function readSkill(name) {
59
+ const skill = listSkills().find((listed) => listed.name === name);
60
+ if (skill === undefined)
61
+ throw new Error(`no Skill is named ${name}`);
62
+ return readdirSync(skill.directory, { recursive: true, withFileTypes: true })
63
+ .filter((entry) => entry.isFile())
64
+ .map((entry) => relative(skill.directory, join(entry.parentPath, entry.name)))
65
+ .map((path) => path.split(sep).join('/'))
66
+ .sort(entryFirst)
67
+ .map((path) => ({ path, content: readFileSync(join(skill.directory, path), 'utf8') }));
68
+ }
@@ -0,0 +1,9 @@
1
+ # Get started
2
+
3
+ How your User signs up for House and connects you.
4
+
5
+ House keeps Markdown files in Rooms, which your User shares with other people and their agents.
6
+
7
+ Sign-up page: <origin>/sign-up
8
+
9
+ Unless this client already reads your User's Rooms, register there and connect it; then find out what your User wants, keep the plan with `edit` as documents in their First Room, which `list_rooms` names, and make the Rooms yourself with `create_room` through `run_command`.
package/guide/index.md ADDED
@@ -0,0 +1,7 @@
1
+ # House
2
+
3
+ Where House's guides and formats start: `open_guide` opens a page by its `path` or finds pages by `words`.
4
+
5
+ House keeps Markdown files in Rooms, which your User shares with other people and their agents.
6
+
7
+ - `reference/`, the formats House reads: [Read level](reference/read-level), [App manifest](reference/app-manifest), [Routine](reference/routine), [Schema](reference/schema) and [Triggers](reference/triggers).
@@ -0,0 +1,12 @@
1
+ # App manifest
2
+
3
+ What an App's `app.json` declares.
4
+
5
+ App manifest, `app.json` at an App’s root: `entry`, its HTML page, and `room_slots`, each with a `name`, `access` `read` or `write`, and `types` from the `schema.yaml` beside it, written like a Room schema; `set_app_bindings` fills a slot with Rooms. Where needed it adds `external_origins`, the https origins its page may reach; `account_slots`, each a `name`, the service’s `origin` and its key `form`, `{kind: bearer}`, `{kind: basic}`, `{kind: header, name}`, `{kind: query, name}` or `{kind: path, prefix}`; `conversations: true` to read your User’s conversations; `local_search: true` to call `search`; `inboxes: true` to show and answer customer chats, naming `inbox-chat` and `inbox-message` in `room_slots`; and `theme_colour`, `#rrggbb`. `find_command` "app" shows how its page works.
6
+
7
+ ```json
8
+ {
9
+ "entry": "index.html",
10
+ "room_slots": [{ "name": "tasks", "access": "write", "types": ["task"] }]
11
+ }
12
+ ```
@@ -0,0 +1,9 @@
1
+ # Read level
2
+
3
+ How much of a Room a grant or a Share lets its holder read, and where.
4
+
5
+ Read level, `read` on a Room grant or a Share’s Room item, each level including the one before it: `mcp`, MCP Read, reads through `search`, `shell` and `inspect` alone, each reply at most <mcp-read-size> of the Room’s text, with no copy anywhere; `online`, Online Read, also opens the Room in browser Apps while the browser is online, and reads as `mcp` everywhere else; `local`, Local Read, copies the Room to the browser and the Environments, offline included, and is the one `export` needs. Apps need `online` or `local`.
6
+
7
+ ```json
8
+ { "room_ref": "r_ab12.1", "read": "online", "operations": ["write"] }
9
+ ```
@@ -0,0 +1,14 @@
1
+ # Routine
2
+
3
+ How a Routine document in the Private Room is written.
4
+
5
+ Routine, `/rooms/private/collaboration/routines/<name>.md`: `description` and `input`, a text saying what to send, empty if nothing; the body is the instruction. A Routine Request's input is text the Agent reads, and House checks no shape.
6
+
7
+ ```markdown
8
+ ---
9
+ description: Summarize a thread in five bullets.
10
+ input: The thread to summarize, as a ref or pasted text.
11
+ ---
12
+
13
+ Read the thread and write five bullets under summaries/.
14
+ ```
@@ -0,0 +1,28 @@
1
+ # Schema
2
+
3
+ How a Room's `settings/schema.yaml` declares its document types.
4
+
5
+ Schema, `/rooms/<room>/settings/schema.yaml`: one JSON Schema in YAML. Each top-level key is an MD type holding the schema of its frontmatter; `$defs` holds shapes reused through `$ref: '#/$defs/<name>'`. An object setting neither `additionalProperties` nor `unevaluatedProperties` refuses other keys. House adds `reference` on a string at any depth, naming the types, or `attachment`, its value links to, and `cross_room: true` beside it, letting that be another Room's document of those types; a link or reference written as its `/rooms/<room>/` path is stored relative to the document; `sections`, a type's `##` headings, each `{ kind: text }` or `{ kind: list, items }`; and `member_document: true` on one type, whose documents name their member with `member`, a User Identifier from `list_members`. On a type, `create_roles` lists the Schema Roles that may create its documents and `delete_roles` those that may delete them, and changing a document's `type` needs both; on a field of a type's own `properties`, `change_roles` lists those that may change it, `initial_roles` those that may set it on a new document, and `transitions` the `{from, to, roles}` changes it may make, `null` standing for absent; `owner` and `member` are built in, `[]` allows nobody, and only someone who manages the Room's access changes them. `edit` creates or replaces the whole text, and may rewrite the documents a change breaks in the same call. A Room may use a Shared schema instead, which `use_shared_schema` chooses: its `settings/schema.yaml` is then a read-only copy of it.
6
+
7
+ ```yaml
8
+ person:
9
+ member_document: true
10
+ properties:
11
+ role: { type: string }
12
+ task:
13
+ properties:
14
+ title: { type: string }
15
+ status: { enum: [open, done] }
16
+ owner: { type: string, reference: person }
17
+ required: [title]
18
+ sections:
19
+ Notes: { kind: text }
20
+ ```
21
+
22
+ ```markdown
23
+ ---
24
+ type: person
25
+ member: <User Identifier from list_members>
26
+ role: Designer
27
+ ---
28
+ ```
@@ -0,0 +1,7 @@
1
+ # Triggers
2
+
3
+ How a Trigger's `condition` and `actions` are written for `create_trigger` and `edit_trigger`.
4
+
5
+ `condition`: When it fires, e.g. `{on: "documents", scope: {type: "task"}, mode: "starts_matching", new_documents: false, match: {all: [{path: "/fields/status", eq: "review"}, {path: "/members/assignee", eq: "<User id>"}]}}`; for a new document in a folder, `{on: "documents", scope: {folder: "/answers"}, mode: "starts_matching", new_documents: true, match: {all: []}}`; on documents, `moved_between_rooms: true` fires only on a document moved in from or out to another Room, `false` only on any other change; for arriving items, `{on: "items", address: "<push address>", match: {path: "/json/ref", eq: "refs/heads/main"}}`. A clock is `{on: "clock", cron: "0 9 * * 1", time_zone: "Europe/Berlin"}`, or a day before each task is due: `{on: "clock", scope: {type: "task"}, date: "/fields/due", days: -1, at: "09:00", time_zone: "Europe/Berlin", match: <test>}`.
6
+
7
+ `actions`: What it does; a `notify` action names whom it reaches, e.g. `{kind: "notify", title: "Ready to test", text: "A task waits.", to: {users: ["<User id>"], fields: ["/members/assignee"], roles: ["owner"]}}`, Users by id, the member a field of the matched document names, and every holder of a Schema Role; a `routine` action names a Routine Access.
@@ -0,0 +1,25 @@
1
+ # Entry Data Contract
2
+
3
+ How a Site App's `entry_data` has House serve each route as HTML from Rooms.
4
+
5
+ Entry Data Contract, `entry_data` in a Site App’s `app.json`: House serves each route as HTML from your Rooms before the App starts, running none of your code. Each of `routes` has a `path` (`/`, `/posts/:slug`), optional `query` (functional `?name` parameters with defaults), `queries` (Local Query objects with `types` and a `pathPrefix` at each scope, no `rooms`; `{"$param": "slug"}` takes an argument, `"as": "integer"` converts it; `boundQuery` from `@agentshouse/mdruntime/entry` fills the same query in your page), `interaction` (the queries its first interaction needs), `title`, `description` and `body`. A body node is text, `{text: v}`, `{markdown: v}` of a document, `{tag, attrs, children}`, `{each: v, as, children}` or `{if: v, then, else}`; a value `v` is a literal, `{param}`, `{at, pointer}` into a query result or `each` item, `{join: [v]}`, `{count: v}` or `{stem: v}`. `styles` lists release CSS files. A route that fails starts the App as usual, and the Site document names why.
6
+
7
+ ```json
8
+ {
9
+ "routes": [
10
+ {
11
+ "path": "/posts/:slug",
12
+ "queries": {
13
+ "post": {
14
+ "types": ["post"],
15
+ "pathPrefix": "posts",
16
+ "where": { "/slug": { "$param": "slug" } },
17
+ "select": { "fields": ["/title"], "markdown": true }
18
+ }
19
+ },
20
+ "title": { "at": "post", "pointer": "/documents/0/fields/title" },
21
+ "body": [{ "markdown": { "at": "post", "pointer": "/documents/0" } }]
22
+ }
23
+ ]
24
+ }
25
+ ```
@@ -0,0 +1,6 @@
1
+ [
2
+ "ae4dda60da7b174f22dc5fa9bf07fc27c25864932a74230de4f9f509ddd91f41",
3
+ "0bdcd43e1603ba6673a3cae56d70bbf90657cbeefd124e91a47be2ca1cbd6c93",
4
+ "4106a87ec66da6f7b2a10e74d18e6559d68d5f4fe7415dae68f5c7bdb9081968",
5
+ "ac814a2197038f037b657bb719340395f89f319ac9fcdd386629c3fe2cec68f1"
6
+ ]