@agentshouse/skills 0.1.0-cfb6385975145 → 0.1.0-cfeaa26aaefdb

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/README.md CHANGED
@@ -23,9 +23,9 @@ for (const { name } of listSkills()) console.log(name, readSkill(name).length);
23
23
  console.log(OWNER_RULES);
24
24
  ```
25
25
 
26
- `agentInstructions(houseRef, copies)` returns the first prompt's lines, `GUIDE` every Guide page
27
- with its path, title, first line and text, `WORK_ON_TICKET` the `routines/work-on-ticket` page's
28
- text and `EARLIER_OWNER_RULES_SHA256` the earlier texts' digests.
26
+ `agentInstructions(houseRef, copies)` returns the first prompt's lines, which name no Local copy,
27
+ `GUIDE` every Guide page with its path, title, first line and text and
28
+ `EARLIER_OWNER_RULES_SHA256` the earlier texts' digests.
29
29
 
30
30
  The skills come from [mattpocock/skills](https://github.com/mattpocock/skills); thank you, Matt
31
31
  Pocock.
@@ -4,7 +4,6 @@
4
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
5
  "To give the User a file, upload it with `house upload` and link it; for an App, a Site or a website, follow the Guide from `index` with `house open_guide`. Never start a server.",
6
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`:"
7
+ "For another Agent's help, `send_routine_request` naming one of the Room's Routine cards under `house/routines/`, or the Agent and what you ask, as its Trust level there allows; its mail comes back into this conversation."
8
+ ]
10
9
  }
package/dist/index.d.ts CHANGED
@@ -15,8 +15,7 @@ export interface GuidePage {
15
15
  export declare const GUIDE_ROOT = "index";
16
16
  export declare const GUIDE: readonly GuidePage[];
17
17
  export declare const OWNER_RULES: string;
18
- export declare const WORK_ON_TICKET: string;
19
18
  export declare const EARLIER_OWNER_RULES_SHA256: readonly string[];
20
- export declare function agentInstructions(houseRef: string, copies: readonly string[]): string[];
19
+ export declare function agentInstructions(_houseRef: string, _copies: readonly string[]): string[];
21
20
  export declare function listSkills(): readonly Skill[];
22
21
  export declare function readSkill(name: string): readonly SkillFile[];
package/dist/index.js CHANGED
@@ -4,14 +4,12 @@ import { fileURLToPath } from 'node:url';
4
4
  const PACKAGE = fileURLToPath(new URL('..', import.meta.url));
5
5
  const SET = join(PACKAGE, 'set');
6
6
  const ENTRY = 'SKILL.md';
7
- const REF_PLACEHOLDER = '<house-ref>';
8
7
  const TREE = join(PACKAGE, 'guide');
9
8
  const PAGE = '.md';
10
9
  const FRONTMATTER = /^---\n[\s\S]*?\n---\n\n?/u;
11
10
  const HEADING = /^# /u;
12
11
  export const GUIDE_ROOT = 'index';
13
12
  const OWNER_RULES_PAGE = 'owner-rules';
14
- const WORK_ON_TICKET_PAGE = 'routines/work-on-ticket';
15
13
  function packaged(name) {
16
14
  return readFileSync(join(PACKAGE, name), 'utf8');
17
15
  }
@@ -33,17 +31,10 @@ export const GUIDE = readdirSync(TREE, {
33
31
  .map((entry) => guidePage(relative(TREE, join(entry.parentPath, entry.name))))
34
32
  .sort((left, right) => (left.path < right.path ? -1 : 1));
35
33
  export const OWNER_RULES = GUIDE.find((page) => page.path === OWNER_RULES_PAGE).text;
36
- export const WORK_ON_TICKET = GUIDE.find((page) => page.path === WORK_ON_TICKET_PAGE).text;
37
34
  export const EARLIER_OWNER_RULES_SHA256 = JSON.parse(packaged('owner-rules-earlier.json'));
38
35
  const INSTRUCTIONS = JSON.parse(packaged('agent-instructions.json'));
39
- export function agentInstructions(houseRef, copies) {
40
- if (copies.length === 0)
41
- return [...INSTRUCTIONS.lines];
42
- return [
43
- ...INSTRUCTIONS.lines,
44
- INSTRUCTIONS.copies.replaceAll(REF_PLACEHOLDER, houseRef),
45
- ...copies,
46
- ];
36
+ export function agentInstructions(_houseRef, _copies) {
37
+ return [...INSTRUCTIONS.lines];
47
38
  }
48
39
  export function listSkills() {
49
40
  return readdirSync(SET, { withFileTypes: true })
@@ -11,3 +11,4 @@ An App is a web page over your User's Rooms, for your User and whom they share i
11
11
  - [An App's page](apps/page)
12
12
  - [Verify an App](apps/verify)
13
13
  - [The App runtime](apps/runtime)
14
+ - [App functions](apps/functions)
@@ -5,7 +5,10 @@ How an App keeps what it shows as typed documents in the Rooms bound to it.
5
5
  - 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 ([Schema](reference/schema)).
6
6
  - 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.
7
7
  - Link types with `reference` fields instead of copying values; a query follows them with `hop` and `backlinks`.
8
- - `app.json` `room_slots` names the Rooms the App needs, each with `access` `read` or `write` and the `types` it uses ([App manifest](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.
8
+ - `app.json` `room_slots` names the Rooms the App needs, each with `access` `read` or `write` and the `types` it uses ([App manifest](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, which returns to the App.
9
9
  - 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.
10
- - 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.
11
- - 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.
10
+ - 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.
11
+ - House's own facts, such as your User's Rooms and the Space each is in, come from House acts: name each in `app.json` `house_acts` and call it with `runtime.call('<act>', input)`, such as `runtime.call('rooms/read', {})`. A call needs a connection, so offline show what the bound Rooms hold. A slot holding the Private Room brings its Triggers and none of the other documents House writes under its `house/`.
12
+ - `shares/create` lets a person into Rooms and Apps: its answer's `added` is `true` when their _Who can add me_ setting admits your User, so they are in at once, and `false` when the Share waits for their Accept.
13
+ - What no browser can do, such as acting when documents change or at a set time, is a Trigger (`create_trigger`) that notifies or runs one of the App's [functions](apps/functions) on House's servers; what it writes reaches the App at once.
14
+ - A Trigger may also start a [Routine](reference/routine) on an Agent of your User.
@@ -0,0 +1,67 @@
1
+ # Example: assign a lead
2
+
3
+ How a function hands each new lead to the manager on shift with the fewest leads since their shift began, records its decision and calls the CRM.
4
+
5
+ A CRM posts each new lead to the Room's push address (`create_push_address`) as `{"event": "lead.created", "card": {"id": 101}}`. The Room holds three types: `staff/<key>.md` with `manager_id`, the manager's id at the CRM; `shifts/<key>.md` with `staff`, the key, and `starts` and `ends` as ISO instants; and `decisions/<card>.md` with `card`, `staff` and `decided_at`. The App's `external_origins` lists the CRM's origin, the Room holds a Service key for it, and a Trigger runs the function for each lead:
6
+
7
+ ```json
8
+ {
9
+ "on": "items",
10
+ "address": "<push address>",
11
+ "match": { "path": "/json/event", "eq": "lead.created" }
12
+ }
13
+ ```
14
+
15
+ ```js
16
+ export default async (event, house) => {
17
+ const lead = JSON.parse(event.item.body);
18
+ const now = event.moment;
19
+ const card = lead.card.id;
20
+ const read = async (query) => house.query({ select: { fields: 'all' }, ...query });
21
+ const [held] = (await read({ types: ['decision'], paths: [`decisions/${card}.md`] })).documents;
22
+ if (held !== undefined) return;
23
+ const { documents: shifts } = await read({
24
+ types: ['shift'],
25
+ where: { '/starts': { $lte: now }, '/ends': { $gt: now } },
26
+ });
27
+ if (shifts.length === 0) throw new Error('no manager is on shift');
28
+ const leads = async (shift) =>
29
+ (
30
+ await read({
31
+ types: ['decision'],
32
+ where: { '/staff': shift.fields.staff, '/decided_at': { $gte: shift.fields.starts } },
33
+ limit: 1,
34
+ total: true,
35
+ })
36
+ ).total;
37
+ const counted = await Promise.all(
38
+ shifts.map(async (shift) => ({ shift, leads: await leads(shift) })),
39
+ );
40
+ const [{ shift: chosen }] = counted.sort((left, right) => left.leads - right.leads);
41
+ const [manager] = (await read({ types: ['staff'], paths: [`staff/${chosen.fields.staff}.md`] }))
42
+ .documents;
43
+ const answer = await house.callExternal({
44
+ url: `https://crm.example/cards/${card}`,
45
+ method: 'PUT',
46
+ headers: { 'content-type': 'application/json', 'idempotency-key': event.id },
47
+ body: JSON.stringify({ status: 'assigned', manager: manager.fields.manager_id }),
48
+ room_ref: event.room,
49
+ });
50
+ if (answer.status !== 200) throw new Error(`the CRM answered ${answer.status}: ${answer.body}`);
51
+ const saved = await house.saveParts(event.room, [
52
+ {
53
+ path: `decisions/${card}.md`,
54
+ parts: {
55
+ creation: {
56
+ type: 'decision',
57
+ fields: { card, staff: chosen.fields.staff, decided_at: now },
58
+ sections: {},
59
+ },
60
+ },
61
+ },
62
+ ]);
63
+ if (saved.outcome !== 'saved') throw new Error(JSON.stringify(saved));
64
+ };
65
+ ```
66
+
67
+ A lead the CRM delivers again finds its decision and calls nothing. Two leads that arrive together run one after another, so the second counts the first's decision and goes to another manager when two are tied. Each count is a query's `total`, so a long shift is never cut at a page. A failed call fails the run with the CRM's answer and tells the Room's owner; the next lead still runs.
@@ -42,9 +42,7 @@ function titleForm(
42
42
  });
43
43
  const keep = () =>
44
44
  queued(async () => {
45
- unsaved.add(title);
46
45
  const written = await write(room, path(), held, parts(title.value, held));
47
- unsaved.delete(title);
48
46
  held = written.draft;
49
47
  if (written.stale) title.value = String(titleOf(held) ?? shown);
50
48
  return written.stale;
@@ -23,43 +23,11 @@ task:
23
23
  ```ts
24
24
  import { houseBridge } from '@agentshouse/app-bridge';
25
25
 
26
- type Task = { top: number; field: string | null; start: number | null; end: number | null };
27
-
28
26
  const bridge = houseBridge();
29
27
  let shown!: () => void;
30
28
  const showing = new Promise<void>((resolve) => (shown = resolve));
31
29
  let writing: Promise<unknown> = Promise.resolve();
32
- const unsaved = new Set<HTMLInputElement>();
33
- bridge.cooperate({
34
- checkpoint: () => showing,
35
- async persist() {
36
- await writing;
37
- if ([...unsaved].some((title) => title.isConnected)) throw new Error('unsaved');
38
- },
39
- capture(): Task {
40
- const at = document.activeElement;
41
- const field = at instanceof HTMLInputElement && at.selectionStart !== null ? at : null;
42
- return {
43
- top: scrollY,
44
- field: field?.ariaLabel ?? null,
45
- start: field?.selectionStart ?? null,
46
- end: field?.selectionEnd ?? null,
47
- };
48
- },
49
- async restore(task) {
50
- await showing;
51
- const held = task as Task | null;
52
- if (held === null) return;
53
- const field = Array.from(document.querySelectorAll('input')).find(
54
- (input) => held.field !== null && input.ariaLabel === held.field,
55
- );
56
- field?.focus({ preventScroll: true });
57
- field?.setSelectionRange(held.start, held.end);
58
- scrollTo(0, held.top);
59
- },
60
- ready: () => true,
61
- settle() {},
62
- });
30
+ bridge.cooperate({ placed: () => showing });
63
31
  const runtime = await bridge.runtime;
64
32
  const rooms = await runtime.observe('rooms');
65
33
  const sync = await runtime.observe('sync-status');
@@ -0,0 +1,13 @@
1
+ # App functions
2
+
3
+ How an App declares a function that House runs on its servers when a Trigger fires, with no page open and no model.
4
+
5
+ - **Declare.** `app.json` lists `functions`, each `{"name": "assign lead", "entry": "functions/assign.js"}` ([App manifest](reference/app-manifest)). The entry is one ES module whose default export is the function and which imports nothing, so your build bundles each function, with all it imports, into `dist/`, such as `esbuild src/assign.ts --bundle --format=esm --outfile=dist/functions/assign.js`. A publish is refused whole for an entry that is missing, imports, has no default export or is over <function-size>, for a name used twice, and for more than <release-functions> functions.
6
+ - **Run.** A Trigger's `function` action names the App and one of its functions ([Triggers](reference/triggers)). House runs the App's latest release when the Event's turn comes, one Event of a Trigger after another, so a run reads what the run before it wrote. Runs share a fixed number of slots on House's servers, and a run waits, its action `waiting`, until one is free.
7
+ - **The event.** `export default async (event, house) => {}` receives `kind`, the condition's `on`, with `room`, `trigger`, `id`, `moment` and what matched: `path` and `reference`, a document change's `transition`, an arriving item's `item` with its `headers` and raw `body` text, a dated document's `document`, a delivered notification's `notification`, the facts of its web document, or a message's `message`, its `sender` and its request's `routine`, never its words, with the document that request was started for as `path` and `reference`. `Date` reads `moment`. The return value is ignored, so write what you keep into the Room.
8
+ - **The App API.** `house` is the page's App API over the Trigger's Room alone: `query(q)` ([Local Query](apps/local-query)), which reads only the documents of the `types`, folders (`folder`, `pathPrefix`) and `paths` it names and answers over them alone, so the rest of a large Room is never read: name the `types` or `pathPrefix` of its `hop` and `backlinks` too, and select its parts, since a query or `hop` or `backlinks` naming none, or selecting `backlinks` or `'all'`, is refused `invalid-query`, `saveParts(room, parts)`, `call(name, input)` for the Room acts, each by its House act name, such as `records/read` and `rooms/members`, and `callExternal(request)`. `query` and `callExternal` reject a refusal with its `code` for the function to catch, while `call` answers it as a result whose `kind` is `rejected` and `saveParts` as `outcome: 'refused'`, as on the page, so check what they answer. Its writes are the App's, by `App <App> (function <function>)`, fire Triggers, its own included, and count against the Room owner's Write limit.
9
+ - **Reach.** A run holds no `fetch`, timer, file, `console` or `Intl`: keep times as ISO instants, or convert a local time by its UTC offset yourself. `callExternal` takes the request `bridge.callExternal` takes and is the one way out, to an origin the release's `external_origins` lists; naming its Room, `room_ref: event.room`, carries that Room's Service key, which the function never sees, or is refused `service_key_not_found` naming the `origin`. Its calls count against the Room owner's `external_calls` limit.
10
+ - **Never repeated.** A run that started is never run again, even when it failed after a call reached the service. Send `event.id` as the service's idempotency key where it takes one, and name what you write by what the event carries, so a second delivery of the same thing finds it.
11
+ - **Failure and limits.** A throw settles the run `failed` with `function_failed` and its text, which `list_trigger_events` shows and House tells the Room's owner; the next Event still runs. A run ends `failed` at <function-time>, at <function-memory> or past <function-calls> external calls: `time_cap_reached`, `memory_cap_reached` or `external_call_cap_reached`.
12
+
13
+ Next: [Example: assign a lead](apps/example-function)
@@ -2,15 +2,15 @@
2
2
 
3
3
  How an App reads and writes its Rooms on the device, offline included.
4
4
 
5
- - 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.
6
- - 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.
5
+ - 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.
6
+ - 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. `rooms` answers once the runtime knows the Rooms the page reaches. Offer binding a Room when it is empty while the sync status names House reachable (`providerUnreachable` false) and `observe('connection-refusal')` answers `null`; an empty list at any other moment says only that nothing can be listed yet.
7
7
  - 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.
8
8
  - 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.
9
9
  - 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.
10
10
  - 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.
11
11
  - 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.
12
12
  - Offline, all of this works and saves wait. `bridge.onStatus` reports `offline`; say so quietly.
13
- - 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.
14
- - 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.
13
+ - A Room your User holds at the Open level 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.
14
+ - An outside service is reached only at the origins `app.json` lists in `external_origins`, directly or through `bridge.callExternal`, which House's server sends. A call that names a Room bound to the App in a `write` slot with `room_ref` carries that Room's Service key for the origin, which the page never sees; any other call carries no key. A call through a Room that holds no key for the origin is refused `service_key_not_found` naming the `origin`: tell the person that the Room's owner or a manager enters the key on the Room's Service keys, in its Info, since no App opens that page.
15
15
  - 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.
16
16
  - 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 [The App runtime](apps/runtime).
@@ -3,7 +3,7 @@
3
3
  What an App's page draws and how it behaves in the browser.
4
4
 
5
5
  - The App draws everything; House draws nothing around it. Set the tab's title with `bridge.setTab({ title })`.
6
- - 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; a push to the address already shown replaces it, so a double click adds no history entry. After an act that ends what the page shows, and when a dialog closes, `bridge.replace` the next address; a replace with the address this page was opened from goes back to it, so Back returns to where the person came from. Keep the open item, tab, filter and search in the address. Addresses under `/render`, `/info` and `/tab-icon` are House's.
6
+ - 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; a push to the address already shown replaces it, so a double click adds no history entry. After an act that ends what the page shows, and when a dialog closes, `bridge.replace` the next address; a replace with the address this page was opened from goes back to it, so Back returns to where the person came from. Keep the open item, tab, filter and search in the address. Addresses under `/render` and `/info` are House's.
7
7
  - Files: take one in with `binaryFile.attach` the moment it is chosen and keep the Path it answers, hand one out with `binaryFile.download`, and show an image or video in `<room-media>` naming its width and height, so nothing waits for media and nothing moves when it arrives; never read a file whole to show or download it ([files](apps/runtime-files)).
8
8
  - Mobile first: lay out from 360 pixels wide up, with no action behind hover alone.
9
9
  - Every control works from the keyboard, with visible focus and a label.
@@ -11,3 +11,4 @@ What an App's page draws and how it behaves in the browser.
11
11
  - Long names truncate with an ellipsis and show in full on hover.
12
12
  - The element is the link: a row, card or name opens its item; no "Open" buttons.
13
13
  - Offer installation yourself: `bridge.install()` from a click shows the browser's prompt, or answers `manual` where you show how.
14
+ - Offer a way out where your User expects one: `bridge.ceremony('/sign-out')` signs them out of House and opens sign-in.
@@ -42,4 +42,4 @@ read-only instead of retrying. `observe('readiness')` tells whether code and bou
42
42
  locally (`ready`), each Room's state, each refused Room's file and rule, the persistence answer, `space`
43
43
  (`full` once the device's storage refused a write, Save and messages refusing as `durable-write` until
44
44
  `retry()` or a later test write finds space, or once the origin refused a newer release), and the
45
- `installation`'s Rooms, parts, bytes, `received`, `step` and `paused`.
45
+ `installation`'s Rooms, parts, bytes, `step` and `paused`.
@@ -6,7 +6,8 @@ App code reaches the MD Runtime through the remote client. The surfaces are Room
6
6
  Search, Observe, Types, Room Draft, Save, History, Resolve, binary files, Call,
7
7
  Reached, Log, the reported persistence answer, reauthorization, conversation, the chat
8
8
  draft, and Presentation State. Sync Status, readiness, the Service Mode, the storage-loss
9
- statement, the deployment facts and the signed-in member are observations only.
9
+ statement, the deployment facts, the signed-in member and the count of authority changes
10
+ are observations only.
10
11
 
11
12
  The host attaches each port with its reach, and every member and observation answers
12
13
  within it: a Room outside it answers as one the replica does not hold, Rooms lists only
@@ -20,7 +21,8 @@ unsubscribe, and `close()` ends it. Closing the port ends every handle and rejec
20
21
  pending call and observation; a reconnected client observes again.
21
22
 
22
23
  `observe('agents')` answers every Agent a new conversation can go to, each `{ identity,
23
- name }`, with a conversation yet or not. Each conversation the Provider lists is held
24
+ name }`, a retired one left out: online every Agent of the User, with a conversation yet or
25
+ not, offline every Agent a conversation names. Each conversation the Provider lists is held
24
26
  complete in the browser. `conversation.list()` and `observe('conversations')` answer each
25
27
  with its Agent's identity and name, its name, created and finished times, last outcome,
26
28
  live `state` (`asking` while a question waits, `working` while a turn runs, else `idle`)
@@ -31,8 +31,9 @@ or remove one as a Document, its bytes following.
31
31
  one, before any byte; asks for the file only as it nears the view; draws a file the
32
32
  browser keeps from its bytes, offline too, and reads and keeps an image; plays a video
33
33
  or audio file from the Provider's address loading only its metadata; asks again when a
34
- shown file's address lapses; asks again while the Room does not hold the file yet; and
35
- shows its children where the file cannot be read.
34
+ shown file's address lapses; asks again while the file cannot be shown yet, such as
35
+ while the Room does not hold it or House did not answer, until House refuses it for
36
+ good; and shows its children where the file cannot be read.
36
37
  - `binaryFile.read(room, path, revision?)` answers `{ kind: 'binary', stream, byteLength,
37
38
  mediaType }`, the bytes a `ReadableStream` of parts of at most a mebibyte, which the
38
39
  browser keeps for later reads, offline too.
@@ -43,7 +44,7 @@ browser does not keep, and Call go to the Provider through the Adapter.
43
44
  Call takes the Action names the Adapter declares, any other answering `undeclared`. A Call
44
45
  result whose effect the Provider places in a Room carries `at`, that Room and Room
45
46
  Version: `reached(room, roomVersion)` answers whether the Room's confirmed state has
46
- reached it, refreshing the Room until it has, and `reached(room, roomVersion, path)`
47
+ reached it, which the Room's next changes bring, and `reached(room, roomVersion, path)`
47
48
  whether the held copy of that document was written later. A History revision names its
48
49
  author, `{ kind: 'user', id }` or `{ kind: 'inbox', id, name }`; History of a Path no
49
50
  document holds answers the removed document's revisions, newest first, with `deleted:
@@ -17,3 +17,7 @@ learns it: the User the Provider signed in, `{ kind: 'user', id, name }` like a
17
17
  until the Provider first names one, and where it names none. It is kept
18
18
  with the Local Replicas, answers offline once known, and every port hears the next member
19
19
  named; signing out answers `null`, then ends the observation with the authorization.
20
+ `observe('authority-changes')` answers how many times House told the runtime that what its
21
+ User may see changed, a Room's members, a Share or an offer among them, and hears each next
22
+ one, never a reconnection, so a view that read such a fact through Call reads it again when
23
+ the count moves.
@@ -2,9 +2,11 @@
2
2
 
3
3
  How to get an App's repository, build it and publish every change.
4
4
 
5
- - 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`.
5
+ - 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 user.name=<agent-git-name> -c user.email=<agent-git-email> <address>`, so Git commits as you in every later step too. Run `npm ci --ignore-scripts`, then publish with `npm run publish`, never with `--name`.
6
+ - Git signs in as you with a credential your User makes for you on `<origin>/connections`, given as its password.
7
+ - When the Kit runs you, add `-c 'credential.helper=!kit git-credential'` to the clone instead, so the Kit signs Git in.
6
8
  - 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`.
7
9
  - 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>.
8
10
  - 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.
9
11
  - 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`.
10
- - 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](apps/example-tasks) 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.
12
+ - A page whose first draw is not yet the place it opened on, as one that draws its Room's documents once `observe` answers, registers `bridge.cooperate({ placed })` in its entry script before its first draw, where `placed` resolves once that place shows what it holds, as [the example](apps/example-tasks) does: House keeps its preparation surface over the page until then, and shows a page that registers none at its first draw. An open page keeps the release it loaded and runs the next one at its next load, so it needs no update code; `node_modules/@agentshouse/app-bridge/docs/families.md` describes each bridge member.
@@ -6,7 +6,6 @@ Before `npm run publish`:
6
6
 
7
7
  - `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.
8
8
  - Every slot type is in `schema.yaml` exactly as in the bound Room's `settings/schema.yaml`.
9
- - The entry script registers `bridge.cooperate` before its first draw.
10
9
  - The page works 360 pixels wide, offline, and reloaded at a deep address.
11
10
 
12
11
  After it:
@@ -14,4 +13,4 @@ After it:
14
13
  - `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.
15
14
  - Bind the Room once with `set_app_bindings`.
16
15
  - See it yourself: sign your own browser in with `create_sign_in_code`, then open the App's url.
17
- - Give your User its address. An open page that registers `bridge.cooperate` takes each release by itself; nothing asks them to update.
16
+ - Give your User its address. An open page keeps the release it loaded and takes the next one at its next load, offline too; nothing asks them to update.
package/guide/index.md CHANGED
@@ -6,7 +6,7 @@ House keeps Markdown files in Rooms, which your User shares with other people an
6
6
 
7
7
  While your User's My Room, which `list_rooms` marks, holds no `wayfinding-map`, open [House Onboarding](skills/house-onboarding) first.
8
8
 
9
- - `apps/`, building an App over your User's Rooms: [Build an App](apps/build).
10
- - `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).
11
- - `routines/`, the Routines House writes into every Private Room: [Work on ticket](routines/work-on-ticket).
9
+ - `apps/`, building an App over your User's Rooms: [Build an App](apps/build), and the functions House runs for it when a Trigger fires: [App functions](apps/functions).
10
+ - `reference/`, the formats House reads: [Access level](reference/access-level), [App manifest](reference/app-manifest), [Schema](reference/schema) and [Triggers](reference/triggers).
11
+ - `routines/`, the Routine the panel writes for My Room for each Agent it makes, naming that Agent, written as [Routine](reference/routine) says: [Work on ticket](routines/work-on-ticket).
12
12
  - `skills/`, House's Skills: [Prototype an App](skills/prototype-app).
@@ -23,9 +23,11 @@ The skills are already set up; never run `setup-matt-pocock-skills`.
23
23
  - Rejected requests (triage): `out-of-scope/<concept>.md`
24
24
  - Implementation notes (implement-spec): `projects/<name>/notes/`
25
25
  - PR bodies (pr): `projects/<name>/prs/<branch>.md`
26
- - Wayfinding map: one `wayfinding-map` document, `projects/<name>/map.md`,
27
- and its tickets, `wayfinding-ticket` documents in order as
28
- `projects/<name>/map/NN-<slug>.md`: a ticket carries its `title`, its
26
+ - Wayfinding map: one `wayfinding-map` document, `projects/<name>/map.md`
27
+ with `type: wayfinding-map` in its frontmatter, and its tickets,
28
+ `wayfinding-ticket` documents in order as `projects/<name>/map/NN-<slug>.md`
29
+ with `type: wayfinding-ticket`, since House knows a map or a ticket only by
30
+ its `type`: a ticket carries its `title`, its
29
31
  `map` (the map's path), its `kind` (its type) and `blocked_by` (its
30
32
  blockers); a claim sets `status: in-progress` and the
31
33
  `assignee`, and a resolution writes the Answer and sets `done`, or
@@ -0,0 +1,9 @@
1
+ # Access level
2
+
3
+ How much of a Room a grant or a Share lets its holder do with its text, and where.
4
+
5
+ Access level, `level` on a Room grant or a Share’s Room item, each level including the one before it: `search`, the Search level, reaches the Room through `search`, `shell` and `inspect` alone, each reply at most <mcp-read-size> of the Room’s text, with no copy anywhere; `open`, the Open level, also opens the Room in browser Apps while the browser is online, and acts as `search` everywhere else; `keep`, the Keep level, also copies the Room to the devices that keep it, offline included, and is the one `export` needs. Apps need `open` or `keep`.
6
+
7
+ ```json
8
+ { "room_ref": "r_ab12.1", "level": "open", "operations": ["write"] }
9
+ ```
@@ -2,7 +2,7 @@
2
2
 
3
3
  What an App's `app.json` declares.
4
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`; `house_model`, the House model types it shows, which it reads from your User’s Private Room under `house/` without binding that Room; `house_acts`, the House acts it calls by their act names, such as `rooms/rename`, never one only a person takes or one that changes what an App reaches; and `theme_colour`, `#rrggbb`. A User who does not own the App is asked once to allow the House model types and House acts it declares, and again only for those a later release adds; a declined one reads nothing and is refused. [Build an App](apps/build) shows how its page works.
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; `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`; `house_acts`, the House acts it calls by their act names, such as `rooms/read` for your User’s Rooms or `rooms/rename`, never one only a person takes or one that changes what an App reaches, except that a Self-hosted Building's App may declare its administrators' acts, `building/people/read`, `building/people/add`, `building/people/remove`, `building/administrators/name`, `building/administrators/take`, `building/factor/require`, `building/factor/stop-requiring` and `building/factor/reset`, which its page calls in an administrator's browser once it has asked them to confirm each, `building/people/read` refusing anyone else `not_building_administrator`; `functions`, each a `name` and the `entry` file a Trigger runs on House's servers ([App functions](apps/functions)); and `theme_colour`, `#rrggbb`. A User who does not own the App is asked once to allow the House acts it declares, and again only for those a later release adds; a declined one is refused. [Build an App](apps/build) shows how its page works.
6
6
 
7
7
  ```json
8
8
  {
@@ -1,13 +1,16 @@
1
1
  # Routine
2
2
 
3
- How a Routine document in the Private Room is written.
3
+ How a Routine document in the Private Room is written, one for each Room it serves.
4
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.
5
+ Routine, `/rooms/private/rooms/<room-handle>/routines/<name>.md`, serving the Room `<room-handle>` names: `description`; `input`, a text saying what to send, empty if nothing; and `agent`, the identifier of the one of your User's current Agents it starts. The body is the instruction, which never leaves the Private Room. Writing it needs your User's write in that Room. House writes its card, the description, input and Agent, at `/rooms/<room-handle>/house/routines/<name>.md`, where every member reads it, and refuses a name a card there already holds as `routine_name_taken`. A Routine a second Room needs is a copy under that Room's handle. Anyone who writes that Room asks it with `send_routine_request` naming the card, or names the `agent` with their own words, as the Agent's Trust level in that Room allows: under Any request, the default, either starts at once; under Routines only the card starts at once and own words are refused `own_text_not_admitted`; under Approve each either waits for the Agent's owner to approve it. Its owner sets the level on the Agent's House page; the owner, their own agents and their Private Room's Triggers start it at once at every level, a Trigger of any other Room asks as that Room's writers do, and `inspect` on a Room root names the Agents you may ask there with their levels. A Routine Request's input is text the Agent reads, and House checks no shape. Mail your conversation sends with `send_mail` and a request it sent reaches the conversation that request started at once under Any request; below it, `send_mail` answers `waiting: true` and the mail waits for that Agent's owner, and a declined one comes back to you as House's message saying so. That conversation's mail always comes back to yours. A request's run is the first turn of the conversation it starts: during that turn the Agent names the document it wrote in the request's Room as the request's result with `set_request_result`, and the turn's end finishes the request. The conversation stays active until it is Done, which `mark_done` sets at the end of a turn and which nothing undoes, so mail reaches it after its run.
6
+
7
+ A Trigger's `message` action puts its `text`, a link to what matched and its files into the active conversation, one not Done, its Routine started from that Room for what the Event is about: the chat of a customer's `inbox-message`, the matched document or item, or the Trigger for a `cron` clock. Its Event shows `continued` with that `request`; with none, it starts the Routine as a `routine` action does, `requested`. A conversation waiting for approval refuses it, `refused` with `conversation_waiting`. The Agent reads a customer's message as a customer's words, one from a Trigger of its owner's Private Room as the owner's instruction, and any other as what matched, never an instruction. A `routine` action starts a new request on every Event. A `done` action marks Done every active conversation a request of its Trigger's Room started for the Event's document, whoever owns its Agent, but one waiting for approval, which stays for its owner to approve or decline; its Event shows `marked` with their number, `conversations`, 0 when there are none, as on a `cron` clock, whose Event names no document.
6
8
 
7
9
  ```markdown
8
10
  ---
9
11
  description: Summarize a thread in five bullets.
10
12
  input: The thread to summarize, as a ref or pasted text.
13
+ agent: 019a0000-0000-7000-8000-000000000001
11
14
  ---
12
15
 
13
16
  Read the thread and write five bullets under summaries/.
@@ -1,9 +1,15 @@
1
1
  # Triggers
2
2
 
3
- How a Trigger's `condition` and `actions` are written for `create_trigger` and `edit_trigger`.
3
+ How a Trigger's `condition` and `actions` are written for `create_trigger` and `edit_trigger`; House writes a Room's Triggers as read-only `house-trigger` documents at `house/triggers/<trigger>.md`.
4
4
 
5
- A Room's Triggers are its read-only `house-trigger` documents, one at `house/triggers/<trigger>.md` for each, which House writes as the Trigger's acts left it.
5
+ `condition`: When it fires, e.g. `{on: "documents", scope: {type: "task"}, mode: "starts_matching", new_documents: false, match: {path: "/fields/status", eq: "review"}}`; for a new document in a folder, `{on: "documents", scope: {folder: "/answers"}, mode: "starts_matching", new_documents: true, match: {all: []}}`; for any edit of one document, `{on: "documents", scope: {document: "/plan.md"}, mode: "field_changes", field: "/text"}`; `moved_between_rooms: true` fires only on a document moved from or to another Room, `false` on any other change; for items, `{on: "items", address: "<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>}`. In your Private Room alone, a notification to you: `{on: "notifications", match: {path: "/fields/kind", eq: "share-offer"}}`, testing its `house-notification` document's `/fields/kind` and its value fields, `match` optional; such a Trigger holds no `notify` action. A `/fields/…` or `/members/…` path reads only what the document's own type declares: a document with no `type` has no fields, whatever its front matter holds, so give it a `type` naming one in `settings/schema.yaml` that declares the field.
6
6
 
7
- `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>}`.
7
+ A message into a conversation a [Routine Request](reference/routine) of the Room started: `{on: "messages", match: {path: "/sender", eq: "requester"}}`, testing `/sender`, `/routine` and `/path`, the request's document, never its words.
8
8
 
9
- `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.
9
+ `actions`: a `notify` action names whom it reaches, e.g. `{kind: "notify", title: "Ready", 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 `function` action names an App and one function its latest release declares, e.g. `{kind: "function", app: "a_…", function: "assign lead"}`, which you may add only for an App you hold that has this Room in a write slot of your bindings; [App functions](apps/functions) says how House runs it. `list_trigger_events` shows each run `ran` with its `release`, or `failed` with `function_failed` and its `error`, never run again, or `function_not_started` while House tries to start it, `final` after its last try.
10
+
11
+ In your Private Room alone, a `function` action naming no `app` runs one of your Environment functions ([wizard](skills/wizard)), e.g. `{kind: "function", function: "nightly backup"}`; elsewhere, or for a name you do not hold, it is refused `environment_function_not_found`. Kit runs its script with no terminal, the Event as JSON in the file `HOUSE_INPUT` names, one Event after another. `list_trigger_events` shows each run `ran`, or `failed` with `function_failed` and its `error`, never run again, with its `run`, `exit_code` and `output`; a Kit not connected at its turn reads `no_target` with `environment_offline`, never run later.
12
+
13
+ A Trigger acts on at most <trigger-events-per-hour> Events in each UTC hour, across its versions, skipped and failed ones not counted; `list_trigger_events` shows the first past the number `missed: "skipped"` with `code: "cap_reached"`, which starts nothing, and none later that hour; its creator is told once per version. A Trigger whose own actions make what it fires on stops there, so narrow its condition.
14
+
15
+ `{kind: "routine", card: "<name>", payload: "<input>"}` starts a [Routine](reference/routine) and `{kind: "message", card: "<name>", text: "<text>"}` messages it; no such card is refused `routine_not_found`, a retired one `routine_retired`. `{kind: "done"}` marks Done each active conversation any Routine Request of the Room started for the Event's document, `marked` with their number.
@@ -9,7 +9,7 @@ The Routine an Agent runs when a person sets a Wayfinding ticket in progress wit
9
9
 
10
10
  Work the one ticket this request names, then stop:
11
11
 
12
- 1. Open the `wayfinding-ticket` the request names and set its `conversation` to this conversation.
12
+ 1. Open the `wayfinding-ticket` the request names and set its `conversation` to the id of this conversation, which House's first message names.
13
13
  2. Work it by its `kind`: for `research`, find out and say where you found it; for `prototype`, build the smallest thing that answers its Question; for `grilling`, ask your User until its Question is settled; for `task`, do what it asks.
14
14
  3. Write what you found or made in its Answer section and set its `status` to `done`.
15
15
  4. Start no other ticket, not even one this ticket unblocks.
@@ -10,7 +10,7 @@ Build it as [Build an App](apps/build) says, with what follows. Anything private
10
10
  - 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.
11
11
  - Everything it shows lives as documents in its Room; put its demo data there, never in code.
12
12
  - A visitor's edits stay in their browser and never reach House: write them with `save` as an App does.
13
- - To receive messages from visitors, name an Intake Room with `set_site_intake_room` and send each with `runtime.call('submit', { content })`, online only.
13
+ - To receive messages from visitors, name an Intake Room with `set_site_intake_room` and send each with `runtime.call('site/submission', { content })`, online only. `content` is one Markdown document the Intake Room's `settings/schema.yaml` accepts, and House adds its `site` and `received` fields. The call answers `{ kind: 'action', result: { submitted: true } }` once House stored it. Anything else stored nothing: `{ kind: 'rejected', code }` says why, such as `bot_check_failed` for a browser that failed the bot check, as automated test browsers do, `site_intake_room_absent`, `invalid_documents` with the `path`, `rule` and `field` it broke, `submission_reserved_key`, `submission_too_large`, `limit_reached` or `cap_reached`. So thank the visitor only for `submitted`, and otherwise show that nothing was sent.
14
14
  - `describe_site` sets the title and description search engines show.
15
15
  - With `google_analytics_id` set by `describe_site`, House measures a visitor only after they accept. While `bridge.onStatus` reports `analytics.measures`, ask them on the Site unless `analytics.choice` holds their answer, report it with `bridge.analyticsConsent('accept')` or `bridge.analyticsConsent('decline')`, and keep a way on every page to decline later, which reloads the page unmeasured.
16
16
  - See it yourself: open its url, which needs no sign-in.
@@ -16,7 +16,7 @@ Run `wayfinder` to chart the map; this page adds only what is House's. `wayfinde
16
16
 
17
17
  ## Keep the plan
18
18
 
19
- - Keep the map as one `wayfinding-map` and its tickets as `wayfinding-ticket` documents in My Room, which `list_rooms` marks with `my_room: true`. Chart the map even when the way looks clear: My Room's map is how House knows onboarding ran.
19
+ - Keep the map as one `wayfinding-map` and its tickets as `wayfinding-ticket` documents in My Room, which `list_rooms` marks with `my_room: true`, each declaring `type: wayfinding-map` or `type: wayfinding-ticket` in its frontmatter: a field named for the type makes no map. Chart the map even when the way looks clear: My Room's map is how House knows onboarding ran.
20
20
  - Keep decisions about the account, billing, runtime or credentials in the Private Room, `/rooms/private`.
21
21
  - Make the App for your User's goal a ticket.
22
22
 
@@ -11,6 +11,12 @@ Write one bash script for the steps only the User can do: it says what to open a
11
11
 
12
12
  Create it with `create_web_wizard`, for the Environment you work through or another of the User's that they name (`list_environments`). Give the User the link it answers: they read the whole script there, and nothing runs until they press Run. Its output and what they type never reach you. When they say they are done, read the document at the path it answered, which says how it ended, and go on only once its state is finished with exit code 0.
13
13
 
14
+ ## As a function
15
+
16
+ For a script the User may want to keep, propose a name when you create the Web Wizard. Only the User keeps a script: they tick Keep as a function on its page, may change the name, and press Keep, which runs nothing and makes the script one of their Environment functions. The document then reads kept with the function's name; keeping under a name they already use replaces that function's script. `inspect` on the Environment lists its functions, and `delete_function` deletes one. On a function's page the User can also press Run now, which runs it in a terminal they type into, so a kept script may ask them for a password there; nothing of that run reaches you.
17
+
18
+ `run_function` runs a function on its Environment, whose Kit must be connected, and answers when the run ends. Your input reaches the script as JSON in the file `HOUSE_INPUT` names. The run has no terminal, so a script that asks for typing fails at once; such a step stays a Web Wizard. The answer's ending is exited with its exit_code, timed_out at the run's time limit, not_started with its cause, or interrupted when House lost the run; output is the end of what the script printed, cut when its start was dropped.
19
+
14
20
  ## As a script they run
15
21
 
16
22
  When House refuses the Web Wizard because the Environment's Kit is not connected, because you reach no Environment of the User, or because the Environment you act through does not reach the User's Private Room, write the script into the Room where the Owner rules, `/rooms/private/owner-rules.md`, keep wizards, and give the User its link and the command that runs it.
@@ -5,5 +5,6 @@
5
5
  "ac814a2197038f037b657bb719340395f89f319ac9fcdd386629c3fe2cec68f1",
6
6
  "29951ee394aad9979eecfed3e5dc32c2c86e2226b6a380db5dbd4f305f4d1156",
7
7
  "b99799a2fcddcb99e6ac14acc8d7a92a9f8f69b53f6b41edbc6f8e978256c71a",
8
- "d14166e300ce35455ca51ec4959c2bc77668e5cb7f890d2b89c4d2c1e02af6b5"
8
+ "d14166e300ce35455ca51ec4959c2bc77668e5cb7f890d2b89c4d2c1e02af6b5",
9
+ "e612bbaa90e41d60b9bcd0367a074b9735b9239c287e56fa1becc8364562a422"
9
10
  ]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentshouse/skills",
3
- "version": "0.1.0-cfb6385975145",
3
+ "version": "0.1.0-cfeaa26aaefdb",
4
4
  "description": "The upstream Skills agents.house serves its agents, verbatim, and the texts it writes for them: the Guide, its own Skills among the pages, and the Agent instructions",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/upstream.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "repository": "https://github.com/mattpocock/skills",
3
- "commit": "5beb35b040e3de97e54fa9e386d7e42d15c295c2",
4
- "date": "2026-10-19",
3
+ "commit": "59db609eb1e18aaf59b63284d0e52255aff0c63c",
4
+ "date": "2026-10-27",
5
5
  "skills": [
6
6
  "ask-matt",
7
7
  "code-review",
@@ -1,9 +0,0 @@
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
- ```