@north-light/cli 0.3.533 → 0.3.534

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.
@@ -29,7 +29,7 @@ export interface CronDTO {
29
29
  command: string;
30
30
  /** The grant the cron runs under (checked at every fire, call and birth); null only for the terminal on a runtime with no issuer. */
31
31
  grant_id: string | null;
32
- /** The scopes the cron is bounded to; null = the grant's whole current set (a person-armed terminal cron). */
32
+ /** The scopes the cron is bounded to; null = the grant's whole current set at each fire (a person-armed terminal cron, or one armed by a caller with no scope limit). */
33
33
  scopes: string[] | null;
34
34
  /** The run of the node that armed it, when a node did. */
35
35
  origin_run_id: string | null;
@@ -11,6 +11,11 @@ Changes to `@north-light/sdk` that can change what an existing app sees. Newest
11
11
 
12
12
  ## 0.3.533
13
13
 
14
+ A cron armed by a caller with no scope limit follows the grant, and a narrowed scope pauses it.
15
+
16
+ - `POST /v1/crons` stores and returns `scopes: null` for a cron armed by a node, cron or app token with no scope limit, where it used to return a copy of the grant's scopes. A cron with `scopes: null` runs under the grant's current scopes at every fire, including scopes the grant gains later. A cron armed with an explicit scope list keeps it and is refused if the grant stops covering any of it.
17
+ - A scheduled fire refused `scope_narrowed` now pauses the cron, recurring ones included (`state: paused` in `GET /v1/crons`), and notifies whoever armed it. Resume it with `POST /v1/crons/:id/resume` once the grant covers the scope again.
18
+
14
19
  Three reads take optional narrowing. Additive: no request or response an existing app sends or reads changes.
15
20
 
16
21
  - `GET /v1/canvas/roster` node rows carry an optional `description`. A runtime that predates the field omits it.
@@ -17,7 +17,7 @@ A provider is a service the person's runtime calls for your app. Your app asks t
17
17
  | `context` | Ask about the person from their memory, and look up public profiles | `context:<group>` | [[northlight-apps/providers/context]] |
18
18
  | `device` | The person's Mac: browser, files, Messages and shell | `device:<group>` | [[northlight-apps/providers/device]] |
19
19
 
20
- > **Want your app to have its own number and text its users?** The `sms` provider texts only the signed-in person's own phone. For your own number, use Comms: [the Comms guide for coding agents](https://comms.northlight.ai/llms.txt), and the SDK [`@north-light/comms`](https://www.npmjs.com/package/@north-light/comms) on npm.
20
+ > **Want your app to have its own number and text its users?** The `sms` provider texts only the signed-in person's own phone. For your own number, use Comms: [the Comms guide for coding agents](https://comms.northlight.ai/llms.txt), and the SDK [`@north-light/comms`](https://www.npmjs.com/package/@north-light/comms) on npm. Other apps that offer a service to developers are listed under [Services you can build on](https://sdk.docs.northlight.ai/docs/services).
21
21
 
22
22
  ## Where names come from
23
23
 
@@ -7,7 +7,7 @@ when-and-why-to-read: When you need Texting (sms), this knowledge should be read
7
7
 
8
8
  # Texting (sms)
9
9
 
10
- > **Want your app to have its own number and text its users?** The `sms` provider cannot: it texts only the signed-in person's own phone. Use Comms instead: [the Comms guide for coding agents](https://comms.northlight.ai/llms.txt) covers fake mode, testers and production, and the SDK is [`@north-light/comms`](https://www.npmjs.com/package/@north-light/comms) on npm.
10
+ > **Want your app to have its own number and text its users?** The `sms` provider cannot: it texts only the signed-in person's own phone. Use Comms instead: [the Comms guide for coding agents](https://comms.northlight.ai/llms.txt) covers fake mode, testers and production, and the SDK is [`@north-light/comms`](https://www.npmjs.com/package/@north-light/comms) on npm. See also [Services you can build on](https://sdk.docs.northlight.ai/docs/services).
11
11
 
12
12
  The `sms` provider sends a text from Northlight's number to the verified phone of the person who signed in to your app. It has no recipient argument, so an app cannot text anyone else.
13
13
 
@@ -1 +1 @@
1
- var r=Object.defineProperty;var e=(o,t)=>r(o,"name",{value:t,configurable:!0});import{defineBranch as a}from"../core/command.js";import{dashboardLeaf as n}from"./dashboard.js";import{canvasSnapshotLeaf as s}from"./canvas-snapshot.js";import{canvasRosterLeaf as i}from"./canvas-roster.js";import{browseLeaf as d}from"./canvas-browse.js";import{reviveLeaf as c}from"./revive.js";import{attentionBranch as h}from"./attention.js";import{chordLeaf as m}from"./chord.js";import{canvasPruneLeaf as p}from"./canvas-prune.js";import{historyBranch as u}from"./canvas-history.js";import{issueNewLeaf as f}from"./canvas-issue.js";import{canvasConfigBranch as l}from"./canvas-config.js";import{canvasUseLeaf as g}from"./canvas-use.js";import{readLeaf as w}from"./canvas/read.js";import{listLeaf as v}from"./canvas/list.js";import{searchLeaf as b}from"./canvas/search.js";import{watchLeaf as y}from"./canvas/watch.js";import{unwatchLeaf as L}from"./canvas/unwatch.js";import{edgesLeaf as k}from"./canvas/edges.js";import{analyticsLeaf as j}from"./canvas-analytics.js";const x=a({name:"issue",description:"capture a quick todo/idea/bug into a GitHub issue (or a .todos/ note)",whenToUse:"the alt+c \u2192 i chord uses this; capture a passing thought without leaving your pane",tier:"hidden",help:{name:"canvas issue",summary:"capture a quick todo/idea/bug into a GitHub issue",model:"Quick brain-dump capture, bound to the alt+c \u2192 i \u2192 (idea|bug) prefix chord. `new` reads a free-text note, auto-generates the title via a headless pi call, and files it with `gh issue create --label <type>` when the pane\u2019s cwd is a GitHub repo, else writes `<git-root>/.todos/<slug>.md`. Meant for the chord/popup, not hand-running."},children:[f]});function J(){return a({name:"canvas",rootEntry:{concept:"every object in the agent graph \u2014 documents, nodes, jobs and their owners \u2014 read, listed, searched and watched by name, plus the whole-graph views: render it, see who is blocked, mass-revive disconnected nodes",desc:"read, list, search and watch objects; whole-graph views and supervision",useWhen:"reading a document, node or job by name, finding one you cannot name, watching an object for changes, surveying the full graph, finding blocked agents, or mass-reviving every disconnected node; documents are written with `crtr doc`; run `crtr canvas -h`"},help:{name:"canvas",summary:"read, find and watch objects; observe and supervise the whole agent graph",model:"Every document, node, bash job and owner (person, app, repo, plugin, profile) is an object with an id and a name, and the same verbs work on all of them. `read` returns one object by name or id and makes you a watcher of it; `list` filters objects by type, kind, owner, privacy or name prefix; `search` finds them by text; `edges` shows what an object links to, who acted on it and who watches it; `watch`/`unwatch` add or remove your watch without reading. `list`, `search` and `edges` record nothing. Documents are written, edited and moved with `crtr doc`, and pointed at as `[[name]]`. Canvas-wide views, distinct from per-node work (`node`) and a node's own spine write verb (`push`): `dashboard` renders the subscription forest as a tree; `browse` opens an interactive full-screen navigator over the whole canvas and resumes the chosen node; `attention` aggregates pending replies from the user across the graph; `revive --all` mass-reconnects every disconnected node (bringing back ONE named node is `node lifecycle revive <id>` instead); `history` searches past node conversations; `prune` bounds growth by deleting terminal nodes past a TTL; `analytics` opens the read-only Analytics page (graph sizes, agent time, document reads, API-price estimate) for a person. `config` registers/lists remote canvas targets (preview endpoint + relay token); `use` switches remote-capable read commands such as `node inspect` onto a configured target or back to local."},children:[w,v,b,y,L,k,n,s,i,d,h,c,u,m,x,p,l,g,j]})}e(J,"registerCanvas");export{x as issueBranch,J as registerCanvas};
1
+ var r=Object.defineProperty;var e=(t,o)=>r(t,"name",{value:o,configurable:!0});import{defineBranch as a}from"../core/command.js";import{dashboardLeaf as n}from"./dashboard.js";import{canvasSnapshotLeaf as s}from"./canvas-snapshot.js";import{canvasRosterLeaf as i}from"./canvas-roster.js";import{browseLeaf as d}from"./canvas-browse.js";import{reviveLeaf as c}from"./revive.js";import{attentionBranch as h}from"./attention.js";import{chordLeaf as m}from"./chord.js";import{canvasPruneLeaf as p}from"./canvas-prune.js";import{historyBranch as u}from"./canvas-history.js";import{issueNewLeaf as f}from"./canvas-issue.js";import{canvasConfigBranch as l}from"./canvas-config.js";import{canvasUseLeaf as g}from"./canvas-use.js";import{readLeaf as w}from"./canvas/read.js";import{listLeaf as v}from"./canvas/list.js";import{searchLeaf as b}from"./canvas/search.js";import{watchLeaf as y}from"./canvas/watch.js";import{unwatchLeaf as L}from"./canvas/unwatch.js";import{edgesLeaf as k}from"./canvas/edges.js";import{analyticsLeaf as j}from"./canvas-analytics.js";const x=a({name:"issue",description:"capture a quick todo/idea/bug into a GitHub issue (or a .todos/ note)",whenToUse:"the alt+c \u2192 i chord uses this; capture a passing thought without leaving your pane",tier:"hidden",help:{name:"canvas issue",summary:"capture a quick todo/idea/bug into a GitHub issue",model:"Quick brain-dump capture, bound to the alt+c \u2192 i \u2192 (idea|bug) prefix chord. `new` reads a free-text note, auto-generates the title via a headless pi call, and files it with `gh issue create --label <type>` when the pane\u2019s cwd is a GitHub repo, else writes `<git-root>/.todos/<slug>.md`. Meant for the chord/popup, not hand-running."},children:[f]});function J(){return a({name:"canvas",rootEntry:{concept:"every object in the agent graph \u2014 documents, nodes, jobs and their owners \u2014 read, listed, searched and watched by name, plus the whole-graph views: `dashboard` prints the subscription tree once, `browse` is the interactive navigator, `attention` shows who is blocked, `revive --all` mass-revives disconnected nodes",desc:"read, list, search and watch objects; whole-graph views and supervision",useWhen:"reading a document, node or job by name, finding one you cannot name, watching an object for changes, surveying the full graph, finding blocked agents, or mass-reviving every disconnected node; documents are written with `crtr doc`; run `crtr canvas -h`"},help:{name:"canvas",summary:"read, find and watch objects; observe and supervise the whole agent graph",model:"Every document, node, bash job and owner (person, app, repo, plugin, profile) is an object with an id and a name, and the same verbs work on all of them. `read` returns one object by name or id and makes you a watcher of it; `list` filters objects by type, kind, owner, privacy or name prefix; `search` finds them by text; `edges` shows what an object links to, who acted on it and who watches it; `watch`/`unwatch` add or remove your watch without reading. `list`, `search` and `edges` record nothing. Documents are written, edited and moved with `crtr doc`, and pointed at as `[[name]]`. Canvas-wide views, distinct from per-node work (`node`) and a node's own spine write verb (`push`): `dashboard` renders the subscription forest as a tree; `browse` opens an interactive full-screen navigator over the whole canvas and resumes the chosen node; `attention` aggregates pending replies from the user across the graph; `revive --all` mass-reconnects every disconnected node (bringing back ONE named node is `node lifecycle revive <id>` instead); `history` searches past node conversations; `prune` bounds growth by deleting terminal nodes past a TTL; `analytics` opens the read-only Analytics page (graph sizes, agent time, document reads, API-price estimate) for a person. `config` registers/lists remote canvas targets (preview endpoint + relay token); `use` switches remote-capable read commands such as `node inspect` onto a configured target or back to local."},children:[w,v,b,y,L,k,n,s,i,d,h,c,u,m,x,p,l,g,j]})}e(J,"registerCanvas");export{x as issueBranch,J as registerCanvas};
@@ -1,10 +1,10 @@
1
- var oe=Object.defineProperty;var s=(e,n)=>oe(e,"name",{value:n,configurable:!0});import{randomUUID as se}from"node:crypto";import{CrtrError as O,notFound as v,usage as p}from"../errors.js";import{ExitCode as D}from"../../types.js";import{canChange as L,canRead as P,missingMemoryScope as ue,missingMemoryScopeUnder as ce,ownerChain as we}from"./access.js";import{bodyOf as pe,get as le,put as B}from"./bodies.js";import{dropUnresolved as me,linksOf as de,recordActedOn as N,setLinks as G,setUnresolved as fe,unresolvedLinksCouldName as ye}from"./edges.js";import{publish as k}from"./events.js";import{resolveName as he}from"./names.js";import{createObject as Ee,getById as m}from"./objects.js";import{liveLookups as _e,readerForNode as ve,readerForObject as Ne}from"./reader.js";import{replaceRules as K,setRulesOwner as Re}from"./rules.js";import{recordWatch as I}from"./watches.js";import{withCanvasWrite as y}from"../canvas/db.js";import{filterAsync as q,forEachAsync as ge,mapAsync as R}from"../async-array.js";function E(e){return e.class==="node"?e.node:e.class==="app"?e.app:e.class==="object"?e.object:e.person}s(E,"actorOf");function Se(e,n){return new O("conflict",e,D.GENERAL,n)}s(Se,"conflict");function h(e){if(e.type!=="document")throw p(`${e.type} ${e.id} is not a document`)}s(h,"assertDocument");async function Y(e,n){const t=await we(e,n);return(t.length?t[t.length-1]:n).type}s(Y,"spaceType");async function $(e,n,t){if(t!=="public"&&await Y(e,n)==="repo")throw p(`a repo owns only public documents; ${n.name} cannot own a ${t} one`,{rule:"repo-public-only",next:"Write it as public, or give it another owner (your own documents can carry `if: {repo: <handle>}`)."})}s($,"assertPublicInRepo");async function H(e,n,t,a){if(t.trim()===""||t!==t.trim())throw p(`invalid document name ${JSON.stringify(t)}`);const i=await e.prepare("SELECT id FROM objects WHERE (type = 'document' OR strpos(type, '.') > 0) AND owner = ? AND name = ? AND deleted_at IS NULL AND home_machine IS NULL").get(n.id,t);if(i&&i.id!==a?.id)throw Se(`${n.name} already has a document named ${t}`,{name:t,holder:i.id})}s(H,"assertNameFree");async function U(e,n,t){if(!await P(e,n,t))throw v(`no document ${t.id}`)}s(U,"readable");function V(e){return new O("scope_missing",`missing scope ${e}`,D.GENERAL,{scope:e})}s(V,"scopeMissing");async function x(e,n,t){await U(e,n,t);const a=await ue(e,n,t,"rw");if(a!==null)throw V(a);if(!await L(e,n,t))throw new O("forbidden",`you cannot change ${t.name}`,D.GENERAL,{id:t.id})}s(x,"changeable");async function X(e,n){const t=await e.prepare(`SELECT p.rev_id FROM revision_parents rp JOIN revisions p ON p.rev_id = rp.parent
2
- WHERE rp.child = ? ORDER BY p.rev_id`).all(n.rev_id);return{rev_id:n.rev_id,seq:n.seq,number:n.number,body_hash:n.body_hash,author:n.author,rationale:n.rationale,verbatim:n.verbatim===1,created_at:n.created_at,parents:t.map(a=>a.rev_id)}}s(X,"toRevision");async function C(e,n){const t=await e.prepare(`SELECT * FROM revisions r WHERE r.document = ?
3
- AND NOT EXISTS (SELECT 1 FROM revision_parents p WHERE p.parent = r.rev_id) ORDER BY r.number DESC, r.rev_id`).all(n.id);return await R(t,async a=>await X(e,a))}s(C,"heads");function Oe(e){return e.map(n=>n.rev_id).sort().join(",")}s(Oe,"headsKeyOf");async function T(e,n){const t=await e.prepare("SELECT * FROM documents WHERE object = ?").get(n.id);if(!t)throw v(`no document record for ${n.id}`);return{summary:t.summary,heads_key:t.heads_key,revision:t.revision,why_it_exists:t.why_it_exists,unlisted:t.unlisted===1,lint_ignore:t.lint_ignore_json===null?null:JSON.parse(t.lint_ignore_json),extensions:t.extensions_json===null?null:JSON.parse(t.extensions_json),source_path:t.source_path}}s(T,"documentRecord");async function j(e,n,t){const a=await e.prepare(`INSERT INTO revisions (document, rev_id, number, body_hash, author, rationale, verbatim, created_at)
4
- VALUES (?, ?, ?, ?, ?, ?, ?, ?) RETURNING *`).get(n.id,t.rev_id??se(),t.number,t.body_hash,t.author?.id??null,t.rationale,t.verbatim?1:0,t.created_at??new Date().toISOString()),i=e.prepare("INSERT INTO revision_parents (child, parent) VALUES (?, ?)");for(const r of t.parents)await i.run(a.rev_id,r);return a}s(j,"insertRevision");function De(e){const n=new Set;for(const t of e.matchAll(/\[\[([^[\]\n]+?)\]\]/g))n.add(t[1].trim());return[...n]}s(De,"linksIn");async function z(e,n,t){const a=[],i=[];for(const r of t){const o=await he(e,n,r);"object"in o?a.push({written_as:r,dst:o.object}):"deleted"in o?a.push({written_as:r,dst:o.deleted}):"remote"in o||i.push(r)}return{links:a,unresolved:i}}s(z,"resolveLinks");async function Q(e,n,t,a){const{links:i,unresolved:r}=await z(e,n,De(a));return await ae(e,await G(e,t,i)),await fe(e,t,r),r}s(Q,"setRevisionLinks");async function W(e,n,t){const a=new Map;for(const{src:i,name:r}of await ye(e,n.name,n.id))a.set(i,[...a.get(i)??[],r]);for(const[i,r]of a){const o=await m(e,i),u=o?.owner==null?null:await m(e,o.owner);if(!o||o.deleted_at||o.home_machine!==null||!u||u.deleted_at)continue;const c=u.type==="node"?await ve(e,u.id,t??(async()=>[]),_e(e),!0):await Ne(e,o,t),w=(await z(e,c,r)).links.filter(l=>l.dst.id===n.id);w.length!==0&&(await G(e,o,[...await de(e,o),...w]),await me(e,o,w.map(l=>l.written_as)))}}s(W,"linkNewlyNamed");async function ke(e,n,t){return n.type==="node"?n.id:await e.prepare(`SELECT 1 FROM objects o JOIN objects d ON d.owner = o.id
1
+ var oe=Object.defineProperty;var s=(t,n)=>oe(t,"name",{value:n,configurable:!0});import{randomUUID as se}from"node:crypto";import{CrtrError as v,notFound as N,usage as p}from"../errors.js";import{ExitCode as R}from"../../types.js";import{canChange as j,canRead as P,missingMemoryScope as ce,missingMemoryScopeUnder as ue,ownerChain as we}from"./access.js";import{bodyOf as pe,get as le,put as B}from"./bodies.js";import{dropUnresolved as me,linksOf as de,recordActedOn as g,setLinks as G,setUnresolved as fe,unresolvedLinksCouldName as he}from"./edges.js";import{publish as x}from"./events.js";import{resolveName as ye}from"./names.js";import{createObject as Ee,getById as m}from"./objects.js";import{liveLookups as _e,readerForNode as ve,readerForObject as Ne}from"./reader.js";import{replaceRules as K,setRulesOwner as Re}from"./rules.js";import{recordWatch as I}from"./watches.js";import{withCanvasWrite as h}from"../canvas/db.js";import{filterAsync as q,forEachAsync as ge,mapAsync as S}from"../async-array.js";function E(t){return t.class==="node"?t.node:t.class==="app"?t.app:t.class==="object"?t.object:t.person}s(E,"actorOf");function Se(t,n){return new v("conflict",t,R.GENERAL,n)}s(Se,"conflict");function y(t){if(t.type!=="document")throw p(`${t.type} ${t.id} is not a document`)}s(y,"assertDocument");async function Y(t,n){const e=await we(t,n);return(e.length?e[e.length-1]:n).type}s(Y,"spaceType");async function $(t,n,e){if(e!=="public"&&await Y(t,n)==="repo")throw p(`a repo owns only public documents; ${n.name} cannot own a ${e} one`,{rule:"repo-public-only",next:"Write it as public, or give it another owner (your own documents can carry `if: {repo: <handle>}`)."})}s($,"assertPublicInRepo");async function H(t,n,e,i){if(e.trim()===""||e!==e.trim())throw p(`invalid document name ${JSON.stringify(e)}`);const a=await t.prepare("SELECT id FROM objects WHERE (type = 'document' OR strpos(type, '.') > 0) AND owner = ? AND name = ? AND deleted_at IS NULL AND home_machine IS NULL").get(n.id,e);if(a&&a.id!==i?.id)throw Se(`${n.name} already has a document named ${e}`,{name:e,holder:a.id})}s(H,"assertNameFree");async function U(t,n,e){if(!await P(t,n,e))throw N(`no document ${e.id}`)}s(U,"readable");function V(t){return new v("scope_missing",`missing scope ${t}`,R.GENERAL,{scope:t})}s(V,"scopeMissing");async function T(t,n,e){await U(t,n,e);const{source_path:i}=await O(t,e);if(i!==null)throw new v("forbidden",`${e.name} is a built-in document shipped by a package (served from ${i}); \`crtr doc\` cannot change it`,R.GENERAL,{id:e.id,next:"Edit it in the source repository of the package that ships it (for crtr's own documents, the crouter repo), not with `crtr doc`."});const a=await ce(t,n,e,"rw");if(a!==null)throw V(a);if(!await j(t,n,e))throw new v("forbidden",`you cannot change ${e.name}`,R.GENERAL,{id:e.id})}s(T,"changeable");async function X(t,n){const e=await t.prepare(`SELECT p.rev_id FROM revision_parents rp JOIN revisions p ON p.rev_id = rp.parent
2
+ WHERE rp.child = ? ORDER BY p.rev_id`).all(n.rev_id);return{rev_id:n.rev_id,seq:n.seq,number:n.number,body_hash:n.body_hash,author:n.author,rationale:n.rationale,verbatim:n.verbatim===1,created_at:n.created_at,parents:e.map(i=>i.rev_id)}}s(X,"toRevision");async function C(t,n){const e=await t.prepare(`SELECT * FROM revisions r WHERE r.document = ?
3
+ AND NOT EXISTS (SELECT 1 FROM revision_parents p WHERE p.parent = r.rev_id) ORDER BY r.number DESC, r.rev_id`).all(n.id);return await S(e,async i=>await X(t,i))}s(C,"heads");function Oe(t){return t.map(n=>n.rev_id).sort().join(",")}s(Oe,"headsKeyOf");async function O(t,n){const e=await t.prepare("SELECT * FROM documents WHERE object = ?").get(n.id);if(!e)throw N(`no document record for ${n.id}`);return{summary:e.summary,heads_key:e.heads_key,revision:e.revision,why_it_exists:e.why_it_exists,unlisted:e.unlisted===1,lint_ignore:e.lint_ignore_json===null?null:JSON.parse(e.lint_ignore_json),extensions:e.extensions_json===null?null:JSON.parse(e.extensions_json),source_path:e.source_path}}s(O,"documentRecord");async function A(t,n,e){const i=await t.prepare(`INSERT INTO revisions (document, rev_id, number, body_hash, author, rationale, verbatim, created_at)
4
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?) RETURNING *`).get(n.id,e.rev_id??se(),e.number,e.body_hash,e.author?.id??null,e.rationale,e.verbatim?1:0,e.created_at??new Date().toISOString()),a=t.prepare("INSERT INTO revision_parents (child, parent) VALUES (?, ?)");for(const r of e.parents)await a.run(i.rev_id,r);return i}s(A,"insertRevision");function De(t){const n=new Set;for(const e of t.matchAll(/\[\[([^[\]\n]+?)\]\]/g))n.add(e[1].trim());return[...n]}s(De,"linksIn");async function z(t,n,e){const i=[],a=[];for(const r of e){const o=await ye(t,n,r);"object"in o?i.push({written_as:r,dst:o.object}):"deleted"in o?i.push({written_as:r,dst:o.deleted}):"remote"in o||a.push(r)}return{links:i,unresolved:a}}s(z,"resolveLinks");async function Q(t,n,e,i){const{links:a,unresolved:r}=await z(t,n,De(i));return await ae(t,await G(t,e,a)),await fe(t,e,r),r}s(Q,"setRevisionLinks");async function W(t,n,e){const i=new Map;for(const{src:a,name:r}of await he(t,n.name,n.id))i.set(a,[...i.get(a)??[],r]);for(const[a,r]of i){const o=await m(t,a),c=o?.owner==null?null:await m(t,o.owner);if(!o||o.deleted_at||o.home_machine!==null||!c||c.deleted_at)continue;const u=c.type==="node"?await ve(t,c.id,e??(async()=>[]),_e(t),!0):await Ne(t,o,e),w=(await z(t,u,r)).links.filter(l=>l.dst.id===n.id);w.length!==0&&(await G(t,o,[...await de(t,o),...w]),await me(t,o,w.map(l=>l.written_as)))}}s(W,"linkNewlyNamed");async function ke(t,n,e){return n.type==="node"?n.id:await t.prepare(`SELECT 1 FROM objects o JOIN objects d ON d.owner = o.id
5
5
  WHERE o.name = ? AND o.type <> ? AND o.type IN ('person','app','repo','profile')
6
- AND d.type = 'document' AND d.name = ? AND d.deleted_at IS NULL LIMIT 1`).get(n.name,n.type,t)?`${n.type}:${n.name}`:n.name}s(ke,"ownerPrefix");async function Z(e,n,t){const a=await e.prepare(`SELECT e.written_as, o.* FROM edges e JOIN objects o ON o.id = e.dst
7
- WHERE e.type = 'links-to' AND e.src = ? AND e.written_as IS NOT NULL`).all(n.id);if(a.length===0)return t;const i=new Map;for(const r of a){if(r.deleted_at||r.written_as===r.id)continue;const o=!r.written_as.includes("/");if(o&&r.name===r.written_as)continue;const u=r.owner===null?null:await m(e,r.owner),c=u?`${await ke(e,u,r.name)}/${r.name}`:r.name,w=o&&r.owner===n.owner?r.name:c;w!==r.written_as&&i.set(r.written_as,w)}return i.size===0?t:t.replace(/\[\[([^[\]\n]+?)\]\]/g,(r,o)=>{const u=i.get(o.trim());return u===void 0?r:`[[${u}]]`})}s(Z,"withCurrentNames");async function M(e,n,t){if(t.preview!==void 0){if(t.preview.trim()==="")throw p("a document needs a preview",{field:"preview"});await e.prepare("UPDATE objects SET preview = ? WHERE id = ?").run(t.preview,n.id)}t.privacy!==void 0&&(await $(e,await m(e,n.owner),t.privacy),await e.prepare("UPDATE objects SET privacy = ? WHERE id = ?").run(t.privacy,n.id));const a=s(async(r,o)=>await e.prepareUncached(`UPDATE documents SET ${r} = ? WHERE object = ?`).run(o,n.id),"set");t.summary!==void 0&&await a("summary",t.summary),t.whyItExists!==void 0&&await a("why_it_exists",t.whyItExists),t.unlisted!==void 0&&await a("unlisted",t.unlisted?1:0),t.lintIgnore!==void 0&&await a("lint_ignore_json",t.lintIgnore===null?null:JSON.stringify(t.lintIgnore)),t.extensions!==void 0&&await a("extensions_json",t.extensions===null?null:JSON.stringify(t.extensions));const i=await m(e,n.id);return t.rules!==void 0&&await K(e,i,t.rules),i}s(M,"applyDocumentFields");async function b(e,n,t){if(n.owner.deleted_at)throw p(`${n.owner.name} is deleted`);if(n.preview.trim()==="")throw p("a document needs a preview",{field:"preview"});await $(e,n.owner,n.privacy),await H(e,n.owner,n.name);const a=await Ee(e,{...n.id?{id:n.id}:{},type:"document",kind:n.kind,owner:n.owner.id,privacy:n.privacy,name:n.name,preview:n.preview});await e.prepare("INSERT INTO documents (object, heads_key, revision) VALUES (?, '', 0)").run(a.id);const i=await M(e,a,{...n,preview:void 0,privacy:void 0,rules:n.rules??[]});return await W(e,i,t),i}s(b,"createDocument");async function F(e,n){const t=await C(e,n),a=Oe(t),i=Math.max(0,...t.map(r=>r.number));return await e.prepare("UPDATE documents SET heads_key = ?, revision = ? WHERE object = ?").run(a,i,n.id),{heads_key:a,revision:i,headCount:t.length}}s(F,"setHeads");async function Ke(e,n,t,a,i){const r=B(a);return await y(async()=>{const o=E(n),u=await ce(e,n,t.owner,"rw");if(u!==null)throw V(u);const c=await b(e,t,i),w=await j(e,c,{number:1,body_hash:r,author:o,rationale:null,parents:[]}),{heads_key:l}=await F(e,c);if(!await L(e,n,c))throw v(`no owner ${t.owner.name} you can write to`);const d=await Q(e,n,c,a);return await N(e,o,c),await I(e,o,c,"write",{readHeadsKey:l}),await k(e,{event:"created",object:c,actor:o,attrs:{revision:1},pointer:w.rev_id},i),{object:c,revision:1,unresolved:d}},{savepoint:!0})}s(Ke,"writeDocument");async function qe(e,n,t){return(await e.prepare("SELECT read_heads_key FROM watches WHERE watcher = ? AND object = ?").get(E(n).id,t.id))?.read_heads_key??null}s(qe,"readBase");function xe(e,n){let t=e;for(const[a,{old:i,new:r}]of n.entries()){if(i==="")throw p(`block ${a+1}: a replacement needs non-empty old text`,{field:"replace"});const o=t.indexOf(i);if(o<0)throw p(`block ${a+1}: old text not found: ${JSON.stringify(i.slice(0,120))}`,{field:"replace",old:i});if(t.indexOf(i,o+1)>=0)throw p(`block ${a+1}: old text occurs more than once: ${JSON.stringify(i.slice(0,120))}`,{field:"replace",old:i});t=t.slice(0,o)+r+t.slice(o+i.length)}return t}s(xe,"applyReplacements");async function Ye(e,n,t,a,i,r,o={},u){if(h(t),await x(e,n,t),t.deleted_at)throw p(`${t.name} is deleted`);if(r===null)throw new O("read_first",`read ${t.name} before editing it`,D.GENERAL,{id:t.id});if(i.trim()==="")throw p("an edit needs a rationale",{field:"rationale"});const c=await T(e,t);if(c.heads_key!==r)return{stale:{heads_key:c.heads_key,revision:c.revision}};const w=await C(e,t);let l;if("body"in a)l=a.body;else{if(w.length!==1)throw p(`${t.name} has ${w.length} heads and needs a merge; pass the whole merged body`,{heads:w.length});l=xe(await Z(e,t,le(w[0].body_hash)),a.replace)}const d=B(l);try{return await y(async()=>{const f=E(n),g=1+Math.max(0,...w.map(_=>_.number)),J=await j(e,t,{number:g,body_hash:d,author:f,rationale:i,verbatim:a.verbatim===!0,parents:w.map(_=>_.rev_id)}),A=J.rev_id,ie=await e.prepare("UPDATE documents SET heads_key = ?, revision = ? WHERE object = ? AND heads_key = ?").run(A,g,t.id,r);if(Number(ie.changes)===0){const _=await T(e,t);throw new ee(_.heads_key,_.revision)}const S=await M(e,t,o),re=await Q(e,n,S,l);return await N(e,f,S),await I(e,f,S,"write",{readHeadsKey:A}),await k(e,{event:"edited",object:S,actor:f,pointer:J.rev_id,attrs:{revision:g,from_revision:c.revision,rationale:i}},u),{revision:g,heads_key:A,unresolved:re}},{savepoint:!0})}catch(f){if(f instanceof ee)return{stale:{heads_key:f.heads_key,revision:f.revision}};throw f}}s(Ye,"editDocument");class ee extends Error{static{s(this,"StaleEdit")}heads_key;revision;constructor(n,t){super("stale"),this.heads_key=n,this.revision=t}}async function Ve(e,n,t,a){return h(t),n&&await x(e,n,t),await y(async()=>{const i=await M(e,t,a);return n&&await N(e,E(n),i),i},{savepoint:!0})}s(Ve,"setDocumentRecord");async function Te(e,n,t,a){const i=E(n),{heads_key:r}=await T(e,t);await I(e,i,t,"read",{readHeadsKey:r}),await k(e,{event:"read",object:t,actor:i,attrs:{}},a)}s(Te,"recordRead");async function Xe(e,n,t,a){return h(t),await U(e,n,t),await y(async()=>{const i=await T(e,t),r=await R(await C(e,t),async u=>({...u,body:await Z(e,t,pe(u.body_hash,i.source_path))})),o=(await q(await R(await e.prepare("SELECT dst, written_as FROM edges WHERE type = 'links-to' AND src = ? ORDER BY written_as").all(t.id),async u=>({written_as:u.written_as??"",object:await m(e,u.dst)})),async u=>u.object&&await P(e,n,u.object))).map(u=>({...u,deleted:u.object.deleted_at!==null}));return t.deleted_at||await Te(e,n,t,a),{object:t,record:i,heads:r,needsMerge:r.length>1,links:o}},{savepoint:!0})}s(Xe,"readDocument");async function ze(e,n,t){h(t),await U(e,n,t);const a=await e.prepare("SELECT * FROM revisions WHERE document = ? ORDER BY number DESC, created_at DESC, seq DESC").all(t.id);return await R(a,async i=>await X(e,i))}s(ze,"history");async function Qe(e,n,t,a,i){if(h(t),t.deleted_at)throw p(`${t.name} is deleted`);return n&&await x(e,n,t),await y(async()=>{const r=a.owner??await m(e,t.owner),o=a.name??t.name;await H(e,r,o,t),await $(e,r,t.privacy),await e.prepare("UPDATE objects SET name = ?, owner = ? WHERE id = ?").run(o,r.id,t.id);const u=await m(e,t.id);if(r.id!==t.owner&&(await Re(e,u),n&&!await L(e,n,u)))throw v(`no owner ${r.name} you can move documents to`);return n&&await N(e,E(n),u),(o!==t.name||r.id!==t.owner)&&await W(e,u,i),u},{savepoint:!0})}s(Qe,"moveDocument");async function Ze(e,n,t,a,i){return h(t),t.deleted_at?t:(n&&await x(e,n,t),await y(async()=>{const r=await te(e,n,t,a,i);return await ae(e,await ne(e,t.id),a),r},{savepoint:!0}))}s(Ze,"deleteDocument");async function be(e,n,t,a){return h(n),n.deleted_at?await y(async()=>{await H(e,await m(e,n.owner),t,n),await e.prepare("UPDATE objects SET deleted_at = NULL, name = ? WHERE id = ?").run(t,n.id);const i=await m(e,n.id);return await W(e,i,a),i},{savepoint:!0}):n}s(be,"restoreDocument");async function te(e,n,t,a,i){await e.prepare("UPDATE objects SET deleted_at = ? WHERE id = ?").run(i??new Date().toISOString(),t.id);const r=await m(e,t.id),o=n?E(n):null;return o&&await N(e,o,r),await k(e,{event:"deleted",object:r,actor:o,attrs:{}},a),await e.prepare("DELETE FROM watches WHERE object = ?").run(t.id),await K(e,r,[]),r}s(te,"markDeleted");async function ne(e,n){return(await e.prepare("SELECT dst FROM edges WHERE type = 'links-to' AND src = ?").all(n)).map(({dst:t})=>t)}s(ne,"linkTargets");async function ae(e,n,t){const a=e.prepare(`SELECT 1 FROM objects d JOIN objects w ON w.id = d.owner
6
+ AND d.type = 'document' AND d.name = ? AND d.deleted_at IS NULL LIMIT 1`).get(n.name,n.type,e)?`${n.type}:${n.name}`:n.name}s(ke,"ownerPrefix");async function Z(t,n,e){const i=await t.prepare(`SELECT e.written_as, o.* FROM edges e JOIN objects o ON o.id = e.dst
7
+ WHERE e.type = 'links-to' AND e.src = ? AND e.written_as IS NOT NULL`).all(n.id);if(i.length===0)return e;const a=new Map;for(const r of i){if(r.deleted_at||r.written_as===r.id)continue;const o=!r.written_as.includes("/");if(o&&r.name===r.written_as)continue;const c=r.owner===null?null:await m(t,r.owner),u=c?`${await ke(t,c,r.name)}/${r.name}`:r.name,w=o&&r.owner===n.owner?r.name:u;w!==r.written_as&&a.set(r.written_as,w)}return a.size===0?e:e.replace(/\[\[([^[\]\n]+?)\]\]/g,(r,o)=>{const c=a.get(o.trim());return c===void 0?r:`[[${c}]]`})}s(Z,"withCurrentNames");async function M(t,n,e){if(e.preview!==void 0){if(e.preview.trim()==="")throw p("a document needs a preview",{field:"preview"});await t.prepare("UPDATE objects SET preview = ? WHERE id = ?").run(e.preview,n.id)}e.privacy!==void 0&&(await $(t,await m(t,n.owner),e.privacy),await t.prepare("UPDATE objects SET privacy = ? WHERE id = ?").run(e.privacy,n.id));const i=s(async(r,o)=>await t.prepareUncached(`UPDATE documents SET ${r} = ? WHERE object = ?`).run(o,n.id),"set");e.summary!==void 0&&await i("summary",e.summary),e.whyItExists!==void 0&&await i("why_it_exists",e.whyItExists),e.unlisted!==void 0&&await i("unlisted",e.unlisted?1:0),e.lintIgnore!==void 0&&await i("lint_ignore_json",e.lintIgnore===null?null:JSON.stringify(e.lintIgnore)),e.extensions!==void 0&&await i("extensions_json",e.extensions===null?null:JSON.stringify(e.extensions));const a=await m(t,n.id);return e.rules!==void 0&&await K(t,a,e.rules),a}s(M,"applyDocumentFields");async function b(t,n,e){if(n.owner.deleted_at)throw p(`${n.owner.name} is deleted`);if(n.preview.trim()==="")throw p("a document needs a preview",{field:"preview"});await $(t,n.owner,n.privacy),await H(t,n.owner,n.name);const i=await Ee(t,{...n.id?{id:n.id}:{},type:"document",kind:n.kind,owner:n.owner.id,privacy:n.privacy,name:n.name,preview:n.preview});await t.prepare("INSERT INTO documents (object, heads_key, revision) VALUES (?, '', 0)").run(i.id);const a=await M(t,i,{...n,preview:void 0,privacy:void 0,rules:n.rules??[]});return await W(t,a,e),a}s(b,"createDocument");async function F(t,n){const e=await C(t,n),i=Oe(e),a=Math.max(0,...e.map(r=>r.number));return await t.prepare("UPDATE documents SET heads_key = ?, revision = ? WHERE object = ?").run(i,a,n.id),{heads_key:i,revision:a,headCount:e.length}}s(F,"setHeads");async function Ke(t,n,e,i,a){const r=B(i);return await h(async()=>{const o=E(n),c=await ue(t,n,e.owner,"rw");if(c!==null)throw V(c);const u=await b(t,e,a),w=await A(t,u,{number:1,body_hash:r,author:o,rationale:null,parents:[]}),{heads_key:l}=await F(t,u);if(!await j(t,n,u))throw N(`no owner ${e.owner.name} you can write to`);const d=await Q(t,n,u,i);return await g(t,o,u),await I(t,o,u,"write",{readHeadsKey:l}),await x(t,{event:"created",object:u,actor:o,attrs:{revision:1},pointer:w.rev_id},a),{object:u,revision:1,unresolved:d}},{savepoint:!0})}s(Ke,"writeDocument");async function qe(t,n,e){return(await t.prepare("SELECT read_heads_key FROM watches WHERE watcher = ? AND object = ?").get(E(n).id,e.id))?.read_heads_key??null}s(qe,"readBase");function xe(t,n){let e=t;for(const[i,{old:a,new:r}]of n.entries()){if(a==="")throw p(`block ${i+1}: a replacement needs non-empty old text`,{field:"replace"});const o=e.indexOf(a);if(o<0)throw p(`block ${i+1}: old text not found: ${JSON.stringify(a.slice(0,120))}`,{field:"replace",old:a});if(e.indexOf(a,o+1)>=0)throw p(`block ${i+1}: old text occurs more than once: ${JSON.stringify(a.slice(0,120))}`,{field:"replace",old:a});e=e.slice(0,o)+r+e.slice(o+a.length)}return e}s(xe,"applyReplacements");async function Ye(t,n,e,i,a,r,o={},c){if(y(e),await T(t,n,e),e.deleted_at)throw p(`${e.name} is deleted`);if(r===null)throw new v("read_first",`read ${e.name} before editing it`,R.GENERAL,{id:e.id});if(a.trim()==="")throw p("an edit needs a rationale",{field:"rationale"});const u=await O(t,e);if(u.heads_key!==r)return{stale:{heads_key:u.heads_key,revision:u.revision}};const w=await C(t,e);let l;if("body"in i)l=i.body;else{if(w.length!==1)throw p(`${e.name} has ${w.length} heads and needs a merge; pass the whole merged body`,{heads:w.length});l=xe(await Z(t,e,le(w[0].body_hash)),i.replace)}const d=B(l);try{return await h(async()=>{const f=E(n),D=1+Math.max(0,...w.map(_=>_.number)),J=await A(t,e,{number:D,body_hash:d,author:f,rationale:a,verbatim:i.verbatim===!0,parents:w.map(_=>_.rev_id)}),L=J.rev_id,ie=await t.prepare("UPDATE documents SET heads_key = ?, revision = ? WHERE object = ? AND heads_key = ?").run(L,D,e.id,r);if(Number(ie.changes)===0){const _=await O(t,e);throw new ee(_.heads_key,_.revision)}const k=await M(t,e,o),re=await Q(t,n,k,l);return await g(t,f,k),await I(t,f,k,"write",{readHeadsKey:L}),await x(t,{event:"edited",object:k,actor:f,pointer:J.rev_id,attrs:{revision:D,from_revision:u.revision,rationale:a}},c),{revision:D,heads_key:L,unresolved:re}},{savepoint:!0})}catch(f){if(f instanceof ee)return{stale:{heads_key:f.heads_key,revision:f.revision}};throw f}}s(Ye,"editDocument");class ee extends Error{static{s(this,"StaleEdit")}heads_key;revision;constructor(n,e){super("stale"),this.heads_key=n,this.revision=e}}async function Ve(t,n,e,i){return y(e),n&&await T(t,n,e),await h(async()=>{const a=await M(t,e,i);return n&&await g(t,E(n),a),a},{savepoint:!0})}s(Ve,"setDocumentRecord");async function Te(t,n,e,i){const a=E(n),{heads_key:r}=await O(t,e);await I(t,a,e,"read",{readHeadsKey:r}),await x(t,{event:"read",object:e,actor:a,attrs:{}},i)}s(Te,"recordRead");async function Xe(t,n,e,i){return y(e),await U(t,n,e),await h(async()=>{const a=await O(t,e),r=await S(await C(t,e),async c=>({...c,body:await Z(t,e,pe(c.body_hash,a.source_path))})),o=(await q(await S(await t.prepare("SELECT dst, written_as FROM edges WHERE type = 'links-to' AND src = ? ORDER BY written_as").all(e.id),async c=>({written_as:c.written_as??"",object:await m(t,c.dst)})),async c=>c.object&&await P(t,n,c.object))).map(c=>({...c,deleted:c.object.deleted_at!==null}));return e.deleted_at||await Te(t,n,e,i),{object:e,record:a,heads:r,needsMerge:r.length>1,links:o}},{savepoint:!0})}s(Xe,"readDocument");async function ze(t,n,e){y(e),await U(t,n,e);const i=await t.prepare("SELECT * FROM revisions WHERE document = ? ORDER BY number DESC, created_at DESC, seq DESC").all(e.id);return await S(i,async a=>await X(t,a))}s(ze,"history");async function Qe(t,n,e,i,a){if(y(e),e.deleted_at)throw p(`${e.name} is deleted`);return n&&await T(t,n,e),await h(async()=>{const r=i.owner??await m(t,e.owner),o=i.name??e.name;await H(t,r,o,e),await $(t,r,e.privacy),await t.prepare("UPDATE objects SET name = ?, owner = ? WHERE id = ?").run(o,r.id,e.id);const c=await m(t,e.id);if(r.id!==e.owner&&(await Re(t,c),n&&!await j(t,n,c)))throw N(`no owner ${r.name} you can move documents to`);return n&&await g(t,E(n),c),(o!==e.name||r.id!==e.owner)&&await W(t,c,a),c},{savepoint:!0})}s(Qe,"moveDocument");async function Ze(t,n,e,i,a){return y(e),e.deleted_at?e:(n&&await T(t,n,e),await h(async()=>{const r=await te(t,n,e,i,a);return await ae(t,await ne(t,e.id),i),r},{savepoint:!0}))}s(Ze,"deleteDocument");async function be(t,n,e,i){return y(n),n.deleted_at?await h(async()=>{await H(t,await m(t,n.owner),e,n),await t.prepare("UPDATE objects SET deleted_at = NULL, name = ? WHERE id = ?").run(e,n.id);const a=await m(t,n.id);return await W(t,a,i),a},{savepoint:!0}):n}s(be,"restoreDocument");async function te(t,n,e,i,a){await t.prepare("UPDATE objects SET deleted_at = ? WHERE id = ?").run(a??new Date().toISOString(),e.id);const r=await m(t,e.id),o=n?E(n):null;return o&&await g(t,o,r),await x(t,{event:"deleted",object:r,actor:o,attrs:{}},i),await t.prepare("DELETE FROM watches WHERE object = ?").run(e.id),await K(t,r,[]),r}s(te,"markDeleted");async function ne(t,n){return(await t.prepare("SELECT dst FROM edges WHERE type = 'links-to' AND src = ?").all(n)).map(({dst:e})=>e)}s(ne,"linkTargets");async function ae(t,n,e){const i=t.prepare(`SELECT 1 FROM objects d JOIN objects w ON w.id = d.owner
8
8
  WHERE d.id = ? AND d.type = 'document' AND d.deleted_at IS NULL AND w.deleted_at IS NOT NULL
9
9
  AND NOT EXISTS (SELECT 1 FROM edges e JOIN objects s ON s.id = e.src
10
- WHERE e.type = 'links-to' AND e.dst = d.id AND e.src != d.id AND s.deleted_at IS NULL)`),i=[...n];for(;i.length>0;){const r=i.pop();await a.get(r)!==void 0&&(await te(e,null,await m(e,r),t),i.push(...await ne(e,r)))}}s(ae,"collectUnlinked");async function et(e,n,t){if(n.revisions.length===0)throw p(`import of ${n.name}: no revisions`);return await y(async()=>{const a=await b(e,n,t);await e.prepare("UPDATE objects SET created_at = ? WHERE id = ?").run(n.revisions[0].created_at,a.id);let i=null;return await ge(n.revisions,async(r,o)=>{i=(await j(e,a,{...r,number:o+1,parents:i===null?[]:[i]})).rev_id}),await F(e,a),await m(e,a.id)},{savepoint:!0})}s(et,"importDocument");async function tt(e,n,t){if(h(n),n.privacy!=="public"&&await Y(e,n)==="repo")throw p(`a repo owns only public documents; refusing revisions of ${n.name}`);return await y(async()=>{const a=e.prepare("SELECT rev_id, number, document FROM revisions WHERE rev_id = ?");let i=await q(t,async u=>!await a.get(u.rev_id));for(;i.length>0;){const u=[];for(const c of i){const w=await R(c.parents,async d=>await a.get(d));if(w.some(d=>d===void 0)){u.push(c);continue}const l=w;if(l.some(d=>d.document!==n.id))throw p(`revision ${c.rev_id} names a parent of another document`);await j(e,n,{...c,author:null,number:1+Math.max(0,...l.map(d=>d.number)),parents:l.map(d=>d.rev_id)})}if(u.length===i.length)throw p(`revisions of ${n.name} name unknown parents: ${u.map(c=>c.rev_id).join(", ")}`);i=u}const{heads_key:r,headCount:o}=await F(e,n);return{heads_key:r,headCount:o}},{savepoint:!0})}s(tt,"insertImportedRevisions");async function nt(e,n){const t=await m(e,n);if(!t||t.type!=="document")throw v(`no document ${n}`);return t}s(nt,"documentById");export{E as actorOf,M as applyDocumentFields,xe as applyReplacements,ae as collectUnlinked,Ze as deleteDocument,nt as documentById,T as documentRecord,Ye as editDocument,C as heads,ze as history,et as importDocument,tt as insertImportedRevisions,De as linksIn,Qe as moveDocument,qe as readBase,Xe as readDocument,Te as recordRead,be as restoreDocument,Ve as setDocumentRecord,Q as setRevisionLinks,Z as withCurrentNames,Ke as writeDocument};
10
+ WHERE e.type = 'links-to' AND e.dst = d.id AND e.src != d.id AND s.deleted_at IS NULL)`),a=[...n];for(;a.length>0;){const r=a.pop();await i.get(r)!==void 0&&(await te(t,null,await m(t,r),e),a.push(...await ne(t,r)))}}s(ae,"collectUnlinked");async function et(t,n,e){if(n.revisions.length===0)throw p(`import of ${n.name}: no revisions`);return await h(async()=>{const i=await b(t,n,e);await t.prepare("UPDATE objects SET created_at = ? WHERE id = ?").run(n.revisions[0].created_at,i.id);let a=null;return await ge(n.revisions,async(r,o)=>{a=(await A(t,i,{...r,number:o+1,parents:a===null?[]:[a]})).rev_id}),await F(t,i),await m(t,i.id)},{savepoint:!0})}s(et,"importDocument");async function tt(t,n,e){if(y(n),n.privacy!=="public"&&await Y(t,n)==="repo")throw p(`a repo owns only public documents; refusing revisions of ${n.name}`);return await h(async()=>{const i=t.prepare("SELECT rev_id, number, document FROM revisions WHERE rev_id = ?");let a=await q(e,async c=>!await i.get(c.rev_id));for(;a.length>0;){const c=[];for(const u of a){const w=await S(u.parents,async d=>await i.get(d));if(w.some(d=>d===void 0)){c.push(u);continue}const l=w;if(l.some(d=>d.document!==n.id))throw p(`revision ${u.rev_id} names a parent of another document`);await A(t,n,{...u,author:null,number:1+Math.max(0,...l.map(d=>d.number)),parents:l.map(d=>d.rev_id)})}if(c.length===a.length)throw p(`revisions of ${n.name} name unknown parents: ${c.map(u=>u.rev_id).join(", ")}`);a=c}const{heads_key:r,headCount:o}=await F(t,n);return{heads_key:r,headCount:o}},{savepoint:!0})}s(tt,"insertImportedRevisions");async function nt(t,n){const e=await m(t,n);if(!e||e.type!=="document")throw N(`no document ${n}`);return e}s(nt,"documentById");export{E as actorOf,M as applyDocumentFields,xe as applyReplacements,ae as collectUnlinked,Ze as deleteDocument,nt as documentById,O as documentRecord,Ye as editDocument,C as heads,ze as history,et as importDocument,tt as insertImportedRevisions,De as linksIn,Qe as moveDocument,qe as readBase,Xe as readDocument,Te as recordRead,be as restoreDocument,Ve as setDocumentRecord,Q as setRevisionLinks,Z as withCurrentNames,Ke as writeDocument};
@@ -1,6 +1,7 @@
1
1
  import { type CanvasDb } from '../canvas/db.js';
2
2
  import { type HumanRequestRecord } from '../human/requests.js';
3
3
  import { type PreparedPage } from '../human/page-authoring.js';
4
+ import { type PageManifest } from '../human/page-schema.js';
4
5
  /**
5
6
  * A question is a settleable human page request. Most belong to a run; a
6
7
  * provider approval can also belong to an app without a run. Its question id
@@ -29,6 +30,7 @@ export declare function providerApprovalPage(question: {
29
30
  label: string;
30
31
  }>;
31
32
  }): PreparedPage;
33
+ export declare function manifestOf(request: HumanRequestRecord): PageManifest;
32
34
  /** The question's run, asking node, and app, or null when the request is not a question. */
33
35
  export declare function questionRun(request: HumanRequestRecord, db?: CanvasDb): Promise<AskingNode | null>;
34
36
  export type QuestionKind = 'decision' | 'notice';
@@ -1,4 +1,4 @@
1
1
  var g=Object.defineProperty;var r=(n,e)=>g(n,"name",{value:e,configurable:!0});import{openDb as s}from"../canvas/db.js";import{askingNodeSql as c,getRequest as y}from"../human/requests.js";import{preparePageSource as h}from"../human/page-authoring.js";import{pageAwaitsResponse as d}from"../human/page-schema.js";import{appendRunEvent as f}from"./events.js";async function p(n,e){return await n.prepare("SELECT question_id, grantee, sub, provider, tool, status, option_id FROM provider_approvals WHERE question_id = ?").get(e)??null}r(p,"providerQuestion");function j(n){if(!n.title.trim()||!n.body.trim()||!Array.isArray(n.options)||n.options.length===0)throw new Error("provider approval needs a title, body, and options");const e=new Set(["deny"]);for(const o of n.options){if(!o.option_id||!o.label||e.has(o.option_id))throw new Error("provider approval option ids must be nonempty, unique, and different from deny");e.add(o.option_id)}const t=[...n.options.map(({option_id:o,label:a})=>({id:o,label:a})),{id:"deny",label:"Don't do this"}],i=`export default function Approval() { return <Page title={${JSON.stringify(n.title)}}><UserQuestion id="options" label="Choose an option" body={${JSON.stringify(n.body)}} mode="single" required={true} allowFreetext={false} options={${JSON.stringify(t)}} /></Page>; }`;return h({dialect:"jsx",document:i,delivery:{placement:"inline",inbox:!1,reply:!0}})}r(j,"providerApprovalPage");async function N(n,e){return e===null?null:await n.prepare("SELECT node_id, run_id, grantee FROM node_rows WHERE node_id = ? AND run_id IS NOT NULL").get(e)??null}r(N,"askingNode");function u(n){return JSON.parse(n.specification_json)}r(u,"manifestOf");async function l(n,e){if(e??=await s(),n.kind!=="page"||!n.settleable)return null;const t=u(n);return await N(e,n.requester_node_id??(d(t)?null:t.source?.nodeId??null))}r(l,"questionRun");function _(n){return d(u(n))?"decision":"notice"}r(_,"questionKind");async function w(n,e,t){t??=await s();const i=u(n),o=await p(t,n.request_id);return{question_id:n.request_id,initiating_app:e,title:n.title,...n.subtitle?{subtitle:n.subtitle}:{},...o?{provider:o.provider,tool:o.tool}:{},steps:i.steps,slots:i.slots.map(a=>({...a.id===void 0?{}:{id:a.id},kind:a.kind,step:a.step,config:a.config}))}}r(w,"questionFields");async function I(n,e){const t=await l(e,n);t===null||e.state!=="open"||_(e)==="notice"||await f(n,t.run_id,t.node_id,"waiting_on_user",await w(e,t.grantee,n))}r(I,"appendWaitingOnUser");async function J(n,e){const t=e.result_json===null?null:JSON.parse(e.result_json);if(await p(n,e.request_id)!==null){const a=t?.responses?.options?.selectedOptionIds?.[0];if(e.outcome==="answered"&&(typeof a!="string"||!a))throw new Error(`provider approval ${e.request_id} has no selected option`);await n.prepare("UPDATE provider_approvals SET status = ?, option_id = ? WHERE question_id = ? AND status = 'open'").run(e.outcome==="answered"?"answered":"canceled",e.outcome==="answered"?a:null,e.request_id)}if(e.outcome!=="answered"||t===null)return;const o=await l(e,n);o!==null&&await f(n,o.run_id,o.node_id,"question.answered",{question_id:e.request_id,answer:t.responses??{}})}r(J,"appendQuestionAnswered");async function m(n,e,t){return(await t.prepare(`SELECT h.request_id, h.specification_json FROM human_requests h JOIN node_rows n ON n.node_id = ${c("h")}
2
2
  WHERE n.run_id = ? AND h.kind = 'page' AND h.state = 'open' AND h.settleable = 1
3
3
  ORDER BY h.created_at, h.request_id`).all(n)).filter(i=>d(JSON.parse(i.specification_json))===(e==="decision")).map(i=>i.request_id)}r(m,"openIdsOfKind");async function T(n,e){return e??=await s(),await m(n,"decision",e)}r(T,"openQuestionIds");async function L(n,e){return e??=await s(),await m(n,"notice",e)}r(L,"openNoticeIds");function E(n){return n.state!=="settled"?"open":n.outcome==="answered"?"answered":"canceled"}r(E,"questionStatus");function v(n){if(n.result_json===null)return null;const e=JSON.parse(n.result_json);return n.outcome==="answered"?e.responses??{}:e}r(v,"questionResult");async function F(n,e,t){const i=E(n);return{...await w(n,e?.grantee??t.grantee),kind:_(n),...e===null?{}:{run_id:e.run_id,node_id:e.node_id},status:i,...i==="answered"?{answer:v(n)}:{}}}r(F,"questionObject");async function Q(n){const e=await y(n);if(e===null||e.kind!=="page"||!e.settleable)return null;const t=await s(),i=await l(e,t),o=await p(t,n);return i===null&&o===null?null:{request:e,run:i,approval:o}}r(Q,"getQuestion");async function O(n){const[e,t]="run_id"in n?["n.run_id = ?",n.run_id]:"node_id"in n?["n.node_id = ?",n.node_id]:["n.grantee = ?",n.grantee];return(await(await s()).prepareUncached(`SELECT h.request_id FROM human_requests h JOIN node_rows n ON n.node_id = ${c("h")}
4
- WHERE ${e} AND n.run_id IS NOT NULL AND h.kind = 'page' AND h.state = 'open' AND h.settleable = 1`).all(t)).map(i=>i.request_id)}r(O,"openQuestionIdsFor");async function $(n,e){const t=await O(n);if(!t.length)return 0;const{cancelHumanTicket:i}=await import("../../daemon/human/finish.js");let o=0;for(const a of t)(await i(a,{reason:e,actor:"crtrd"})).outcome==="settled"&&(o+=1);return o}r($,"cancelOpenQuestions");export{J as appendQuestionAnswered,I as appendWaitingOnUser,$ as cancelOpenQuestions,Q as getQuestion,L as openNoticeIds,T as openQuestionIds,O as openQuestionIdsFor,j as providerApprovalPage,_ as questionKind,F as questionObject,v as questionResult,l as questionRun,E as questionStatus};
4
+ WHERE ${e} AND n.run_id IS NOT NULL AND h.kind = 'page' AND h.state = 'open' AND h.settleable = 1`).all(t)).map(i=>i.request_id)}r(O,"openQuestionIdsFor");async function $(n,e){const t=await O(n);if(!t.length)return 0;const{cancelHumanTicket:i}=await import("../../daemon/human/finish.js");let o=0;for(const a of t)(await i(a,{reason:e,actor:"crtrd"})).outcome==="settled"&&(o+=1);return o}r($,"cancelOpenQuestions");export{J as appendQuestionAnswered,I as appendWaitingOnUser,$ as cancelOpenQuestions,Q as getQuestion,u as manifestOf,L as openNoticeIds,T as openQuestionIds,O as openQuestionIdsFor,j as providerApprovalPage,_ as questionKind,F as questionObject,v as questionResult,l as questionRun,E as questionStatus};
@@ -1,18 +1,18 @@
1
- var Z=Object.defineProperty;var o=(e,r)=>Z(e,"name",{value:r,configurable:!0});import{existsSync as k,mkdirSync as P,readFileSync as ee,realpathSync as H,rmSync as te,writeFileSync as re}from"node:fs";import{createHash as ne}from"node:crypto";import{dirname as oe,isAbsolute as ae,join as se,relative as ie,resolve as N,sep as ce}from"node:path";import{gitAsync as he,gitSync as i}from"./git.js";import{crtrHome as C}from"./canvas/paths.js";import{closeWorktreeAndRepoint as ue,getNode as v,recordLandIntent as le}from"./canvas/index.js";import{CONFIG_FILE as de,CRTR_DIR_NAME as O,STATE_FILE as fe}from"../types.js";import{withExclusiveDirectoryLock as pe,withExclusiveDirectoryLockAsync as me}from"./exclusive-lock.js";import{parseStatusPorcelain as be,planLandIntoCheckout as ke,refDeleteTransactionStdin as _e}from"./worktree-landing.js";import{shellQuote as w}from"../shared/shell-quote.js";class f extends Error{static{o(this,"WorktreeError")}code;next;detail;constructor(r,t,n,a){super(t),this.name="WorktreeError",this.code=r,this.next=n,this.detail=a}}function _(e,r,t,n){const a=i(r,e);if(a.status!==0){const s=(a.stderr.trim()||a.stdout.trim()).trim();throw new f(t,`git ${r.join(" ")} failed`,n,s)}return a.stdout.trim()}o(_,"runGit");function F(){return`${C()}/worktrees`}o(F,"managedWorktreesRoot");function ge(e){return`${F()}/${e}`}o(ge,"managedWorktreePath");function Xe(e){return e?.managed_worktree?.state==="open"}o(Xe,"hasOpenManagedWorktree");async function x(e){const t=(await v(e))?.managed_worktree;return t?.state==="open"?t:null}o(x,"openManagedWorktreeForNode");function we(e){const r=i(["rev-parse","--show-toplevel"],e);if(r.status!==0){const t=(r.stderr.trim()||r.stdout.trim()).trim();throw new f("not_git_repo",`cwd ${e} is not a git repository`,"Pass --cwd <real repo> pointing inside a git repository.",t)}return r.stdout.trim()}o(we,"repoRootFor");function ye(e){const r=ie(E(F()),E(e));return r!==""&&r!==".."&&!r.startsWith(`..${ce}`)&&!ae(r)}o(ye,"isManagedWorktreeBase");function $e(e,r){const t=`refs/heads/${r}`,n=i(["show-ref","--verify","--quiet",t],e);if(n.status===0)return!0;if(n.status===1&&n.executionFailed!==!0)return!1;const a=n.stderr.trim()||"<empty>",s=n.processError===void 0?"":`
1
+ var Z=Object.defineProperty;var o=(e,r)=>Z(e,"name",{value:r,configurable:!0});import{existsSync as k,mkdirSync as H,readFileSync as ee,realpathSync as N,rmSync as te,writeFileSync as re}from"node:fs";import{createHash as ne}from"node:crypto";import{dirname as oe,isAbsolute as ae,join as se,relative as ie,resolve as C,sep as ce}from"node:path";import{gitAsync as he,gitSync as i}from"./git.js";import{crtrHome as O}from"./canvas/paths.js";import{closeWorktreeAndRepoint as ue,getNode as v,recordLandIntent as le}from"./canvas/index.js";import{CONFIG_FILE as de,CRTR_DIR_NAME as A,STATE_FILE as fe}from"../types.js";import{withExclusiveDirectoryLock as pe,withExclusiveDirectoryLockAsync as me}from"./exclusive-lock.js";import{parseStatusPorcelain as be,planLandIntoCheckout as ke,refDeleteTransactionStdin as _e}from"./worktree-landing.js";import{shellQuote as w}from"../shared/shell-quote.js";class f extends Error{static{o(this,"WorktreeError")}code;next;detail;constructor(r,t,n,a){super(t),this.name="WorktreeError",this.code=r,this.next=n,this.detail=a}}function _(e,r,t,n){const a=i(r,e);if(a.status!==0){const s=(a.stderr.trim()||a.stdout.trim()).trim();throw new f(t,`git ${r.join(" ")} failed`,n,s)}return a.stdout.trim()}o(_,"runGit");function F(){return`${O()}/worktrees`}o(F,"managedWorktreesRoot");function ge(e){return`${F()}/${e}`}o(ge,"managedWorktreePath");function Xe(e){return e?.managed_worktree?.state==="open"}o(Xe,"hasOpenManagedWorktree");async function x(e){const t=(await v(e))?.managed_worktree;return t?.state==="open"?t:null}o(x,"openManagedWorktreeForNode");function we(e){const r=i(["rev-parse","--show-toplevel"],e);if(r.status!==0){const t=(r.stderr.trim()||r.stdout.trim()).trim();throw new f("not_git_repo",`cwd ${e} is not a git repository`,"Pass --cwd <real repo> pointing inside a git repository.",t)}return r.stdout.trim()}o(we,"repoRootFor");function ye(e){const r=ie(E(F()),E(e));return r!==""&&r!==".."&&!r.startsWith(`..${ce}`)&&!ae(r)}o(ye,"isManagedWorktreeBase");function $e(e,r){const t=`refs/heads/${r}`,n=i(["show-ref","--verify","--quiet",t],e);if(n.status===0)return!0;if(n.status===1&&n.executionFailed!==!0)return!1;const a=n.stderr.trim()||"<empty>",s=n.processError===void 0?"":`
2
2
  process error: ${n.processError}`;throw new f("recorded_branch_inspection_failed",`could not inspect recorded branch '${r}' (git show-ref exited with status ${n.status})`,"Inspect the repository and its Git metadata, then retry.",`status: ${n.status}
3
- stderr: ${a}${s}`)}o($e,"recordedBranchExists");function A(e,r){const t=`refs/heads/${r}`,n=i(["show-ref","--verify","--quiet",t],e);if(n.status===1&&n.executionFailed!==!0)throw new f("missing_base_branch",`managed worktree base '${r}' does not name an existing local branch`,"The base must be a local branch that landing can fast-forward \u2014 freshen your local branch from origin first (rebase-not-merge), then base on it.");if(n.status!==0){const a=n.stderr.trim()||"<empty>",s=n.processError===void 0?"":`
3
+ stderr: ${a}${s}`)}o($e,"recordedBranchExists");function R(e,r){const t=`refs/heads/${r}`,n=i(["show-ref","--verify","--quiet",t],e);if(n.status===1&&n.executionFailed!==!0)throw new f("missing_base_branch",`managed worktree base '${r}' does not name an existing local branch`,"The base must be a local branch that landing can fast-forward \u2014 freshen your local branch from origin first (rebase-not-merge), then base on it.");if(n.status!==0){const a=n.stderr.trim()||"<empty>",s=n.processError===void 0?"":`
4
4
  process error: ${n.processError}`;throw new f("base_branch_inspection_failed",`could not inspect local base branch '${r}' (git show-ref exited with status ${n.status})`,"Inspect the repository and its Git metadata, then retry.",`status: ${n.status}
5
- stderr: ${a}${s}`)}return _(e,["rev-parse","--verify",t],"base_branch_resolve_failed","Inspect the local base branch, then retry.")}o(A,"resolveLandableBase");function ve(e,r){const t=_(e,["worktree","list","--porcelain"],"worktree_list_failed","Inspect the repository worktrees manually, then retry.");return I(t,r)}o(ve,"checkoutPathForBranch");function I(e,r){let t=null;for(const n of e.split(`
6
- `))if(n.startsWith("worktree "))t=n.slice(9).trim();else if(n===`branch refs/heads/${r}`)return t;return null}o(I,"checkoutPathForBranchInList");function xe(e,r){return e===r||E(e)===E(r)}o(xe,"samePath");function E(e){try{return H.native(e)}catch{return N(e)}}o(E,"canonicalPath");const q=3e4;function G(e){const r=`${C()}/worktree-locks`;return P(r,{recursive:!0}),`${r}/${ne("sha256").update(e).digest("hex")}`}o(G,"worktreeLockPathForCommonDir");function S(e){const r=_(e,["rev-parse","--git-common-dir"],"git_common_dir_failed","Inspect the repository git metadata, then retry.");return H(N(e,r))}o(S,"commonGitDir");function B(e,r){const t=S(e);return pe(G(t),r,{timeoutMs:q,timeoutError:o(()=>new f("worktree_lock_timeout",`timed out waiting to create a managed worktree in ${t}`,"Wait for the other managed-worktree operation to finish, then retry."),"timeoutError")})}o(B,"withRepositoryWorktreeLock");async function j(e,r){const t=S(e);return me(G(t),r,{timeoutMs:q,timeoutError:o(()=>new f("worktree_lock_timeout",`timed out waiting to create a managed worktree in ${t}`,"Wait for the other managed-worktree operation to finish, then retry."),"timeoutError")})}o(j,"withRepositoryWorktreeLockAsync");function D(e,r,t){const n=i(["worktree","list","--porcelain"],e);if(n.status!==0)return null;let a=null;for(const s of n.stdout.split(`
5
+ stderr: ${a}${s}`)}return _(e,["rev-parse","--verify",t],"base_branch_resolve_failed","Inspect the local base branch, then retry.")}o(R,"resolveLandableBase");function ve(e,r){const t=_(e,["worktree","list","--porcelain"],"worktree_list_failed","Inspect the repository worktrees manually, then retry.");return I(t,r)}o(ve,"checkoutPathForBranch");function I(e,r){let t=null;for(const n of e.split(`
6
+ `))if(n.startsWith("worktree "))t=n.slice(9).trim();else if(n===`branch refs/heads/${r}`)return t;return null}o(I,"checkoutPathForBranchInList");function xe(e,r){return e===r||E(e)===E(r)}o(xe,"samePath");function E(e){try{return N.native(e)}catch{return C(e)}}o(E,"canonicalPath");const q=3e4;function G(e){const r=`${O()}/worktree-locks`;return H(r,{recursive:!0}),`${r}/${ne("sha256").update(e).digest("hex")}`}o(G,"worktreeLockPathForCommonDir");function S(e){const r=_(e,["rev-parse","--git-common-dir"],"git_common_dir_failed","Inspect the repository git metadata, then retry.");return N(C(e,r))}o(S,"commonGitDir");function B(e,r){const t=S(e);return pe(G(t),r,{timeoutMs:q,timeoutError:o(()=>new f("worktree_lock_timeout",`timed out waiting to create a managed worktree in ${t}`,"Wait for the other managed-worktree operation to finish, then retry."),"timeoutError")})}o(B,"withRepositoryWorktreeLock");async function j(e,r){const t=S(e);return me(G(t),r,{timeoutMs:q,timeoutError:o(()=>new f("worktree_lock_timeout",`timed out waiting to create a managed worktree in ${t}`,"Wait for the other managed-worktree operation to finish, then retry."),"timeoutError")})}o(j,"withRepositoryWorktreeLockAsync");function D(e,r,t){const n=i(["worktree","list","--porcelain"],e);if(n.status!==0)return null;let a=null;for(const s of n.stdout.split(`
7
7
  `))if(s.startsWith("worktree "))a=s.slice(9).trim();else if(a!==null&&xe(a,r)&&s===`branch refs/heads/${t}`)return!0;return!1}o(D,"registeredWorktree");function Ee(e,r,t){const n=[],a=`refs/heads/${r}`,s=i(["rev-parse","--verify","--quiet",a],e);s.status===0&&n.push({ref:a,sha:s.stdout.trim()});const u=i(["rev-parse","--symbolic-full-name","--verify",`${r}@{upstream}`],e),h=u.status===0&&u.stdout.trim().startsWith("refs/")?u.stdout.trim():`refs/remotes/origin/${r}`,m=i(["rev-parse","--verify","--quiet",h],e);if(m.status===0&&n.push({ref:h,sha:m.stdout.trim()}),n.length===0)return null;for(const d of n)if(i(["merge-base","--is-ancestor",t,d.sha],e).status===0)return{containingRef:d.ref,containingSha:d.sha};for(const d of n){const l=i(["cherry",d.sha,t],e);if(l.status!==0)continue;if(l.stdout.split(`
8
8
  `).map(c=>c.trim()).filter(c=>c!=="").every(c=>c.startsWith("-")))return{containingRef:d.ref,containingSha:d.sha}}for(const d of n){const l=i(["rev-parse","--verify",`${d.sha}^{tree}`],e);if(l.status!==0)continue;const b=i(["merge-tree","--write-tree",d.sha,t],e);if(b.status===0&&b.stdout.split(`
9
- `)[0]?.trim()===l.stdout.trim())return{containingRef:d.ref,containingSha:d.sha}}return null}o(Ee,"proveBranchContained");function K(e,r){const t=i(["rev-parse","--verify",`refs/heads/${e.branch}`],e.repo_root);if(t.status!==0||t.stdout.trim()!==e.base_sha)return;const n=D(e.repo_root,e.path,e.branch);if(n!==null){if(n){if(i(["worktree","remove","--force",e.path],e.repo_root).status!==0||D(e.repo_root,e.path,e.branch)!==!1)return}else if(r&&k(e.path))try{te(e.path,{recursive:!0,force:!0})}catch{return}try{i(["update-ref","-d",`refs/heads/${e.branch}`,e.base_sha],e.repo_root)}catch{}}}o(K,"rollbackManagedWorktreeLocked");function ze(e){B(e.repo_root,()=>K(e,!1))}o(ze,"rollbackManagedWorktree");const Se="# crtr: exclude crtr's own untracked project-scope bookkeeping (auto-added at managed-worktree creation)",De=[`/${O}/${de}`,`/${O}/${fe}`];function We(e){try{const r=se(S(e),"info","exclude");P(oe(r),{recursive:!0});let t="";try{t=ee(r,"utf8")}catch{}const n=new Set(t.split(`
9
+ `)[0]?.trim()===l.stdout.trim())return{containingRef:d.ref,containingSha:d.sha}}return null}o(Ee,"proveBranchContained");function K(e,r){const t=i(["rev-parse","--verify",`refs/heads/${e.branch}`],e.repo_root);if(t.status!==0||t.stdout.trim()!==e.base_sha)return;const n=D(e.repo_root,e.path,e.branch);if(n!==null){if(n){if(i(["worktree","remove","--force",e.path],e.repo_root).status!==0||D(e.repo_root,e.path,e.branch)!==!1)return}else if(r&&k(e.path))try{te(e.path,{recursive:!0,force:!0})}catch{return}try{i(["update-ref","-d",`refs/heads/${e.branch}`,e.base_sha],e.repo_root)}catch{}}}o(K,"rollbackManagedWorktreeLocked");function ze(e){B(e.repo_root,()=>K(e,!1))}o(ze,"rollbackManagedWorktree");const Se="# crtr: exclude crtr's own untracked project-scope bookkeeping (auto-added at managed-worktree creation)",De=[`/${A}/${de}`,`/${A}/${fe}`,`/${A}/operations.db*`];function We(e){try{const r=se(S(e),"info","exclude");H(oe(r),{recursive:!0});let t="";try{t=ee(r,"utf8")}catch{}const n=new Set(t.split(`
10
10
  `).map(h=>h.trim())),a=De.filter(h=>!n.has(h));if(a.length===0)return;const u=(t.length>0&&!t.endsWith(`
11
11
  `)?`
12
12
  `:"")+Se+`
13
13
  `+a.join(`
14
14
  `)+`
15
- `;re(r,t+u)}catch{}}o(We,"seedManagedWorktreeExclude");function Qe(e,r,t="main"){const n=we(e);if(ye(n))throw new f("managed_worktree_base",`cwd ${n} is a managed worktree and cannot be used as a managed worktree base`,"Pass `--cwd` pointing at the repository\u2019s primary checkout and keep `--worktree` set to the desired existing local base branch, including the parent branch.");return B(n,()=>{const a=A(n,t),s=ge(r),u=`crtr/${r}`;if(k(s))throw new f("worktree_path_exists",`managed worktree path already exists: ${s}`,"Remove the stale path or choose a fresh node id.");if(i(["show-ref","--verify","--quiet",`refs/heads/${u}`],n).status===0)throw new f("worktree_branch_exists",`managed worktree branch already exists: ${u}`,"Remove the stale branch or choose a fresh node id.");const h={state:"open",path:s,branch:u,repo_root:n,base_ref:t,base_sha:a,created:new Date().toISOString()};_(n,["branch",u,a],"worktree_branch_create_failed","Inspect the git error, fix the repository/worktree state, then retry spawning with --worktree.");try{return _(n,["worktree","add",s,u],"worktree_add_failed","Inspect the git error, fix the repository/worktree state, then retry spawning with --worktree."),We(n),h}catch(m){throw K(h,!0),m}})}o(Qe,"createManagedWorktree");function Ae(e){return _(e,["status","--porcelain"],"status_failed","Inspect the worktree manually, then retry.")}o(Ae,"statusPorcelain");function U(e){return _(e,["rev-parse","--abbrev-ref","HEAD"],"branch_check_failed","Inspect the worktree manually, then retry.")}o(U,"currentBranch");function X(e,r){return _(e,["rev-parse","--git-path",r],"git_path_failed","Inspect the worktree manually, then retry.")}o(X,"gitPath");function R(e){return k(X(e,"rebase-merge"))||k(X(e,"rebase-apply"))}o(R,"isRebaseInProgress");async function $(e,r,t="pending"){const n=await v(e);await ue(e,{cwd:r.repo_root,cleanup:t,closedAt:new Date().toISOString(),unbindSession:n?.pi_session_id!=null||n?.pi_session_file!=null})}o($,"persistClosedWorktreeState");async function Re(e,r){const t=(await v(e))?.managed_worktree;t==null||t.state!=="open"||await le(e,{at:new Date().toISOString(),blocked:r})}o(Re,"recordBlockedLandIntent");function Me(e,r,t,n){const a=i(["status","--porcelain"],e);if(a.status!==0)return{ok:!1,detail:(a.stderr.trim()||a.stdout.trim()).trim()};const s=i(["diff","--name-only",t,n],e);if(s.status!==0)return{ok:!1,detail:(s.stderr.trim()||s.stdout.trim()).trim()};const u=s.stdout.split(`
15
+ `;re(r,t+u)}catch{}}o(We,"seedManagedWorktreeExclude");function Qe(e,r,t="main"){const n=we(e);if(ye(n))throw new f("managed_worktree_base",`cwd ${n} is a managed worktree and cannot be used as a managed worktree base`,"Pass `--cwd` pointing at the repository\u2019s primary checkout and keep `--worktree` set to the desired existing local base branch, including the parent branch.");return B(n,()=>{const a=R(n,t),s=ge(r),u=`crtr/${r}`;if(k(s))throw new f("worktree_path_exists",`managed worktree path already exists: ${s}`,"Remove the stale path or choose a fresh node id.");if(i(["show-ref","--verify","--quiet",`refs/heads/${u}`],n).status===0)throw new f("worktree_branch_exists",`managed worktree branch already exists: ${u}`,"Remove the stale branch or choose a fresh node id.");const h={state:"open",path:s,branch:u,repo_root:n,base_ref:t,base_sha:a,created:new Date().toISOString()};_(n,["branch",u,a],"worktree_branch_create_failed","Inspect the git error, fix the repository/worktree state, then retry spawning with --worktree.");try{return _(n,["worktree","add",s,u],"worktree_add_failed","Inspect the git error, fix the repository/worktree state, then retry spawning with --worktree."),We(n),h}catch(m){throw K(h,!0),m}})}o(Qe,"createManagedWorktree");function Ae(e){return _(e,["status","--porcelain"],"status_failed","Inspect the worktree manually, then retry.")}o(Ae,"statusPorcelain");function U(e){return _(e,["rev-parse","--abbrev-ref","HEAD"],"branch_check_failed","Inspect the worktree manually, then retry.")}o(U,"currentBranch");function X(e,r){return _(e,["rev-parse","--git-path",r],"git_path_failed","Inspect the worktree manually, then retry.")}o(X,"gitPath");function M(e){return k(X(e,"rebase-merge"))||k(X(e,"rebase-apply"))}o(M,"isRebaseInProgress");async function $(e,r,t="pending"){const n=await v(e);await ue(e,{cwd:r.repo_root,cleanup:t,closedAt:new Date().toISOString(),unbindSession:n?.pi_session_id!=null||n?.pi_session_file!=null})}o($,"persistClosedWorktreeState");async function Re(e,r){const t=(await v(e))?.managed_worktree;t==null||t.state!=="open"||await le(e,{at:new Date().toISOString(),blocked:r})}o(Re,"recordBlockedLandIntent");function Me(e,r,t,n){const a=i(["status","--porcelain"],e);if(a.status!==0)return{ok:!1,detail:(a.stderr.trim()||a.stdout.trim()).trim()};const s=i(["diff","--name-only",t,n],e);if(s.status!==0)return{ok:!1,detail:(s.stderr.trim()||s.stdout.trim()).trim()};const u=s.stdout.split(`
16
16
  `).map(g=>g.trim()).filter(g=>g!==""),h=ke(be(a.stdout),u);if(h.kind==="refuse")return{ok:!1,detail:`untracked files in ${e} would be overwritten by the incoming commits: ${h.collisions.join(", ")}`};const m=o(()=>{const g=i(["merge","--ff-only",n],e);return g.status===0?{ok:!0}:{ok:!1,detail:(g.stderr.trim()||g.stdout.trim()).trim()}},"merge");if(h.kind==="direct")return m();const d=i(["rev-parse","--verify","--quiet","refs/stash"],e),l=d.status===0?d.stdout.trim():null,b=i(["stash","push","--message",`crtr landing ${r}`],e);if(b.status!==0)return{ok:!1,detail:(b.stderr.trim()||b.stdout.trim()).trim()};const c=i(["rev-parse","--verify","--quiet","refs/stash"],e),p=c.status===0?c.stdout.trim():null;if(p===null||p===l)return m();const y=`local changes from ${e} are retained in stash ${p}; recover them with \`git -C ${w(e)} stash apply ${p}\` and resolve any conflict.`,W=m();return W.ok?i(["stash","apply",p],e).status===0?(z(e,p),{ok:!0}):(i(["reset","--hard",n],e),{ok:!0,stash:`the shared base checkout was reset to landed commit ${n} because local changes conflicted with it. ${y}`}):i(["stash","apply",p],e).status===0?(z(e,p),W):{ok:!1,detail:`${W.detail}
17
- ${y}`}}o(Me,"landIntoBaseCheckout");function z(e,r){const t=i(["rev-parse","--verify","--quiet","refs/stash"],e);t.status===0&&t.stdout.trim()===r&&i(["stash","drop"],e)}o(z,"dropStashIfStillTip");function M(e){const r=`git -C ${w(e.repo_root)} worktree remove ${w(e.path)}`;return{worktree_removed:!1,worktree_remove_error:`cleanup pending: the caller checkout at ${e.path} remains intact. After its node exits, run \`${r}\` after confirming it is safe to delete.`,branch_deleted:!1,branch_delete_error:`cleanup pending: local branch ${e.branch} remains checked out at ${e.path}; delete it only after that checkout is removed and its landed commit is still contained in ${e.base_ref}.`}}o(M,"deferredCleanupResult");async function Le(e,r,t){if(D(r.repo_root,r.path,t)!==!0)throw new f("wrong_branch",`managed worktree's recorded branch ${r.branch} no longer exists, and its checkout at ${r.path} is not registered to ${t} either`,`Inspect ${r.path} and its git worktree registration, then ask the user with \`crtr human send\` if you are unsure how to proceed.`);const n=_(r.path,["rev-parse","HEAD"],"branch_check_failed","Inspect the worktree manually, then retry.");if(Ee(r.path,r.base_ref,n)===null)throw new f("branch_moved_unlanded",`managed worktree's recorded branch ${r.branch} no longer exists; its checkout is now on ${t} at ${n}, which carries content local base branch '${r.base_ref}' does not yet contain`,`Land or push ${t} through its own workflow (merge its PR, or fast-forward '${r.base_ref}' to include it) before closing, or ask the user with \`crtr human send\` if you are unsure how ${t} should be delivered.`);const s=S(r.repo_root),u=D(r.repo_root,r.path,t)===!0,h=U(r.path)===t,m=_(r.path,["rev-parse","HEAD"],"branch_check_failed","Inspect the worktree manually, then retry.")===n;if(!u||!h||!m)throw new f("wrong_branch",`managed worktree at ${r.path} changed while closing`,`Inspect ${r.path}, then rerun \`crtr node worktree close\`.`);const d={observed_branch:t,observed_sha:n,common_dir:s,authorized:new Date().toISOString()};await $(e,{...r,disposal:d},"pending");const l=`git -C ${w(r.repo_root)} worktree remove ${w(r.path)}`;return{node_id:e,branch:t,worktree_path:r.path,landed_sha:n,worktree_removed:!1,worktree_remove_error:`cleanup pending: the caller checkout at ${r.path} remains intact. After its node exits, run \`${l}\` after confirming it is safe to delete.`,branch_deleted:!1,branch_delete_error:`${t} is not a crtr-managed branch and will never be deleted automatically; it is preserved once the checkout above is removed.`}}o(Le,"closeBranchMovedWorktree");async function Te(e,r){const t=(await v(e))?.managed_worktree;if(t==null)return{message:`node ${e} has no managed worktree`,next:`Nothing to ${r} for this node.`};if(t.state==="abandoned"){const n=t.abandoned?.branch??t.branch;return{message:`node ${e}'s managed worktree was already abandoned`,next:`Nothing to ${r} for this node; its work was retained on branch ${n}.`}}return t.state==="closed"?t.cleanup==="complete"?{message:`node ${e}'s managed worktree is already closed and its cleanup is complete`,next:`Nothing to ${r} for this node.`}:{message:`node ${e}'s managed worktree is already closed; this is not lost ownership \u2014 its checkout at ${t.path} is scheduled for automatic cleanup once that is safe`,next:`Nothing to ${r} for this node. The daemon sweep finishes removing ${t.path} on its own; inspect it directly only if it still needs manual attention.`}:{message:`node ${e} has no open managed worktree`,next:`Nothing to ${r} for this node.`}}o(Te,"noManagedWorktreeMessage");async function Q(e){if(await v(e)===null)throw new f("unknown_node",`unknown node: ${e}`,"Run this from a live node, or pass a real node id once that command supports it.");const t=await x(e);if(t===null){const{message:c,next:p}=await Te(e,"close");throw new f("no_managed_worktree",c,p)}if(!k(t.path)){const c=L(t);if(c===null)throw new f("worktree_missing",`managed worktree path is missing: ${t.path}`,J(t));if(await V(e,t,c)===null)throw new f("worktree_missing",`managed worktree path is missing: ${t.path}`,J(t));return{node_id:e,branch:t.branch,worktree_path:t.path,landed_sha:c.branchSha??t.base_sha,worktree_removed:!0,branch_deleted:!0}}if(R(t.path))throw new f("rebase_in_progress","this managed worktree is mid-rebase",`Resolve conflicts in ${t.path}, run \`git rebase --continue\` there, then rerun \`crtr node worktree close\`.`);if(Ae(t.path)!=="")throw new f("dirty_worktree","managed worktree has uncommitted changes",`Commit the work in ${t.path} (or ask the user with \`crtr human send\` if you do not know whether to keep it), then rerun \`crtr node worktree close\`.`);const a=U(t.path);if(a!==t.branch){const c=a==="HEAD"?"detached HEAD":a,p=$e(t.path,t.branch);if(p||a==="HEAD")throw new f("wrong_branch",`managed worktree is not on its recorded branch (expected ${t.branch}, found ${c})`,p?`Check out ${t.branch} in ${t.path} before closing, or ask the user with \`crtr human send\` if you are unsure why it changed, then rerun \`crtr node worktree close\`.`:`The recorded branch ${t.branch} no longer exists, and ${t.path} is in detached HEAD, so there is no branch to preserve. Check out or create a branch there before closing, or ask the user with \`crtr human send\` if you are unsure how to proceed.`);return await Le(e,t,a)}let s;try{s=A(t.path,t.base_ref)}catch(c){const p=_(t.path,["rev-parse","HEAD"],"landed_sha_failed","Inspect the worktree checkout manually, then rerun `crtr node worktree close`.");if(!(c instanceof f)||c.code!=="missing_base_branch"||p!==t.base_sha)throw c;return await $(e,t,"pending"),{node_id:e,branch:t.branch,worktree_path:t.path,landed_sha:t.base_sha,...M(t)}}const u=i(["rebase",s],t.path);if(u.status!==0){const c=(u.stderr.trim()||u.stdout.trim()).trim();throw new f("rebase_failed",`rebase onto local base branch '${t.base_ref}' did not complete`,`Resolve conflicts in ${t.path}, run \`git rebase --continue\` there, then rerun \`crtr node worktree close\`. If the blocker is not mechanical, ask the user with \`crtr human send\`.`,c)}const h=_(t.path,["rev-parse","HEAD"],"landed_sha_failed","Inspect the worktree checkout manually, then rerun `crtr node worktree close`."),m=o(async(c,p,y)=>(await Re(e,c),new f("land_base_failed",c,p,y)),"blocked"),d=i(["merge-base","--is-ancestor",s,h],t.path);if(d.status!==0)throw await m(`rebased worktree is not a fast-forward of local base branch '${t.base_ref}'`,"Ask the user with `crtr human send` how to land this managed worktree.",(d.stderr.trim()||d.stdout.trim()).trim());const l=ve(t.path,t.base_ref);if(l!==null){const c=i(["rev-parse","HEAD"],l),p=c.stdout.trim();if(c.status!==0||p!==s){const y=c.status!==0?(c.stderr.trim()||p).trim():`expected ${s}, found ${p}`;throw await m(`local base branch '${t.base_ref}' changed while this worktree was closing`,`Reconcile the base checkout at ${l}, then rerun \`crtr node worktree close\`. If you are unsure, ask the user with \`crtr human send\`.`,y)}}let b;if(l!==null){const c=Me(l,t.branch,s,h);if(!c.ok)throw await m(`could not fast-forward local base branch '${t.base_ref}' to this worktree`,`Local base branch '${t.base_ref}' is checked out at ${l} in a state that blocks a fast-forward. Reconcile it there, then rerun \`crtr node worktree close\`. If you are unsure, ask the user with \`crtr human send\`.`,c.detail);b=c.stash}else{const c=i(["update-ref",`refs/heads/${t.base_ref}`,h,s],t.path);if(c.status!==0)throw await m(`could not fast-forward local base branch '${t.base_ref}' to this worktree`,"Ask the user with `crtr human send` how to land this managed worktree.",(c.stderr.trim()||c.stdout.trim()).trim())}return await $(e,t,"pending"),{node_id:e,branch:t.branch,worktree_path:t.path,landed_sha:h,...M(t),...b===void 0?{}:{base_checkout_stash:b}}}o(Q,"closeManagedWorktreeLocked");async function Je(e){const r=await x(e);return r===null?await Q(e):await j(k(r.path)?r.path:r.repo_root,async()=>await Q(e))}o(Je,"closeManagedWorktree");function Pe(e,r,t,n){const a=_e(e.branch,r,t,n),s=i(["update-ref","--stdin"],e.repo_root,a),u=s.status===0;if(u)return{branchDeleted:u};const h=(s.stderr.trim()||s.stdout.trim()).trim();return{branchDeleted:u,branchDeleteError:`local branch ${e.branch} could not be deleted automatically (${h}); run \`git -C ${w(e.repo_root)} branch -D ${w(e.branch)}\` after confirming it is merged.`}}o(Pe,"bestEffortDeleteBranchWithContainmentProof");function L(e){if(e.state!=="open"||k(e.path))return null;const r=`refs/heads/${e.branch}`,t=i(["show-ref","--verify","--quiet",r],e.repo_root);if(t.status===1)return{branchSha:null};if(t.status!==0)return null;const n=i(["rev-parse","--verify",r],e.repo_root);if(n.status!==0)return null;const a=n.stdout.trim(),s=i(["worktree","list","--porcelain"],e.repo_root);if(s.status!==0||I(s.stdout,e.branch)!==null)return null;const u=[],h=`refs/heads/${e.base_ref}`,m=i(["show-ref","--verify","--quiet",h],e.repo_root);if(m.status===0){const l=i(["rev-parse","--verify",h],e.repo_root);if(l.status!==0)return null;u.push({ref:h,sha:l.stdout.trim()})}else if(m.status!==1)return null;const d=i(["rev-parse","--symbolic-full-name","--verify",`${e.base_ref}@{upstream}`],e.repo_root);if(d.status===0){const l=d.stdout.trim(),b=i(["rev-parse","--verify",l],e.repo_root);l.startsWith("refs/")&&b.status===0&&u.push({ref:l,sha:b.stdout.trim()})}else{const l=`refs/remotes/origin/${e.base_ref}`,b=i(["show-ref","--verify","--quiet",l],e.repo_root);if(b.status===0){const c=i(["rev-parse","--verify",l],e.repo_root);c.status===0&&u.push({ref:l,sha:c.stdout.trim()})}else if(b.status!==1)return null}for(const l of u)if(i(["merge-base","--is-ancestor",a,l.sha],e.repo_root).status===0)return{branchSha:a,containingRef:l.ref,containingSha:l.sha};return null}o(L,"proveAbsentPathManagedWorktree");function J(e){return`The recorded checkout path is gone, but crtr cannot prove ${e.branch} is already delivered. Push or land ${e.branch}, then rerun \`crtr push final\`; if you cannot establish what happened to the branch, ask the user with \`crtr human send\`.`}o(J,"missingWorktreeRecoveryHint");function T(e){if(e.state!=="open"||!k(e.path)||R(e.path))return null;const r=i(["rev-parse","--abbrev-ref","HEAD"],e.path);if(r.status!==0||r.stdout.trim()!==e.branch)return null;const t=i(["status","--porcelain"],e.path);if(t.status!==0||t.stdout.trim()!=="")return null;const n=i(["rev-parse","HEAD"],e.path);if(n.status!==0)return null;const a=n.stdout.trim();let s;try{s=A(e.path,e.base_ref)}catch(h){if(h instanceof f&&h.code==="missing_base_branch"&&a===e.base_sha)return{branchSha:a,baseSha:e.base_sha};throw h}return i(["merge-base","--is-ancestor",a,s],e.path).status!==0?null:{branchSha:a,baseSha:s}}o(T,"proveCleanEmptyManagedWorktree");async function V(e,r,t){if(k(r.path))return null;if(t.branchSha===null)return await $(e,r,"complete"),{node_id:e,branch:r.branch,worktree_path:r.path,worktree_removed:!0,branch_deleted:!0};if(t.containingRef===void 0||t.containingSha===void 0)return null;const{branchDeleted:n}=Pe(r,t.branchSha,t.containingRef,t.containingSha);return n?(await $(e,r,"complete"),{node_id:e,branch:r.branch,worktree_path:r.path,worktree_removed:!0,branch_deleted:!0}):null}o(V,"autoDropAbsentPathManagedWorktreeLocked");async function He(e,r,t){const n=T(r);return n===null||n.branchSha!==t.branchSha?null:(await $(e,r,"pending"),{node_id:e,branch:r.branch,worktree_path:r.path,...M(r)})}o(He,"autoDropCleanManagedWorktreeLocked");function Y(e){if(e.state!=="open"||!k(e.path)||R(e.path))return null;const r=i(["status","--porcelain"],e.path);if(r.status!==0||r.stdout.trim()!=="")return null;const t=i(["rev-parse","HEAD"],e.path);return t.status===0?t.stdout.trim():null}o(Y,"cleanCurrentCheckoutTip");async function Ve(e){const r=Y(e);if(r===null)return!1;const t=process.env.GIT_SSH_COMMAND??process.env.GIT_SSH??"ssh",n=await he(["ls-remote","--exit-code","--heads","origin"],e.path,void 0,{timeoutMs:1e4,env:{...process.env,GIT_TERMINAL_PROMPT:"0",GIT_SSH_COMMAND:`${t} -o BatchMode=yes`}});return n.status!==0||!n.stdout.split(`
18
- `).some(s=>{const[u,h]=s.split(" ",2);return u===r&&h?.startsWith("refs/heads/")===!0})?!1:Y(e)===r}o(Ve,"branchIsPushedAtCurrentTip");async function Ye(e){const r=await x(e);if(r===null)return null;const t=!k(r.path);return(t?L(r)===null:T(r)===null)?null:await j(t?r.repo_root:r.path,async()=>{const n=await x(e);if(n===null)return null;if(!k(n.path)){const s=L(n);return s===null?null:await V(e,n,s)}const a=T(n);return a===null?null:await He(e,n,a)})}o(Ye,"autoDropCleanManagedWorktree");export{q as WORKTREE_LOCK_WAIT_MS,f as WorktreeError,Ye as autoDropCleanManagedWorktree,He as autoDropCleanManagedWorktreeLocked,Ve as branchIsPushedAtCurrentTip,I as checkoutPathForBranchInList,Je as closeManagedWorktree,Qe as createManagedWorktree,Xe as hasOpenManagedWorktree,ye as isManagedWorktreeBase,R as isRebaseInProgress,ge as managedWorktreePath,F as managedWorktreesRoot,J as missingWorktreeRecoveryHint,x as openManagedWorktreeForNode,$ as persistClosedWorktreeState,T as proveCleanEmptyManagedWorktree,ze as rollbackManagedWorktree,xe as samePath,G as worktreeLockPathForCommonDir};
17
+ ${y}`}}o(Me,"landIntoBaseCheckout");function z(e,r){const t=i(["rev-parse","--verify","--quiet","refs/stash"],e);t.status===0&&t.stdout.trim()===r&&i(["stash","drop"],e)}o(z,"dropStashIfStillTip");function L(e){const r=`git -C ${w(e.repo_root)} worktree remove ${w(e.path)}`;return{worktree_removed:!1,worktree_remove_error:`cleanup pending: the caller checkout at ${e.path} remains intact. After its node exits, run \`${r}\` after confirming it is safe to delete.`,branch_deleted:!1,branch_delete_error:`cleanup pending: local branch ${e.branch} remains checked out at ${e.path}; delete it only after that checkout is removed and its landed commit is still contained in ${e.base_ref}.`}}o(L,"deferredCleanupResult");async function Le(e,r,t){if(D(r.repo_root,r.path,t)!==!0)throw new f("wrong_branch",`managed worktree's recorded branch ${r.branch} no longer exists, and its checkout at ${r.path} is not registered to ${t} either`,`Inspect ${r.path} and its git worktree registration, then ask the user with \`crtr human send\` if you are unsure how to proceed.`);const n=_(r.path,["rev-parse","HEAD"],"branch_check_failed","Inspect the worktree manually, then retry.");if(Ee(r.path,r.base_ref,n)===null)throw new f("branch_moved_unlanded",`managed worktree's recorded branch ${r.branch} no longer exists; its checkout is now on ${t} at ${n}, which carries content local base branch '${r.base_ref}' does not yet contain`,`Land or push ${t} through its own workflow (merge its PR, or fast-forward '${r.base_ref}' to include it) before closing, or ask the user with \`crtr human send\` if you are unsure how ${t} should be delivered.`);const s=S(r.repo_root),u=D(r.repo_root,r.path,t)===!0,h=U(r.path)===t,m=_(r.path,["rev-parse","HEAD"],"branch_check_failed","Inspect the worktree manually, then retry.")===n;if(!u||!h||!m)throw new f("wrong_branch",`managed worktree at ${r.path} changed while closing`,`Inspect ${r.path}, then rerun \`crtr node worktree close\`.`);const d={observed_branch:t,observed_sha:n,common_dir:s,authorized:new Date().toISOString()};await $(e,{...r,disposal:d},"pending");const l=`git -C ${w(r.repo_root)} worktree remove ${w(r.path)}`;return{node_id:e,branch:t,worktree_path:r.path,landed_sha:n,worktree_removed:!1,worktree_remove_error:`cleanup pending: the caller checkout at ${r.path} remains intact. After its node exits, run \`${l}\` after confirming it is safe to delete.`,branch_deleted:!1,branch_delete_error:`${t} is not a crtr-managed branch and will never be deleted automatically; it is preserved once the checkout above is removed.`}}o(Le,"closeBranchMovedWorktree");async function Te(e,r){const t=(await v(e))?.managed_worktree;if(t==null)return{message:`node ${e} has no managed worktree`,next:`Nothing to ${r} for this node.`};if(t.state==="abandoned"){const n=t.abandoned?.branch??t.branch;return{message:`node ${e}'s managed worktree was already abandoned`,next:`Nothing to ${r} for this node; its work was retained on branch ${n}.`}}return t.state==="closed"?t.cleanup==="complete"?{message:`node ${e}'s managed worktree is already closed and its cleanup is complete`,next:`Nothing to ${r} for this node.`}:{message:`node ${e}'s managed worktree is already closed; this is not lost ownership \u2014 its checkout at ${t.path} is scheduled for automatic cleanup once that is safe`,next:`Nothing to ${r} for this node. The daemon sweep finishes removing ${t.path} on its own; inspect it directly only if it still needs manual attention.`}:{message:`node ${e} has no open managed worktree`,next:`Nothing to ${r} for this node.`}}o(Te,"noManagedWorktreeMessage");async function Q(e){if(await v(e)===null)throw new f("unknown_node",`unknown node: ${e}`,"Run this from a live node, or pass a real node id once that command supports it.");const t=await x(e);if(t===null){const{message:c,next:p}=await Te(e,"close");throw new f("no_managed_worktree",c,p)}if(!k(t.path)){const c=T(t);if(c===null)throw new f("worktree_missing",`managed worktree path is missing: ${t.path}`,J(t));if(await V(e,t,c)===null)throw new f("worktree_missing",`managed worktree path is missing: ${t.path}`,J(t));return{node_id:e,branch:t.branch,worktree_path:t.path,landed_sha:c.branchSha??t.base_sha,worktree_removed:!0,branch_deleted:!0}}if(M(t.path))throw new f("rebase_in_progress","this managed worktree is mid-rebase",`Resolve conflicts in ${t.path}, run \`git rebase --continue\` there, then rerun \`crtr node worktree close\`.`);if(Ae(t.path)!=="")throw new f("dirty_worktree","managed worktree has uncommitted changes",`Commit the work in ${t.path} (or ask the user with \`crtr human send\` if you do not know whether to keep it), then rerun \`crtr node worktree close\`.`);const a=U(t.path);if(a!==t.branch){const c=a==="HEAD"?"detached HEAD":a,p=$e(t.path,t.branch);if(p||a==="HEAD")throw new f("wrong_branch",`managed worktree is not on its recorded branch (expected ${t.branch}, found ${c})`,p?`Check out ${t.branch} in ${t.path} before closing, or ask the user with \`crtr human send\` if you are unsure why it changed, then rerun \`crtr node worktree close\`.`:`The recorded branch ${t.branch} no longer exists, and ${t.path} is in detached HEAD, so there is no branch to preserve. Check out or create a branch there before closing, or ask the user with \`crtr human send\` if you are unsure how to proceed.`);return await Le(e,t,a)}let s;try{s=R(t.path,t.base_ref)}catch(c){const p=_(t.path,["rev-parse","HEAD"],"landed_sha_failed","Inspect the worktree checkout manually, then rerun `crtr node worktree close`.");if(!(c instanceof f)||c.code!=="missing_base_branch"||p!==t.base_sha)throw c;return await $(e,t,"pending"),{node_id:e,branch:t.branch,worktree_path:t.path,landed_sha:t.base_sha,...L(t)}}const u=i(["rebase",s],t.path);if(u.status!==0){const c=(u.stderr.trim()||u.stdout.trim()).trim();throw new f("rebase_failed",`rebase onto local base branch '${t.base_ref}' did not complete`,`Resolve conflicts in ${t.path}, run \`git rebase --continue\` there, then rerun \`crtr node worktree close\`. If the blocker is not mechanical, ask the user with \`crtr human send\`.`,c)}const h=_(t.path,["rev-parse","HEAD"],"landed_sha_failed","Inspect the worktree checkout manually, then rerun `crtr node worktree close`."),m=o(async(c,p,y)=>(await Re(e,c),new f("land_base_failed",c,p,y)),"blocked"),d=i(["merge-base","--is-ancestor",s,h],t.path);if(d.status!==0)throw await m(`rebased worktree is not a fast-forward of local base branch '${t.base_ref}'`,"Ask the user with `crtr human send` how to land this managed worktree.",(d.stderr.trim()||d.stdout.trim()).trim());const l=ve(t.path,t.base_ref);if(l!==null){const c=i(["rev-parse","HEAD"],l),p=c.stdout.trim();if(c.status!==0||p!==s){const y=c.status!==0?(c.stderr.trim()||p).trim():`expected ${s}, found ${p}`;throw await m(`local base branch '${t.base_ref}' changed while this worktree was closing`,`Reconcile the base checkout at ${l}, then rerun \`crtr node worktree close\`. If you are unsure, ask the user with \`crtr human send\`.`,y)}}let b;if(l!==null){const c=Me(l,t.branch,s,h);if(!c.ok)throw await m(`could not fast-forward local base branch '${t.base_ref}' to this worktree`,`Local base branch '${t.base_ref}' is checked out at ${l} in a state that blocks a fast-forward. Reconcile it there, then rerun \`crtr node worktree close\`. If you are unsure, ask the user with \`crtr human send\`.`,c.detail);b=c.stash}else{const c=i(["update-ref",`refs/heads/${t.base_ref}`,h,s],t.path);if(c.status!==0)throw await m(`could not fast-forward local base branch '${t.base_ref}' to this worktree`,"Ask the user with `crtr human send` how to land this managed worktree.",(c.stderr.trim()||c.stdout.trim()).trim())}return await $(e,t,"pending"),{node_id:e,branch:t.branch,worktree_path:t.path,landed_sha:h,...L(t),...b===void 0?{}:{base_checkout_stash:b}}}o(Q,"closeManagedWorktreeLocked");async function Je(e){const r=await x(e);return r===null?await Q(e):await j(k(r.path)?r.path:r.repo_root,async()=>await Q(e))}o(Je,"closeManagedWorktree");function Pe(e,r,t,n){const a=_e(e.branch,r,t,n),s=i(["update-ref","--stdin"],e.repo_root,a),u=s.status===0;if(u)return{branchDeleted:u};const h=(s.stderr.trim()||s.stdout.trim()).trim();return{branchDeleted:u,branchDeleteError:`local branch ${e.branch} could not be deleted automatically (${h}); run \`git -C ${w(e.repo_root)} branch -D ${w(e.branch)}\` after confirming it is merged.`}}o(Pe,"bestEffortDeleteBranchWithContainmentProof");function T(e){if(e.state!=="open"||k(e.path))return null;const r=`refs/heads/${e.branch}`,t=i(["show-ref","--verify","--quiet",r],e.repo_root);if(t.status===1)return{branchSha:null};if(t.status!==0)return null;const n=i(["rev-parse","--verify",r],e.repo_root);if(n.status!==0)return null;const a=n.stdout.trim(),s=i(["worktree","list","--porcelain"],e.repo_root);if(s.status!==0||I(s.stdout,e.branch)!==null)return null;const u=[],h=`refs/heads/${e.base_ref}`,m=i(["show-ref","--verify","--quiet",h],e.repo_root);if(m.status===0){const l=i(["rev-parse","--verify",h],e.repo_root);if(l.status!==0)return null;u.push({ref:h,sha:l.stdout.trim()})}else if(m.status!==1)return null;const d=i(["rev-parse","--symbolic-full-name","--verify",`${e.base_ref}@{upstream}`],e.repo_root);if(d.status===0){const l=d.stdout.trim(),b=i(["rev-parse","--verify",l],e.repo_root);l.startsWith("refs/")&&b.status===0&&u.push({ref:l,sha:b.stdout.trim()})}else{const l=`refs/remotes/origin/${e.base_ref}`,b=i(["show-ref","--verify","--quiet",l],e.repo_root);if(b.status===0){const c=i(["rev-parse","--verify",l],e.repo_root);c.status===0&&u.push({ref:l,sha:c.stdout.trim()})}else if(b.status!==1)return null}for(const l of u)if(i(["merge-base","--is-ancestor",a,l.sha],e.repo_root).status===0)return{branchSha:a,containingRef:l.ref,containingSha:l.sha};return null}o(T,"proveAbsentPathManagedWorktree");function J(e){return`The recorded checkout path is gone, but crtr cannot prove ${e.branch} is already delivered. Push or land ${e.branch}, then rerun \`crtr push final\`; if you cannot establish what happened to the branch, ask the user with \`crtr human send\`.`}o(J,"missingWorktreeRecoveryHint");function P(e){if(e.state!=="open"||!k(e.path)||M(e.path))return null;const r=i(["rev-parse","--abbrev-ref","HEAD"],e.path);if(r.status!==0||r.stdout.trim()!==e.branch)return null;const t=i(["status","--porcelain"],e.path);if(t.status!==0||t.stdout.trim()!=="")return null;const n=i(["rev-parse","HEAD"],e.path);if(n.status!==0)return null;const a=n.stdout.trim();let s;try{s=R(e.path,e.base_ref)}catch(h){if(h instanceof f&&h.code==="missing_base_branch"&&a===e.base_sha)return{branchSha:a,baseSha:e.base_sha};throw h}return i(["merge-base","--is-ancestor",a,s],e.path).status!==0?null:{branchSha:a,baseSha:s}}o(P,"proveCleanEmptyManagedWorktree");async function V(e,r,t){if(k(r.path))return null;if(t.branchSha===null)return await $(e,r,"complete"),{node_id:e,branch:r.branch,worktree_path:r.path,worktree_removed:!0,branch_deleted:!0};if(t.containingRef===void 0||t.containingSha===void 0)return null;const{branchDeleted:n}=Pe(r,t.branchSha,t.containingRef,t.containingSha);return n?(await $(e,r,"complete"),{node_id:e,branch:r.branch,worktree_path:r.path,worktree_removed:!0,branch_deleted:!0}):null}o(V,"autoDropAbsentPathManagedWorktreeLocked");async function He(e,r,t){const n=P(r);return n===null||n.branchSha!==t.branchSha?null:(await $(e,r,"pending"),{node_id:e,branch:r.branch,worktree_path:r.path,...L(r)})}o(He,"autoDropCleanManagedWorktreeLocked");function Y(e){if(e.state!=="open"||!k(e.path)||M(e.path))return null;const r=i(["status","--porcelain"],e.path);if(r.status!==0||r.stdout.trim()!=="")return null;const t=i(["rev-parse","HEAD"],e.path);return t.status===0?t.stdout.trim():null}o(Y,"cleanCurrentCheckoutTip");async function Ve(e){const r=Y(e);if(r===null)return!1;const t=process.env.GIT_SSH_COMMAND??process.env.GIT_SSH??"ssh",n=await he(["ls-remote","--exit-code","--heads","origin"],e.path,void 0,{timeoutMs:1e4,env:{...process.env,GIT_TERMINAL_PROMPT:"0",GIT_SSH_COMMAND:`${t} -o BatchMode=yes`}});return n.status!==0||!n.stdout.split(`
18
+ `).some(s=>{const[u,h]=s.split(" ",2);return u===r&&h?.startsWith("refs/heads/")===!0})?!1:Y(e)===r}o(Ve,"branchIsPushedAtCurrentTip");async function Ye(e){const r=await x(e);if(r===null)return null;const t=!k(r.path);return(t?T(r)===null:P(r)===null)?null:await j(t?r.repo_root:r.path,async()=>{const n=await x(e);if(n===null)return null;if(!k(n.path)){const s=T(n);return s===null?null:await V(e,n,s)}const a=P(n);return a===null?null:await He(e,n,a)})}o(Ye,"autoDropCleanManagedWorktree");export{q as WORKTREE_LOCK_WAIT_MS,f as WorktreeError,Ye as autoDropCleanManagedWorktree,He as autoDropCleanManagedWorktreeLocked,Ve as branchIsPushedAtCurrentTip,I as checkoutPathForBranchInList,Je as closeManagedWorktree,Qe as createManagedWorktree,Xe as hasOpenManagedWorktree,ye as isManagedWorktreeBase,M as isRebaseInProgress,ge as managedWorktreePath,F as managedWorktreesRoot,J as missingWorktreeRecoveryHint,x as openManagedWorktreeForNode,$ as persistClosedWorktreeState,P as proveCleanEmptyManagedWorktree,ze as rollbackManagedWorktree,xe as samePath,G as worktreeLockPathForCommonDir};
@@ -1 +1 @@
1
- var X=Object.defineProperty;var t=(e,n)=>X(e,"name",{value:n,configurable:!0});import{randomUUID as nn}from"node:crypto";import{relative as en,resolve as z,sep as G}from"node:path";import{ApiError as c}from"../../../api/errors.js";import{armCron as rn,parseCronSink as on,getCron as v,listCrons as P,listCronRuns as F,pokeHeldCrons as tn,setCronState as L}from"../../../core/canvas/crons.js";import{getNode as _}from"../../../core/canvas/canvas.js";import{appSpace as sn,cronFolder as an}from"../../../core/layout.js";import{appProcessesSandboxed as cn,cwdIsMounted as ln}from"../../../core/spaces/process.js";import{activeGrant as dn,activeGrantForApp as un,grantFor as pn,terminalGrant as fn}from"../../../core/grants/mirror.js";import{readConfig as wn}from"../../../core/config.js";import{assertScope as Z}from"../../../core/scopes.js";import{InputError as a}from"../../../core/io.js";import{nextSlotAfter as _n}from"../../../core/wake.js";import{executeCron as hn,markSelfCanceledCronRun as mn}from"../../cron-run.js";import{toCronDTO as k,toCronRunDTO as H}from"../map.js";import{appOf as T,assertCeiling as gn,canReadNode as yn}from"../router.js";import{checkCronBirth as bn,checkCronFire as vn}from"../../cron/authority.js";import{endCron as kn}from"../../cron/ended.js";import{requireUsableProfile as Sn}from"./profiles.js";import{filterAsync as $n,mapAsync as Cn}from"../../../core/async-array.js";function E(e,n){const r=e[n];if(typeof r!="string"||r.trim()==="")throw new a({error:"missing_field",message:`cron arm request requires a non-empty "${n}"`,field:n,next:`Send a non-empty string "${n}".`});return r}t(E,"requireString");function C(e,n,r,o){if(e==null)return o;if(typeof e=="string"&&r.includes(e))return e;throw new a({error:`bad_${n}`,message:`"${String(e)}" is not a valid ${n}`,field:n,next:`Use one of: ${r.join(", ")}.`})}t(C,"requireEnum");async function On(e){if(e==="human")return JSON.stringify({human:!0});if(e.startsWith("node:")){const n=e.slice(5).trim();if(n==="")throw new a({error:"bad_sink",message:"node sink requires a node id",field:"sink",next:'Use sink "node:<node-id>".'});if(await _(n)===null)throw new a({error:"sink_node_not_found",message:`sink node ${n} does not exist on the canvas`,field:"sink",next:"Name a live node id, or use spawn:<kind> for a delivery that outlives any one node."});return JSON.stringify({node:n})}if(e.startsWith("spawn:")){const n=e.slice(6).trim();if(n==="")throw new a({error:"bad_sink",message:"spawn sink requires a node kind",field:"sink",next:'Use sink "spawn:<kind>" (e.g. spawn:general).'});return JSON.stringify({spawn:n})}throw new a({error:"bad_sink",message:`"${e}" is not a valid sink`,field:"sink",next:"Use node:<node-id>, spawn:<kind>, or human."})}t(On,"parseSinkSpec");const In=/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{3})?Z$/;function M(e,n){if(!In.test(e)||Number.isNaN(Date.parse(e)))throw new a({error:`bad_${n}`,message:`"${e}" is not a UTC ISO instant`,field:n,next:'Send a resolved UTC instant like "2026-06-07T09:00:00.000Z" (the client resolves the time grammar).'});return e}t(M,"requireUtcIso");function qn(e){try{_n(e,new Date)}catch(n){throw new a({error:"bad_recur",message:`recur is not advanceable: ${n instanceof Error?n.message:String(n)}`,field:"recur",next:'Send recur JSON the daemon can advance: {"every":"6h"} or {"cron":"0 9 * * *","tz":"America/New_York"}.'})}return e}t(qn,"requireRecur");function Nn(e){let n;try{n=JSON.parse(e)}catch{throw new a({error:"bad_env_json",message:"env_json is not valid JSON",field:"env_json",next:'Send a JSON object of string values, e.g. {"API_BASE":"https://x"}.'})}if(typeof n!="object"||n===null||Array.isArray(n))throw new a({error:"bad_env_json",message:"env_json must be a JSON object",field:"env_json",next:'Send a JSON object of string values, e.g. {"API_BASE":"https://x"}.'});for(const[r,o]of Object.entries(n))if(typeof o!="string")throw new a({error:"bad_env_json",message:`env_json value for "${r}" is not a string`,field:"env_json",next:"Every env value must be a string."});return e}t(Nn,"requireEnvJson");function U(e){const n=e.query.get("profile");return n!==null&&n.trim()!==""?n.trim():null}t(U,"callerProfile");function Y(e,n){return n===null||e.scope==="global"||e.profile===n}t(Y,"inScope");function K(e){return e.principal.class==="person"?null:T(e.principal)}t(K,"callerApp");async function J(e,n){return await yn(e.principal,n.grantee,n.created_by??void 0)}t(J,"visibleTo");function jn(e){return typeof e.anchor_node=="string"&&e.anchor_node!==""?e.anchor_node:null}t(jn,"anchorNodeOf");async function An(e,n,r,o,i,l){const d=typeof e.cwd=="string"&&e.cwd.trim()!==""?z(e.cwd):null;if(!cn()){if(d===null)throw new a({error:"missing_field",message:'cron arm request requires a non-empty "cwd"',field:"cwd",next:'Send a non-empty string "cwd".'});return d}const p=o!==null&&await _(o)!==null?o:void 0,u=t(async q=>await ln(n,q,p,i,void 0,l),"reaches");if(d!==null&&await u(d))return d;const s=sn(n).files;if(d!==null&&e.cwd_is_default!==!0)throw new c(400,"invalid_request",`cwd ${d} is not inside a directory this app's cron runs can see, so every run would fail to start; use a directory under ${s}`,{cwd:d,default:s},void 0,{param:"cwd"});const h=r===null?null:(await _(r))?.cwd??null;return h!==null&&await u(h)?h:s}t(An,"resolveArmedCwd");async function Pn(e,n,r){const o=e.principal;if(o.class==="person"){const s=await fn();return{sub:s?.sub??"user:local",grant_id:s?.id??null,scopes:null,origin_run_id:null}}if(o.class==="app"){const s=await un(n);if(s===null)throw new c(401,"grant_revoked",`no active grant for ${n}`);const h=e.ceiling??o.scopes;return{sub:s.sub,grant_id:s.id,scopes:[...h??s.resolved],origin_run_id:null}}if(o.class==="cron"){const s=await vn({grantee:o.app,sub:o.sub,grant_id:o.grant,scopes:o.scopes});if(!s.allowed)throw new c(s.reason==="scope_missing"?403:401,s.reason==="scope_missing"?"scope_missing":"grant_revoked",s.message);return{sub:o.sub,grant_id:o.grant,scopes:o.scopes===null?[...s.effectiveScopes]:[...o.scopes],origin_run_id:o.origin_run}}if(o.class!=="broker")throw new c(401,"unauthorized","this caller cannot arm a cron");const i=r??await _(o.node),l=i?.grant_id??o.grant,d=i?.sub??o.sub,p=l!=null?await dn(l):await pn(n,d);if(p===null){if(n==="app:terminal"&&!wn("user").runtime?.issuer)return{sub:d,grant_id:l??null,scopes:i?.scopes??o.scopes,origin_run_id:i?.run_id??o.run};throw new c(401,"grant_revoked","the node grant is not active")}const u=i?.scopes??o.scopes;return{sub:p.sub,grant_id:p.id,scopes:u===null?[...p.resolved]:[...u],origin_run_id:i?.run_id??o.run}}t(Pn,"armPrincipal");async function Tn(e){const n=e.body??{},r=E(n,"name"),o=E(n,"command"),i=M(E(n,"fire_at"),"fire_at"),l=typeof n.recur=="string"&&n.recur!==""?qn(n.recur):null,d=typeof n.expires_at=="string"&&n.expires_at!==""?M(n.expires_at,"expires_at"):null,p=typeof n.env_json=="string"&&n.env_json!==""?Nn(n.env_json):null,u=typeof n.sink=="string"&&n.sink.trim()!==""?n.sink.trim():null,s=C(n.on_output,"on_output",["silent","on-failure","always","on-change"],u?.startsWith("node:")?"on-change":"on-failure"),h=C(n.overlap,"overlap",["skip","queue","replace"],"skip"),q=C(n.tier,"tier",["normal","urgent","critical"],"normal"),D=s==="always"||s==="on-change";if(D&&u===null)throw new a({error:"sink_required",message:`on_output "${s}" delivers stdout every qualifying run \u2014 it needs a sink`,field:"sink",next:"Pass sink node:<node-id> (a living node's inbox), spawn:<kind> (birth a fresh node per delivery), or human (the humanloop inbox)."});if(!D&&u!==null)throw new a({error:"sink_unused",message:`on_output "${s}" never delivers stdout \u2014 a sink would be dead config`,field:"sink",next:'Drop the sink, or use on_output "always"/"on-change" if delivery is the point.'});const Q=u!==null?await On(u):"{}";if(u?.startsWith("spawn:")&&(gn(e.ceiling,"crtr:act"),e.principal.class==="cron")){const w=await bn({grantee:e.principal.app,sub:e.principal.sub,grant_id:e.principal.grant,scopes:e.principal.scopes});if(!w.allowed)throw new c(w.reason==="scope_missing"?403:401,w.reason==="scope_missing"?"scope_missing":"grant_revoked",w.message)}const y=T(e.principal)??"app:terminal",b=e.principal.class==="broker"?e.principal.node:e.principal.class==="cron"?null:typeof n.created_by=="string"&&n.created_by!==""?n.created_by:null;if(e.principal.class==="broker"&&n.created_by!=null&&n.created_by!==b)throw new c(403,"permission_error","a broker may only arm a cron as its own node");const m=b===null?null:await _(b);if(m!==null){if(m.grantee!==y)throw new a({error:"creator_app_mismatch",message:"creator node belongs to another app",field:"created_by",next:"Use a node of the calling app."});Z(m,"crtr:schedule"),u?.startsWith("spawn:")&&Z(m,"crtr:act")}if(n.isolated!==void 0&&typeof n.isolated!="boolean")throw new c(400,"invalid_request","isolated must be a boolean");if(m?.isolated&&n.isolated===!1)throw new c(403,"isolated_run","a cron created by an isolated node cannot escape isolation");const W=m?.isolated===!0||e.principal.class==="cron"&&e.principal.isolated;if(W&&n.isolated===!1)throw new c(403,"isolated_run","a cron created by an isolated caller cannot escape isolation");const N=W||n.isolated===!0,g=await Pn(e,y,m),j=nn(),S=N?an(y,j):null;if(S!==null&&n.cwd_is_default!==!0){const w=en(S,typeof n.cwd=="string"&&n.cwd.trim()!==""?z(n.cwd):S);if(w===".."||w.startsWith(`..${G}`)||w.startsWith(G))throw new c(400,"invalid_request","an isolated cron must run in its own cron folder",{param:"cwd"})}const V=S??await An(n,y,b,jn(n),N,{sub:g.sub,grantId:g.grant_id,scopes:g.scopes}),f=typeof n.anchor_node=="string"&&n.anchor_node!==""?n.anchor_node:null;if(f!==null&&await _(f)!==null&&(await _(f)).grantee!==y)throw new a({error:"anchor_app_mismatch",message:"anchor node belongs to another app",field:"anchor_node",next:"Use an anchor of the calling app."});if(f!==null&&await _(f)===null)throw new a({error:"anchor_not_found",message:`anchor node ${f} does not exist on the canvas`,field:"anchor_node",next:"Name a live node id, or drop the anchor \u2014 an unanchored cron outlives every node."});const B=n.cancel_on_wake===!0;if(B&&f===null)throw new a({error:"cancel_on_wake_needs_anchor",message:"cancel_on_wake couples the cron to a node's dormancy \u2014 it requires an anchor node",field:"anchor_node",next:"Pass anchor_node with the node whose wake should cancel this cron."});const $=n.run_timeout_s??300;if(typeof $!="number"||!Number.isInteger($)||$<=0)throw new a({error:"bad_run_timeout",message:`run_timeout_s must be a positive integer (got ${String(n.run_timeout_s)})`,field:"run_timeout_s",next:"Send the timeout in whole seconds, e.g. 300."});const A=typeof n.profile=="string"&&n.profile!==""?(await Sn(e,n.profile)).profileId:null,x=C(n.scope,"scope",["profile","global"],A!==null?"profile":"global");if(x==="profile"&&A===null)throw new a({error:"scope_needs_profile",message:'scope "profile" names the profile the cron lists and cancels within \u2014 it requires a profile',field:"profile",next:'Send a profile, or use scope "global" for a canvas-home-wide cron.'});return await rn({cron_id:j,name:r,created_by:b,grantee:y,sub:g.sub,grant_id:g.grant_id,scopes:g.scopes,origin_run_id:g.origin_run_id,isolated:N,command:o,fire_at:i,recur:l,tz:typeof n.tz=="string"?n.tz:null,expires_at:d,anchor_node:f,cancel_on_wake:B,cwd:V,env_json:p,profile:A,scope:x,run_timeout_s:$,overlap:h,on_output:s,sink:Q,tier:q}),{status:201,body:k(await v(j),null)}}t(Tn,"handleArm");async function En(e){const n=U(e),r=e.query.get("created_by");if(r==="")throw new c(400,"invalid_request","created_by must be nonempty when present");const o=await $n((n===null?await P({all:!0}):[...await P({profile:n}),...await P({global:!0})].sort((i,l)=>i.fire_at<l.fire_at?-1:i.fire_at>l.fire_at?1:0)).filter(i=>r===null||i.created_by===r),async i=>await J(e,i));return{status:200,body:await Cn(o,async i=>k(i,await R(i)))}}t(En,"handleList");async function R(e){return(await F(e.cron_id))[0]??null}t(R,"lastRunOf");async function O(e){const n=e.params.cronId,r=await v(n);if(r===null||!await J(e,r))throw new c(404,"cron_not_found",`no cron ${n}`);if(!Y(r,U(e)))throw new c(404,"cron_not_found",`no cron ${n} in this profile \u2014 it is scoped to another profile`);return r}t(O,"requireCron");function I(e,n){const r=K(e);if(r!==null&&n.grantee!==r)throw new c(403,"other_app_run",`cron ${n.cron_id} belongs to another app`);return n}t(I,"requireOwnCron");async function Un(e){const n=await O(e),r=await F(n.cron_id);return{status:200,body:{cron:k(n,r[0]??null),runs:r.map(H)}}}t(Un,"handleShow");async function Jn(e){const n=I(e,await O(e));await L(n.cron_id,"paused");const r=await v(n.cron_id);return{status:200,body:k(r,await R(r))}}t(Jn,"handlePause");async function Rn(e){const n=I(e,await O(e));await L(n.cron_id,"active");const r=await v(n.cron_id);return{status:200,body:k(r,await R(r))}}t(Rn,"handleResume");async function Dn(e){const n=I(e,await O(e));if(n.run_state==="running")throw new c(409,"cron_running",`cron ${n.cron_id} has a run in flight \u2014 wait for it to settle`);const r=await hn(n,{outOfBand:!0});if(r===null)throw new c(409,"cron_running",`cron ${n.cron_id} was claimed by a concurrent run \u2014 check \`cron show\` for its result`);return{status:200,body:H(r)}}t(Dn,"handleRun");async function Wn(e){const n=new Date().toISOString();return{status:200,body:{unparked:await tn(n,K(e)),at:n}}}t(Wn,"handlePoke");function Bn(e,n){const r=e.principal;if(r.class==="cron")return r.cron===n.cron_id?null:{kind:"canceled",by:`cron ${r.cron}`};if(r.class==="broker"){const i=on(n.sink);return i?.kind==="node"&&i.node===r.node?null:{kind:"canceled",by:`node ${r.node}`}}const o=T(r);return{kind:"canceled",by:o===null||o==="app:terminal"?"the person":o}}t(Bn,"cancelNotice");async function xn(e){const n=e.params.cronId,r=await v(n);if(r!==null&&!await J(e,r))throw new c(404,"cron_not_found",`no cron ${n}`);if(r!==null&&!Y(r,U(e)))throw new c(404,"cron_not_found",`no cron ${n} in this profile \u2014 it is scoped to another profile`);r!==null&&I(e,r);const o=e.query.get("run_id"),i=e.principal.class==="cron"?o===e.principal.execution&&n===e.principal.cron:!0,l=o!==null&&i&&mn(n,o);return r!==null&&await kn(r,l?null:Bn(e,r),{preserveRunningProcess:l}),{status:204}}t(xn,"handleCancel");const ce=[{method:"POST",pattern:"/v1/crons",scope:"crtr:schedule",handler:Tn},{method:"GET",pattern:"/v1/crons",handler:En},{method:"GET",pattern:"/v1/crons/:cronId",handler:Un},{method:"POST",pattern:"/v1/crons/:cronId/pause",scope:"crtr:schedule",handler:Jn},{method:"POST",pattern:"/v1/crons/:cronId/resume",scope:"crtr:schedule",handler:Rn},{method:"POST",pattern:"/v1/crons/poke",scope:"crtr:schedule",handler:Wn},{method:"POST",pattern:"/v1/crons/:cronId/run",scope:"crtr:schedule",handler:Dn},{method:"DELETE",pattern:"/v1/crons/:cronId",scope:"crtr:schedule",handler:xn}];export{ce as cronRoutes};
1
+ var X=Object.defineProperty;var t=(e,n)=>X(e,"name",{value:n,configurable:!0});import{randomUUID as nn}from"node:crypto";import{relative as en,resolve as z,sep as G}from"node:path";import{ApiError as c}from"../../../api/errors.js";import{armCron as rn,parseCronSink as on,getCron as v,listCrons as P,listCronRuns as F,pokeHeldCrons as tn,setCronState as L}from"../../../core/canvas/crons.js";import{getNode as _}from"../../../core/canvas/canvas.js";import{appSpace as sn,cronFolder as an}from"../../../core/layout.js";import{appProcessesSandboxed as cn,cwdIsMounted as ln}from"../../../core/spaces/process.js";import{activeGrant as dn,activeGrantForApp as un,grantFor as pn,terminalGrant as fn}from"../../../core/grants/mirror.js";import{readConfig as wn}from"../../../core/config.js";import{assertScope as Z}from"../../../core/scopes.js";import{InputError as a}from"../../../core/io.js";import{nextSlotAfter as _n}from"../../../core/wake.js";import{executeCron as hn,markSelfCanceledCronRun as mn}from"../../cron-run.js";import{toCronDTO as k,toCronRunDTO as H}from"../map.js";import{appOf as T,assertCeiling as gn,canReadNode as yn}from"../router.js";import{checkCronBirth as bn,checkCronFire as vn}from"../../cron/authority.js";import{endCron as kn}from"../../cron/ended.js";import{requireUsableProfile as Sn}from"./profiles.js";import{filterAsync as $n,mapAsync as Cn}from"../../../core/async-array.js";function E(e,n){const r=e[n];if(typeof r!="string"||r.trim()==="")throw new a({error:"missing_field",message:`cron arm request requires a non-empty "${n}"`,field:n,next:`Send a non-empty string "${n}".`});return r}t(E,"requireString");function C(e,n,r,o){if(e==null)return o;if(typeof e=="string"&&r.includes(e))return e;throw new a({error:`bad_${n}`,message:`"${String(e)}" is not a valid ${n}`,field:n,next:`Use one of: ${r.join(", ")}.`})}t(C,"requireEnum");async function On(e){if(e==="human")return JSON.stringify({human:!0});if(e.startsWith("node:")){const n=e.slice(5).trim();if(n==="")throw new a({error:"bad_sink",message:"node sink requires a node id",field:"sink",next:'Use sink "node:<node-id>".'});if(await _(n)===null)throw new a({error:"sink_node_not_found",message:`sink node ${n} does not exist on the canvas`,field:"sink",next:"Name a live node id, or use spawn:<kind> for a delivery that outlives any one node."});return JSON.stringify({node:n})}if(e.startsWith("spawn:")){const n=e.slice(6).trim();if(n==="")throw new a({error:"bad_sink",message:"spawn sink requires a node kind",field:"sink",next:'Use sink "spawn:<kind>" (e.g. spawn:general).'});return JSON.stringify({spawn:n})}throw new a({error:"bad_sink",message:`"${e}" is not a valid sink`,field:"sink",next:"Use node:<node-id>, spawn:<kind>, or human."})}t(On,"parseSinkSpec");const In=/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{3})?Z$/;function M(e,n){if(!In.test(e)||Number.isNaN(Date.parse(e)))throw new a({error:`bad_${n}`,message:`"${e}" is not a UTC ISO instant`,field:n,next:'Send a resolved UTC instant like "2026-06-07T09:00:00.000Z" (the client resolves the time grammar).'});return e}t(M,"requireUtcIso");function qn(e){try{_n(e,new Date)}catch(n){throw new a({error:"bad_recur",message:`recur is not advanceable: ${n instanceof Error?n.message:String(n)}`,field:"recur",next:'Send recur JSON the daemon can advance: {"every":"6h"} or {"cron":"0 9 * * *","tz":"America/New_York"}.'})}return e}t(qn,"requireRecur");function Nn(e){let n;try{n=JSON.parse(e)}catch{throw new a({error:"bad_env_json",message:"env_json is not valid JSON",field:"env_json",next:'Send a JSON object of string values, e.g. {"API_BASE":"https://x"}.'})}if(typeof n!="object"||n===null||Array.isArray(n))throw new a({error:"bad_env_json",message:"env_json must be a JSON object",field:"env_json",next:'Send a JSON object of string values, e.g. {"API_BASE":"https://x"}.'});for(const[r,o]of Object.entries(n))if(typeof o!="string")throw new a({error:"bad_env_json",message:`env_json value for "${r}" is not a string`,field:"env_json",next:"Every env value must be a string."});return e}t(Nn,"requireEnvJson");function U(e){const n=e.query.get("profile");return n!==null&&n.trim()!==""?n.trim():null}t(U,"callerProfile");function Y(e,n){return n===null||e.scope==="global"||e.profile===n}t(Y,"inScope");function K(e){return e.principal.class==="person"?null:T(e.principal)}t(K,"callerApp");async function J(e,n){return await yn(e.principal,n.grantee,n.created_by??void 0)}t(J,"visibleTo");function jn(e){return typeof e.anchor_node=="string"&&e.anchor_node!==""?e.anchor_node:null}t(jn,"anchorNodeOf");async function An(e,n,r,o,i,l){const d=typeof e.cwd=="string"&&e.cwd.trim()!==""?z(e.cwd):null;if(!cn()){if(d===null)throw new a({error:"missing_field",message:'cron arm request requires a non-empty "cwd"',field:"cwd",next:'Send a non-empty string "cwd".'});return d}const h=o!==null&&await _(o)!==null?o:void 0,u=t(async q=>await ln(n,q,h,i,void 0,l),"reaches");if(d!==null&&await u(d))return d;const s=sn(n).files;if(d!==null&&e.cwd_is_default!==!0)throw new c(400,"invalid_request",`cwd ${d} is not inside a directory this app's cron runs can see, so every run would fail to start; use a directory under ${s}`,{cwd:d,default:s},void 0,{param:"cwd"});const p=r===null?null:(await _(r))?.cwd??null;return p!==null&&await u(p)?p:s}t(An,"resolveArmedCwd");async function Pn(e,n,r){const o=e.principal;if(o.class==="person"){const s=await fn();return{sub:s?.sub??"user:local",grant_id:s?.id??null,scopes:null,origin_run_id:null}}if(o.class==="app"){const s=await un(n);if(s===null)throw new c(401,"grant_revoked",`no active grant for ${n}`);const p=e.ceiling??o.scopes;return{sub:s.sub,grant_id:s.id,scopes:p===null?null:[...p],origin_run_id:null}}if(o.class==="cron"){const s=await vn({grantee:o.app,sub:o.sub,grant_id:o.grant,scopes:o.scopes});if(!s.allowed)throw new c(s.reason==="scope_missing"?403:401,s.reason==="scope_missing"?"scope_missing":"grant_revoked",s.message);return{sub:o.sub,grant_id:o.grant,scopes:o.scopes===null?null:[...o.scopes],origin_run_id:o.origin_run}}if(o.class!=="broker")throw new c(401,"unauthorized","this caller cannot arm a cron");const i=r??await _(o.node),l=i?.grant_id??o.grant,d=i?.sub??o.sub,h=l!=null?await dn(l):await pn(n,d);if(h===null){if(n==="app:terminal"&&!wn("user").runtime?.issuer)return{sub:d,grant_id:l??null,scopes:i?.scopes??o.scopes,origin_run_id:i?.run_id??o.run};throw new c(401,"grant_revoked","the node grant is not active")}const u=i?.scopes??o.scopes;return{sub:h.sub,grant_id:h.id,scopes:u===null?null:[...u],origin_run_id:i?.run_id??o.run}}t(Pn,"armPrincipal");async function Tn(e){const n=e.body??{},r=E(n,"name"),o=E(n,"command"),i=M(E(n,"fire_at"),"fire_at"),l=typeof n.recur=="string"&&n.recur!==""?qn(n.recur):null,d=typeof n.expires_at=="string"&&n.expires_at!==""?M(n.expires_at,"expires_at"):null,h=typeof n.env_json=="string"&&n.env_json!==""?Nn(n.env_json):null,u=typeof n.sink=="string"&&n.sink.trim()!==""?n.sink.trim():null,s=C(n.on_output,"on_output",["silent","on-failure","always","on-change"],u?.startsWith("node:")?"on-change":"on-failure"),p=C(n.overlap,"overlap",["skip","queue","replace"],"skip"),q=C(n.tier,"tier",["normal","urgent","critical"],"normal"),D=s==="always"||s==="on-change";if(D&&u===null)throw new a({error:"sink_required",message:`on_output "${s}" delivers stdout every qualifying run \u2014 it needs a sink`,field:"sink",next:"Pass sink node:<node-id> (a living node's inbox), spawn:<kind> (birth a fresh node per delivery), or human (the humanloop inbox)."});if(!D&&u!==null)throw new a({error:"sink_unused",message:`on_output "${s}" never delivers stdout \u2014 a sink would be dead config`,field:"sink",next:'Drop the sink, or use on_output "always"/"on-change" if delivery is the point.'});const Q=u!==null?await On(u):"{}";if(u?.startsWith("spawn:")&&(gn(e.ceiling,"crtr:act"),e.principal.class==="cron")){const w=await bn({grantee:e.principal.app,sub:e.principal.sub,grant_id:e.principal.grant,scopes:e.principal.scopes});if(!w.allowed)throw new c(w.reason==="scope_missing"?403:401,w.reason==="scope_missing"?"scope_missing":"grant_revoked",w.message)}const y=T(e.principal)??"app:terminal",b=e.principal.class==="broker"?e.principal.node:e.principal.class==="cron"?null:typeof n.created_by=="string"&&n.created_by!==""?n.created_by:null;if(e.principal.class==="broker"&&n.created_by!=null&&n.created_by!==b)throw new c(403,"permission_error","a broker may only arm a cron as its own node");const m=b===null?null:await _(b);if(m!==null){if(m.grantee!==y)throw new a({error:"creator_app_mismatch",message:"creator node belongs to another app",field:"created_by",next:"Use a node of the calling app."});Z(m,"crtr:schedule"),u?.startsWith("spawn:")&&Z(m,"crtr:act")}if(n.isolated!==void 0&&typeof n.isolated!="boolean")throw new c(400,"invalid_request","isolated must be a boolean");if(m?.isolated&&n.isolated===!1)throw new c(403,"isolated_run","a cron created by an isolated node cannot escape isolation");const W=m?.isolated===!0||e.principal.class==="cron"&&e.principal.isolated;if(W&&n.isolated===!1)throw new c(403,"isolated_run","a cron created by an isolated caller cannot escape isolation");const N=W||n.isolated===!0,g=await Pn(e,y,m),j=nn(),S=N?an(y,j):null;if(S!==null&&n.cwd_is_default!==!0){const w=en(S,typeof n.cwd=="string"&&n.cwd.trim()!==""?z(n.cwd):S);if(w===".."||w.startsWith(`..${G}`)||w.startsWith(G))throw new c(400,"invalid_request","an isolated cron must run in its own cron folder",{param:"cwd"})}const V=S??await An(n,y,b,jn(n),N,{sub:g.sub,grantId:g.grant_id,scopes:g.scopes}),f=typeof n.anchor_node=="string"&&n.anchor_node!==""?n.anchor_node:null;if(f!==null&&await _(f)!==null&&(await _(f)).grantee!==y)throw new a({error:"anchor_app_mismatch",message:"anchor node belongs to another app",field:"anchor_node",next:"Use an anchor of the calling app."});if(f!==null&&await _(f)===null)throw new a({error:"anchor_not_found",message:`anchor node ${f} does not exist on the canvas`,field:"anchor_node",next:"Name a live node id, or drop the anchor \u2014 an unanchored cron outlives every node."});const B=n.cancel_on_wake===!0;if(B&&f===null)throw new a({error:"cancel_on_wake_needs_anchor",message:"cancel_on_wake couples the cron to a node's dormancy \u2014 it requires an anchor node",field:"anchor_node",next:"Pass anchor_node with the node whose wake should cancel this cron."});const $=n.run_timeout_s??300;if(typeof $!="number"||!Number.isInteger($)||$<=0)throw new a({error:"bad_run_timeout",message:`run_timeout_s must be a positive integer (got ${String(n.run_timeout_s)})`,field:"run_timeout_s",next:"Send the timeout in whole seconds, e.g. 300."});const A=typeof n.profile=="string"&&n.profile!==""?(await Sn(e,n.profile)).profileId:null,x=C(n.scope,"scope",["profile","global"],A!==null?"profile":"global");if(x==="profile"&&A===null)throw new a({error:"scope_needs_profile",message:'scope "profile" names the profile the cron lists and cancels within \u2014 it requires a profile',field:"profile",next:'Send a profile, or use scope "global" for a canvas-home-wide cron.'});return await rn({cron_id:j,name:r,created_by:b,grantee:y,sub:g.sub,grant_id:g.grant_id,scopes:g.scopes,origin_run_id:g.origin_run_id,isolated:N,command:o,fire_at:i,recur:l,tz:typeof n.tz=="string"?n.tz:null,expires_at:d,anchor_node:f,cancel_on_wake:B,cwd:V,env_json:h,profile:A,scope:x,run_timeout_s:$,overlap:p,on_output:s,sink:Q,tier:q}),{status:201,body:k(await v(j),null)}}t(Tn,"handleArm");async function En(e){const n=U(e),r=e.query.get("created_by");if(r==="")throw new c(400,"invalid_request","created_by must be nonempty when present");const o=await $n((n===null?await P({all:!0}):[...await P({profile:n}),...await P({global:!0})].sort((i,l)=>i.fire_at<l.fire_at?-1:i.fire_at>l.fire_at?1:0)).filter(i=>r===null||i.created_by===r),async i=>await J(e,i));return{status:200,body:await Cn(o,async i=>k(i,await R(i)))}}t(En,"handleList");async function R(e){return(await F(e.cron_id))[0]??null}t(R,"lastRunOf");async function O(e){const n=e.params.cronId,r=await v(n);if(r===null||!await J(e,r))throw new c(404,"cron_not_found",`no cron ${n}`);if(!Y(r,U(e)))throw new c(404,"cron_not_found",`no cron ${n} in this profile \u2014 it is scoped to another profile`);return r}t(O,"requireCron");function I(e,n){const r=K(e);if(r!==null&&n.grantee!==r)throw new c(403,"other_app_run",`cron ${n.cron_id} belongs to another app`);return n}t(I,"requireOwnCron");async function Un(e){const n=await O(e),r=await F(n.cron_id);return{status:200,body:{cron:k(n,r[0]??null),runs:r.map(H)}}}t(Un,"handleShow");async function Jn(e){const n=I(e,await O(e));await L(n.cron_id,"paused");const r=await v(n.cron_id);return{status:200,body:k(r,await R(r))}}t(Jn,"handlePause");async function Rn(e){const n=I(e,await O(e));await L(n.cron_id,"active");const r=await v(n.cron_id);return{status:200,body:k(r,await R(r))}}t(Rn,"handleResume");async function Dn(e){const n=I(e,await O(e));if(n.run_state==="running")throw new c(409,"cron_running",`cron ${n.cron_id} has a run in flight \u2014 wait for it to settle`);const r=await hn(n,{outOfBand:!0});if(r===null)throw new c(409,"cron_running",`cron ${n.cron_id} was claimed by a concurrent run \u2014 check \`cron show\` for its result`);return{status:200,body:H(r)}}t(Dn,"handleRun");async function Wn(e){const n=new Date().toISOString();return{status:200,body:{unparked:await tn(n,K(e)),at:n}}}t(Wn,"handlePoke");function Bn(e,n){const r=e.principal;if(r.class==="cron")return r.cron===n.cron_id?null:{kind:"canceled",by:`cron ${r.cron}`};if(r.class==="broker"){const i=on(n.sink);return i?.kind==="node"&&i.node===r.node?null:{kind:"canceled",by:`node ${r.node}`}}const o=T(r);return{kind:"canceled",by:o===null||o==="app:terminal"?"the person":o}}t(Bn,"cancelNotice");async function xn(e){const n=e.params.cronId,r=await v(n);if(r!==null&&!await J(e,r))throw new c(404,"cron_not_found",`no cron ${n}`);if(r!==null&&!Y(r,U(e)))throw new c(404,"cron_not_found",`no cron ${n} in this profile \u2014 it is scoped to another profile`);r!==null&&I(e,r);const o=e.query.get("run_id"),i=e.principal.class==="cron"?o===e.principal.execution&&n===e.principal.cron:!0,l=o!==null&&i&&mn(n,o);return r!==null&&await kn(r,l?null:Bn(e,r),{preserveRunningProcess:l}),{status:204}}t(xn,"handleCancel");const ce=[{method:"POST",pattern:"/v1/crons",scope:"crtr:schedule",handler:Tn},{method:"GET",pattern:"/v1/crons",handler:En},{method:"GET",pattern:"/v1/crons/:cronId",handler:Un},{method:"POST",pattern:"/v1/crons/:cronId/pause",scope:"crtr:schedule",handler:Jn},{method:"POST",pattern:"/v1/crons/:cronId/resume",scope:"crtr:schedule",handler:Rn},{method:"POST",pattern:"/v1/crons/poke",scope:"crtr:schedule",handler:Wn},{method:"POST",pattern:"/v1/crons/:cronId/run",scope:"crtr:schedule",handler:Dn},{method:"DELETE",pattern:"/v1/crons/:cronId",scope:"crtr:schedule",handler:xn}];export{ce as cronRoutes};
@@ -1 +1 @@
1
- var oe=Object.defineProperty;var u=(n,e)=>oe(n,"name",{value:e,configurable:!0});import{ApiError as _}from"../../../api/errors.js";import{DOC_KINDS as P,DOC_UNSET_FIELDS as q}from"../../../api/dto/docs.js";import{PRIVACY_LEVELS_DTO as H}from"../../../api/dto/objects.js";import{withCanvasWrite as k}from"../../../core/canvas/db.js";import{CrtrError as re}from"../../../core/errors.js";import{emitEvent as ie}from"../../../core/events/emit.js";import{canChange as U,spaceOf as ae}from"../../../core/graph/access.js";import{get as O}from"../../../core/graph/bodies.js";import{deleteDocument as se,documentRecord as F,editDocument as ce,heads as x,history as de,linksIn as V,moveDocument as ue,readBase as fe,setDocumentRecord as le,withCurrentNames as me,writeDocument as pe}from"../../../core/graph/documents.js";import{resolveName as K}from"../../../core/graph/names.js";import{getById as L}from"../../../core/graph/objects.js";import{noteRepoDocumentsChanged as we}from"../../../core/graph/repo-sync/index.js";import{checkoutsForCwd as he}from"../../../core/graph/repo-sync/repos.js";import{lintRules as ye,rulesOf as ve}from"../../../core/graph/rules.js";import{unifiedDiff as ge}from"../../../core/graph/diff.js";import{ownerFromFlag as M,readerFor as E,resolveRef as X}from"../reader.js";import{filterAsync as _e,flatMapAsync as be}from"../../../core/async-array.js";function v(n,e,t,o,i={}){return new _(n,e,t,{...i,error:e,message:t,next:o})}u(v,"refuse");const b=u((n,e)=>new _(400,"invalid_request",n,e),"invalid");function R(n){const e=n.body;if(e===void 0)return{};if(typeof e!="object"||e===null||Array.isArray(e))throw b("the request body must be a JSON object");return e}u(R,"bodyOf");const C=u(async n=>async e=>await he(n,e),"reposFnFor");function S(n,e){if(n instanceof re){const t=n.details;if(n.code==="read_first")throw v(409,"read_first",n.message,`Read it first with \`crtr canvas read ${e?.id??"<ref>"}\`, then rerun the edit.`,t??{});if(n.code==="conflict")throw new _(409,"name_taken",n.message,t);if(n.code==="forbidden")throw new _(403,"forbidden",n.message,t);if(n.code==="scope_missing")throw v(403,"scope_missing",n.message,`Launch the run with \`${String(t?.scope)}\` in its scopes list.`,t??{});if(t?.rule==="repo-public-only")throw v(422,"repo_documents_are_public",n.message,'Own it as `user` and add a rule with `if: {"repo": "<handle>"}` so it loads only in that repo.')}throw n}u(S,"mapGraphError");async function W(n,e,t){const o=await X(n,e,decodeURIComponent(t));if("remote"in o)throw v(404,"not_found",z(o.remote),"Check the name with `crtr canvas search`.");if("deleted"in o)throw new _(404,"not_found",`${o.deleted.name} is deleted`,{id:o.deleted.id,deleted:!0});if(o.type!=="document")throw b(`${t} is a ${o.type}, not a document`);return o}u(W,"refDocument");function z(n){return`no object named ${n} on this runtime; it may be on another runtime, or the name may be misspelled`}u(z,"remoteMessage");async function ke(n,e){const t=e.owner===null?null:await L(n,e.owner);return t?t.type==="node"?t.id:t.name:""}u(ke,"ownerHandle");async function j(n,e){const t=await ke(n,e);return t?`${t}/${e.name}`:e.name}u(j,"canonicalName");async function Ee(n,e){const t=e.owner===null?null:await L(n,e.owner),o=t?{id:t.id,type:t.type,handle:t.type==="node"?t.id:t.name}:null;return{id:e.id,type:e.type,kind:e.kind,owner:o,privacy:e.privacy,name:e.name,preview:e.preview??"",created_at:e.created_at,deleted_at:e.deleted_at}}u(Ee,"objectDTO");async function $e(n,e){const t=e.owner===null?null:await L(n,e.owner);return t?.type==="repo"?t:null}u($e,"repoOwner");async function N(n,e,t,o){const i=[...new Map(await be(t,async f=>{const a=await $e(n,f);return a?[[a.id,a]]:[]})).values()],s=e.class==="node"?e.cwd:o,r=e.class==="node"?[...e.checkouts,...e.projectCheckouts].map(f=>f.root):[];return{repos:i,dirs:[...new Set([...s!==void 0?[s]:[],...r])]}}u(N,"repoChange");async function T(n){for(const e of n.repos)try{await we(e,n.dirs)}catch(t){ie({level:"warn",event:"repo_sync.failed",outcome:"failed",error:t,fields:{repo:e.name,step:"export",reason:t instanceof Error?t.message:String(t)}})}}u(T,"syncChanged");const G=/^[A-Za-z0-9_-]+(\/[A-Za-z0-9_-]+)*$/;function Ae(n,e){if(n===void 0)return;if(!Array.isArray(n)){e.push("`rules` must be a list");return}const t=n.map(i=>{if(typeof i!="object"||i===null||Array.isArray(i))return i;const{match_frontmatter:s,...r}=i;return"matchFrontmatter"in r?{...r,match_frontmatter:r.matchFrontmatter}:s===void 0?r:{...r,matchFrontmatter:s}}),o=ye(t);if(o){e.push(o.replaceAll("matchFrontmatter","match_frontmatter"));return}return t}u(Ae,"rulesFromDTO");function Y(n,e){const t={},o=u(a=>{const c=n[a];if(c!==void 0){if(typeof c!="string"){e.push(`\`${a}\` must be a string`);return}return c}},"str"),i=o("preview");i!==void 0&&(i.trim()===""||/[\r\n]/.test(i)?e.push("`preview` must be one non-empty line"):t.preview=i);const s=o("summary");s!==void 0&&(t.summary=s);const r=o("why_it_exists");if(r!==void 0&&(t.whyItExists=r),n.privacy!==void 0&&(H.includes(n.privacy)?t.privacy=n.privacy:e.push(`\`privacy\` must be one of ${H.join(", ")}`)),n.unlisted!==void 0&&(typeof n.unlisted!="boolean"?e.push("`unlisted` must be a boolean"):t.unlisted=n.unlisted),n.lint_ignore!==void 0){const a=n.lint_ignore;!Array.isArray(a)||a.some(c=>typeof c!="string")?e.push("`lint_ignore` must be a list of strings"):t.lintIgnore=a}if(n.extensions!==void 0){const a=n.extensions;typeof a!="object"||a===null||Array.isArray(a)||Object.values(a).some(c=>typeof c!="object"||c===null||Array.isArray(c))?e.push("`extensions` must map each plugin to an object of fields"):t.extensions=a}const f=Ae(n.rules,e);return f!==void 0&&(t.rules=f),t}u(Y,"fieldsFrom");function Z(n){if(n.length>0)throw b(n.join("; "),{errors:n})}u(Z,"failOn");async function J(n,e,t,o){const i=await _e(V(t),async s=>"remote"in await K(n,e,s));return{...o.length>0?{unresolved_links:o}:{},...i.length>0?{remote_links:i}:{}}}u(J,"linkReport");async function Oe(n){const e=R(n),t=[],o=e.name;(typeof o!="string"||!G.test(o))&&t.push("`name` must be segments of letters, digits, `_` or `-` joined by `/`"),typeof e.body!="string"&&t.push("`body` must be a string"),typeof e.preview!="string"&&t.push("`preview` is required");const i=e.kind??"knowledge";P.includes(i)||t.push(i==="report"?"reports are written only by `crtr push`":`\`kind\` must be one of ${P.join(", ")}`);const s=Y(e,t);e.cwd!==void 0&&typeof e.cwd!="string"&&t.push("`cwd` must be a string"),Z(t);const r=e,{dto:f,change:a}=await k(async c=>{const w=await E(c,n),d=await M(c,w,r.owner,r.cwd);if(!await U(c,w,d))throw new _(404,"not_found",`no owner ${d.name} you can write to`);try{const{object:p,unresolved:l}=await pe(c,w,{...s,owner:d,name:r.name,kind:i,preview:s.preview,privacy:s.privacy??"public"},r.body,await C(c));return{dto:{id:p.id,name:await j(c,p),...await J(c,w,r.body,l)},change:await N(c,w,[p],r.cwd)}}catch(p){S(p)}});return await T(a),f}u(Oe,"write");function je(n,e){let t=n;e.forEach(({old:o,new:i},s)=>{const r=o===""?-1:t.indexOf(o);if(r<0)throw v(422,"replace_no_match",`replace[${s}]: old text not found in the revision you read`,"Copy the old text exactly from your last read and rerun.",{index:s});if(t.indexOf(o,r+1)>=0)throw v(422,"replace_ambiguous",`replace[${s}]: old text occurs more than once`,"Include more surrounding text so the old text matches once, and rerun.",{index:s});t=t.slice(0,r)+i+t.slice(r+o.length)})}u(je,"checkReplacements");async function De(n){const e=R(n),t=[],o=e.body!==void 0,i=e.replace!==void 0;o&&i&&t.push("give `body` or `replace`, not both"),o&&typeof e.body!="string"&&t.push("`body` must be a string");const s=e.replace;i&&(!Array.isArray(s)||s.length===0||s.some(d=>typeof d!="object"||d===null||typeof d.old!="string"||typeof d.new!="string"))&&t.push("`replace` must be a non-empty list of {old, new} strings"),(typeof e.rationale!="string"||e.rationale.trim()==="")&&t.push("`rationale` is required"),e.verbatim!==void 0&&typeof e.verbatim!="boolean"&&t.push("`verbatim` must be a boolean"),e.base!==void 0&&(typeof e.base!="string"||e.base==="")&&t.push("`base` must be a non-empty heads key"),e.base!==void 0&&!o&&!i&&t.push("`base` applies only to a `body` or `replace` edit");const r=Y(e,t),f=e.unset??[];if(!Array.isArray(f)||f.some(d=>!q.includes(d)))t.push(`\`unset\` takes only ${q.join(", ")}`);else for(const d of f)d==="summary"&&(r.summary=null),d==="why_it_exists"&&(r.whyItExists=null),d==="unlisted"&&(r.unlisted=!1),d==="lint_ignore"&&(r.lintIgnore=null),d==="extensions"&&(r.extensions=null),d==="rules"&&(r.rules=[]);t.length===0&&!o&&!i&&Object.keys(r).length===0&&t.push("nothing to change: give `body`, `replace`, or a field"),Z(t);const a=e,{dto:c,synced:w}=await k(async d=>{const p=await E(d,n),l=await W(d,p,n.params.ref),g=await j(d,l),A=await N(d,p,[l]);try{if(!o&&!i)return await le(d,p,l,r),{dto:{id:l.id,name:g,revision:(await F(d,l)).revision},synced:A};const y=a.base??await fe(d,p,l);if(i&&y!==null){const ne=await F(d,l),B=await x(d,l);ne.heads_key===y&&B.length===1&&je(await me(d,l,O(B[0].body_hash)),a.replace)}const D=a.verbatim===!0,m=o?{body:a.body,verbatim:D}:{replace:a.replace,verbatim:D},h=await ce(d,p,l,m,a.rationale,y,r,await C(d));if("stale"in h)throw v(409,"stale_revision",`${g} changed after your read; it is now at revision ${h.stale.revision}`,`Read it again with \`crtr canvas read ${g}\`, then redo your change on the current text.`,{stale:h.stale});const I=O((await x(d,l))[0].body_hash);return{dto:{id:l.id,name:g,revision:h.revision,...await J(d,p,I,h.unresolved)},synced:A}}catch(y){S(y,l)}});return await T(w),c}u(De,"edit");async function Re(n){const e=R(n);if(e.new_name===void 0&&e.owner===void 0)throw b("give a new name, an owner, or both");if(e.new_name!==void 0&&(typeof e.new_name!="string"||!G.test(e.new_name)))throw b("`new_name` must be segments of letters, digits, `_` or `-` joined by `/`");const{dto:t,change:o}=await k(async i=>{const s=await E(i,n),r=await W(i,s,n.params.ref),f=e.owner===void 0?void 0:await M(i,s,e.owner,e.cwd);try{const a=await ue(i,s,r,{...e.new_name!==void 0?{name:e.new_name}:{},...f?{owner:f}:{}},await C(i));return{dto:{id:a.id,name:await j(i,a)},change:await N(i,s,[r,a],e.cwd)}}catch(a){S(a,r)}});return await T(o),t}u(Re,"move");async function Ce(n){const{dto:e,change:t}=await k(async o=>{const i=await E(o,n),s=await W(o,i,n.params.ref),r=await j(o,s),f=await N(o,i,[s]);try{await se(o,i,s,await C(o))}catch(a){S(a,s)}return{dto:{id:s.id,name:r},change:f}});return await T(t),e}u(Ce,"remove");async function Se(n){const e=n.query.get("diff"),t=e===null?void 0:Number(e);if(t!==void 0&&!(Number.isInteger(t)&&t>0))throw b("`diff` must be a revision number");return await k(async o=>{const i=await E(o,n),s=await X(o,i,decodeURIComponent(n.params.ref));if("remote"in s)throw v(404,"not_found",z(s.remote),"Check the name with `crtr canvas search`.");const r="deleted"in s?s.deleted:s;if(r.type!=="document")throw b(`${r.name} is a ${r.type}, not a document`);const f=await de(o,i,r),a={object:await Ee(o,r),revisions:f.map(c=>({rev_id:c.rev_id,number:c.number,author_id:c.author,rationale:c.rationale,verbatim:c.verbatim,created_at:c.created_at,parents:c.parents}))};if(t!==void 0){const c=f.find(d=>d.number===t);if(!c)throw new _(404,"not_found",`${r.name} has no revision ${t}`);const w=c.parents[0]===void 0?void 0:f.find(d=>d.rev_id===c.parents[0]);a.diff=ge(w?O(w.body_hash):"",O(c.body_hash))}return a})}u(Se,"revisionHistory");const Q=1e3,ee=1e3,te=3e3,Ne=/^When\b.+\bbecause\b.+/,Te=u(n=>{const e=n.trim();return e===""?0:e.split(/\s+/).length},"words");function Ie(n,e){if(n.some(o=>o.on==="boot"&&o.deliver==="content")&&e>Q)return`body is ${e} words, over the ${Q}-word cap for a boot rule at content (the whole body enters every node's context). Keep the core here and move separate subjects into [[linked]] documents.`;const t=[...new Set(n.filter(o=>o.on!=="boot"&&o.deliver==="content").map(o=>o.on))];return t.length>0&&e>ee?`body is ${e} words, over the ${ee}-word cap for a content rule on ${t.join("/")}. Keep the core here and move separate subjects into [[linked]] documents.`:n.some(o=>o.on==="boot"&&o.deliver==="preview")&&e>te?`body is ${e} words, over the ${te}-word cap for a boot rule at preview. Keep the core here and move separate subjects into [[linked]] documents.`:null}u(Ie,"lengthFinding");async function Fe(n){const e=R(n);return await k(async t=>{const o=await E(t,n),i=e.owner===void 0?null:await M(t,o,e.owner,e.cwd),s=await t.prepare("SELECT * FROM objects WHERE type = 'document' AND deleted_at IS NULL AND home_machine IS NULL AND kind != 'report' ORDER BY name").all(),r=[],f=new Map;for(const a of s){if(i&&a.owner!==i.id||!await U(t,o,a))continue;const c=await F(t,a),w=new Set(c.lint_ignore??[]),d=await j(t,a),p=u((m,h,I)=>{w.has(m)||r.push({name:d,severity:h,rule:m,message:I})},"add"),l=await ve(t,a),g=(await x(t,a))[0],A=g?O(g.body_hash):"",y=Ie(l,Te(A));y&&p("length","error",y),l.some(m=>m.deliver==="preview")&&!Ne.test(a.preview??"")&&p("preview-shape","error",`a rule delivers this document at preview, so the preview must read "When <situation in the reader's work>, read this because <what the reader gains>"`);const D=new Set((await t.prepare("SELECT written_as FROM edges WHERE type = 'links-to' AND src = ?").all(a.id)).map(m=>m.written_as));for(const m of V(A)){if(D.has(m))continue;const h=await K(t,o,m);"remote"in h||"object"in h||"deleted"in h||p("links","warning",`[[${m}]] names no object in your view`)}if(l.some(m=>m.on==="workspace-open"&&m.deliver==="content")){const m=await ae(t,a);m.type==="repo"&&!w.has("workspace-open")&&f.set(m.name,[...f.get(m.name)??[],d])}}for(const[a,c]of f)if(!(c.length<2))for(const w of c)r.push({name:w,severity:"warning",rule:"workspace-open",message:`repo ${a} has ${c.length} documents delivered at content on workspace-open (${c.join(", ")}); keep one and link the rest from it`});return{findings:r}})}u(Fe,"lint");const $=u((n,e=200)=>async t=>({status:e,body:await n(t)}),"ok"),et=[{method:"POST",pattern:"/v1/docs",handler:$(Oe,201)},{method:"PATCH",pattern:"/v1/docs/:ref",handler:$(De)},{method:"POST",pattern:"/v1/docs/:ref/move",handler:$(Re)},{method:"DELETE",pattern:"/v1/docs/:ref",handler:$(Ce)},{method:"GET",pattern:"/v1/docs/:ref/history",handler:$(Se)},{method:"POST",pattern:"/v1/docs/lint",handler:$(Fe)}];export{j as canonicalName,et as docRoutes,N as repoChange,T as syncChanged};
1
+ var oe=Object.defineProperty;var u=(n,e)=>oe(n,"name",{value:e,configurable:!0});import{ApiError as _}from"../../../api/errors.js";import{DOC_KINDS as q,DOC_UNSET_FIELDS as H}from"../../../api/dto/docs.js";import{PRIVACY_LEVELS_DTO as U}from"../../../api/dto/objects.js";import{withCanvasWrite as k}from"../../../core/canvas/db.js";import{CrtrError as re}from"../../../core/errors.js";import{emitEvent as ie}from"../../../core/events/emit.js";import{canChange as V,spaceOf as ae}from"../../../core/graph/access.js";import{get as O}from"../../../core/graph/bodies.js";import{deleteDocument as se,documentRecord as F,editDocument as ce,heads as x,history as de,linksIn as K,moveDocument as ue,readBase as fe,setDocumentRecord as le,withCurrentNames as me,writeDocument as pe}from"../../../core/graph/documents.js";import{resolveName as X}from"../../../core/graph/names.js";import{getById as L}from"../../../core/graph/objects.js";import{noteRepoDocumentsChanged as we}from"../../../core/graph/repo-sync/index.js";import{checkoutsForCwd as he}from"../../../core/graph/repo-sync/repos.js";import{lintRules as ye,rulesOf as ve}from"../../../core/graph/rules.js";import{unifiedDiff as ge}from"../../../core/graph/diff.js";import{ownerFromFlag as M,readerFor as E,resolveRef as z}from"../reader.js";import{filterAsync as _e,flatMapAsync as be}from"../../../core/async-array.js";function y(n,e,t,o,i={}){return new _(n,e,t,{...i,error:e,message:t,next:o})}u(y,"refuse");const b=u((n,e)=>new _(400,"invalid_request",n,e),"invalid");function R(n){const e=n.body;if(e===void 0)return{};if(typeof e!="object"||e===null||Array.isArray(e))throw b("the request body must be a JSON object");return e}u(R,"bodyOf");const C=u(async n=>async e=>await he(n,e),"reposFnFor");function S(n,e){if(n instanceof re){const t=n.details;if(n.code==="read_first")throw y(409,"read_first",n.message,`Read it first with \`crtr canvas read ${e?.id??"<ref>"}\`, then rerun the edit.`,t??{});if(n.code==="conflict")throw new _(409,"name_taken",n.message,t);if(n.code==="forbidden")throw typeof t?.next=="string"?y(403,"forbidden",n.message,t.next,t):new _(403,"forbidden",n.message,t);if(n.code==="scope_missing")throw y(403,"scope_missing",n.message,`Launch the run with \`${String(t?.scope)}\` in its scopes list.`,t??{});if(t?.rule==="repo-public-only")throw y(422,"repo_documents_are_public",n.message,'Own it as `user` and add a rule with `if: {"repo": "<handle>"}` so it loads only in that repo.')}throw n}u(S,"mapGraphError");async function W(n,e,t){const o=await z(n,e,decodeURIComponent(t));if("remote"in o)throw y(404,"not_found",G(o.remote),"Check the name with `crtr canvas search`.");if("deleted"in o)throw new _(404,"not_found",`${o.deleted.name} is deleted`,{id:o.deleted.id,deleted:!0});if(o.type!=="document")throw b(`${t} is a ${o.type}, not a document`);return o}u(W,"refDocument");function G(n){return`no object named ${n} on this runtime; it may be on another runtime, or the name may be misspelled`}u(G,"remoteMessage");async function ke(n,e){const t=e.owner===null?null:await L(n,e.owner);return t?t.type==="node"?t.id:t.name:""}u(ke,"ownerHandle");async function j(n,e){const t=await ke(n,e);return t?`${t}/${e.name}`:e.name}u(j,"canonicalName");async function Ee(n,e){const t=e.owner===null?null:await L(n,e.owner),o=t?{id:t.id,type:t.type,handle:t.type==="node"?t.id:t.name}:null;return{id:e.id,type:e.type,kind:e.kind,owner:o,privacy:e.privacy,name:e.name,preview:e.preview??"",created_at:e.created_at,deleted_at:e.deleted_at}}u(Ee,"objectDTO");async function $e(n,e){const t=e.owner===null?null:await L(n,e.owner);return t?.type==="repo"?t:null}u($e,"repoOwner");async function N(n,e,t,o){const i=[...new Map(await be(t,async f=>{const a=await $e(n,f);return a?[[a.id,a]]:[]})).values()],s=e.class==="node"?e.cwd:o,r=e.class==="node"?[...e.checkouts,...e.projectCheckouts].map(f=>f.root):[];return{repos:i,dirs:[...new Set([...s!==void 0?[s]:[],...r])]}}u(N,"repoChange");async function T(n){for(const e of n.repos)try{await we(e,n.dirs)}catch(t){ie({level:"warn",event:"repo_sync.failed",outcome:"failed",error:t,fields:{repo:e.name,step:"export",reason:t instanceof Error?t.message:String(t)}})}}u(T,"syncChanged");const Y=/^[A-Za-z0-9_-]+(\/[A-Za-z0-9_-]+)*$/;function Ae(n,e){if(n===void 0)return;if(!Array.isArray(n)){e.push("`rules` must be a list");return}const t=n.map(i=>{if(typeof i!="object"||i===null||Array.isArray(i))return i;const{match_frontmatter:s,...r}=i;return"matchFrontmatter"in r?{...r,match_frontmatter:r.matchFrontmatter}:s===void 0?r:{...r,matchFrontmatter:s}}),o=ye(t);if(o){e.push(o.replaceAll("matchFrontmatter","match_frontmatter"));return}return t}u(Ae,"rulesFromDTO");function Z(n,e){const t={},o=u(a=>{const c=n[a];if(c!==void 0){if(typeof c!="string"){e.push(`\`${a}\` must be a string`);return}return c}},"str"),i=o("preview");i!==void 0&&(i.trim()===""||/[\r\n]/.test(i)?e.push("`preview` must be one non-empty line"):t.preview=i);const s=o("summary");s!==void 0&&(t.summary=s);const r=o("why_it_exists");if(r!==void 0&&(t.whyItExists=r),n.privacy!==void 0&&(U.includes(n.privacy)?t.privacy=n.privacy:e.push(`\`privacy\` must be one of ${U.join(", ")}`)),n.unlisted!==void 0&&(typeof n.unlisted!="boolean"?e.push("`unlisted` must be a boolean"):t.unlisted=n.unlisted),n.lint_ignore!==void 0){const a=n.lint_ignore;!Array.isArray(a)||a.some(c=>typeof c!="string")?e.push("`lint_ignore` must be a list of strings"):t.lintIgnore=a}if(n.extensions!==void 0){const a=n.extensions;typeof a!="object"||a===null||Array.isArray(a)||Object.values(a).some(c=>typeof c!="object"||c===null||Array.isArray(c))?e.push("`extensions` must map each plugin to an object of fields"):t.extensions=a}const f=Ae(n.rules,e);return f!==void 0&&(t.rules=f),t}u(Z,"fieldsFrom");function J(n){if(n.length>0)throw b(n.join("; "),{errors:n})}u(J,"failOn");async function Q(n,e,t,o){const i=await _e(K(t),async s=>"remote"in await X(n,e,s));return{...o.length>0?{unresolved_links:o}:{},...i.length>0?{remote_links:i}:{}}}u(Q,"linkReport");async function Oe(n){const e=R(n),t=[],o=e.name;(typeof o!="string"||!Y.test(o))&&t.push("`name` must be segments of letters, digits, `_` or `-` joined by `/`"),typeof e.body!="string"&&t.push("`body` must be a string"),typeof e.preview!="string"&&t.push("`preview` is required");const i=e.kind??"knowledge";q.includes(i)||t.push(i==="report"?"reports are written only by `crtr push`":`\`kind\` must be one of ${q.join(", ")}`);const s=Z(e,t);e.cwd!==void 0&&typeof e.cwd!="string"&&t.push("`cwd` must be a string"),J(t);const r=e,{dto:f,change:a}=await k(async c=>{const w=await E(c,n),d=await M(c,w,r.owner,r.cwd);if(!await V(c,w,d))throw new _(404,"not_found",`no owner ${d.name} you can write to`);try{const{object:p,unresolved:l}=await pe(c,w,{...s,owner:d,name:r.name,kind:i,preview:s.preview,privacy:s.privacy??"public"},r.body,await C(c));return{dto:{id:p.id,name:await j(c,p),...await Q(c,w,r.body,l)},change:await N(c,w,[p],r.cwd)}}catch(p){S(p)}});return await T(a),f}u(Oe,"write");function je(n,e){let t=n;e.forEach(({old:o,new:i},s)=>{const r=o===""?-1:t.indexOf(o);if(r<0)throw y(422,"replace_no_match",`replace[${s}]: old text not found in the revision you read`,"Copy the old text exactly from your last read and rerun.",{index:s});if(t.indexOf(o,r+1)>=0)throw y(422,"replace_ambiguous",`replace[${s}]: old text occurs more than once`,"Include more surrounding text so the old text matches once, and rerun.",{index:s});t=t.slice(0,r)+i+t.slice(r+o.length)})}u(je,"checkReplacements");async function De(n){const e=R(n),t=[],o=e.body!==void 0,i=e.replace!==void 0;o&&i&&t.push("give `body` or `replace`, not both"),o&&typeof e.body!="string"&&t.push("`body` must be a string");const s=e.replace;i&&(!Array.isArray(s)||s.length===0||s.some(d=>typeof d!="object"||d===null||typeof d.old!="string"||typeof d.new!="string"))&&t.push("`replace` must be a non-empty list of {old, new} strings"),(typeof e.rationale!="string"||e.rationale.trim()==="")&&t.push("`rationale` is required"),e.verbatim!==void 0&&typeof e.verbatim!="boolean"&&t.push("`verbatim` must be a boolean"),e.base!==void 0&&(typeof e.base!="string"||e.base==="")&&t.push("`base` must be a non-empty heads key"),e.base!==void 0&&!o&&!i&&t.push("`base` applies only to a `body` or `replace` edit");const r=Z(e,t),f=e.unset??[];if(!Array.isArray(f)||f.some(d=>!H.includes(d)))t.push(`\`unset\` takes only ${H.join(", ")}`);else for(const d of f)d==="summary"&&(r.summary=null),d==="why_it_exists"&&(r.whyItExists=null),d==="unlisted"&&(r.unlisted=!1),d==="lint_ignore"&&(r.lintIgnore=null),d==="extensions"&&(r.extensions=null),d==="rules"&&(r.rules=[]);t.length===0&&!o&&!i&&Object.keys(r).length===0&&t.push("nothing to change: give `body`, `replace`, or a field"),J(t);const a=e,{dto:c,synced:w}=await k(async d=>{const p=await E(d,n),l=await W(d,p,n.params.ref),g=await j(d,l),A=await N(d,p,[l]);try{if(!o&&!i)return await le(d,p,l,r),{dto:{id:l.id,name:g,revision:(await F(d,l)).revision},synced:A};const v=a.base??await fe(d,p,l);if(i&&v!==null){const B=await F(d,l),P=await x(d,l);B.source_path===null&&B.heads_key===v&&P.length===1&&je(await me(d,l,O(P[0].body_hash)),a.replace)}const D=a.verbatim===!0,m=o?{body:a.body,verbatim:D}:{replace:a.replace,verbatim:D},h=await ce(d,p,l,m,a.rationale,v,r,await C(d));if("stale"in h)throw y(409,"stale_revision",`${g} changed after your read; it is now at revision ${h.stale.revision}`,`Read it again with \`crtr canvas read ${g}\`, then redo your change on the current text.`,{stale:h.stale});const I=O((await x(d,l))[0].body_hash);return{dto:{id:l.id,name:g,revision:h.revision,...await Q(d,p,I,h.unresolved)},synced:A}}catch(v){S(v,l)}});return await T(w),c}u(De,"edit");async function Re(n){const e=R(n);if(e.new_name===void 0&&e.owner===void 0)throw b("give a new name, an owner, or both");if(e.new_name!==void 0&&(typeof e.new_name!="string"||!Y.test(e.new_name)))throw b("`new_name` must be segments of letters, digits, `_` or `-` joined by `/`");const{dto:t,change:o}=await k(async i=>{const s=await E(i,n),r=await W(i,s,n.params.ref),f=e.owner===void 0?void 0:await M(i,s,e.owner,e.cwd);try{const a=await ue(i,s,r,{...e.new_name!==void 0?{name:e.new_name}:{},...f?{owner:f}:{}},await C(i));return{dto:{id:a.id,name:await j(i,a)},change:await N(i,s,[r,a],e.cwd)}}catch(a){S(a,r)}});return await T(o),t}u(Re,"move");async function Ce(n){const{dto:e,change:t}=await k(async o=>{const i=await E(o,n),s=await W(o,i,n.params.ref),r=await j(o,s),f=await N(o,i,[s]);try{await se(o,i,s,await C(o))}catch(a){S(a,s)}return{dto:{id:s.id,name:r},change:f}});return await T(t),e}u(Ce,"remove");async function Se(n){const e=n.query.get("diff"),t=e===null?void 0:Number(e);if(t!==void 0&&!(Number.isInteger(t)&&t>0))throw b("`diff` must be a revision number");return await k(async o=>{const i=await E(o,n),s=await z(o,i,decodeURIComponent(n.params.ref));if("remote"in s)throw y(404,"not_found",G(s.remote),"Check the name with `crtr canvas search`.");const r="deleted"in s?s.deleted:s;if(r.type!=="document")throw b(`${r.name} is a ${r.type}, not a document`);const f=await de(o,i,r),a={object:await Ee(o,r),revisions:f.map(c=>({rev_id:c.rev_id,number:c.number,author_id:c.author,rationale:c.rationale,verbatim:c.verbatim,created_at:c.created_at,parents:c.parents}))};if(t!==void 0){const c=f.find(d=>d.number===t);if(!c)throw new _(404,"not_found",`${r.name} has no revision ${t}`);const w=c.parents[0]===void 0?void 0:f.find(d=>d.rev_id===c.parents[0]);a.diff=ge(w?O(w.body_hash):"",O(c.body_hash))}return a})}u(Se,"revisionHistory");const ee=1e3,te=1e3,ne=3e3,Ne=/^When\b.+\bbecause\b.+/,Te=u(n=>{const e=n.trim();return e===""?0:e.split(/\s+/).length},"words");function Ie(n,e){if(n.some(o=>o.on==="boot"&&o.deliver==="content")&&e>ee)return`body is ${e} words, over the ${ee}-word cap for a boot rule at content (the whole body enters every node's context). Keep the core here and move separate subjects into [[linked]] documents.`;const t=[...new Set(n.filter(o=>o.on!=="boot"&&o.deliver==="content").map(o=>o.on))];return t.length>0&&e>te?`body is ${e} words, over the ${te}-word cap for a content rule on ${t.join("/")}. Keep the core here and move separate subjects into [[linked]] documents.`:n.some(o=>o.on==="boot"&&o.deliver==="preview")&&e>ne?`body is ${e} words, over the ${ne}-word cap for a boot rule at preview. Keep the core here and move separate subjects into [[linked]] documents.`:null}u(Ie,"lengthFinding");async function Fe(n){const e=R(n);return await k(async t=>{const o=await E(t,n),i=e.owner===void 0?null:await M(t,o,e.owner,e.cwd),s=await t.prepare("SELECT * FROM objects WHERE type = 'document' AND deleted_at IS NULL AND home_machine IS NULL AND kind != 'report' ORDER BY name").all(),r=[],f=new Map;for(const a of s){if(i&&a.owner!==i.id||!await V(t,o,a))continue;const c=await F(t,a),w=new Set(c.lint_ignore??[]),d=await j(t,a),p=u((m,h,I)=>{w.has(m)||r.push({name:d,severity:h,rule:m,message:I})},"add"),l=await ve(t,a),g=(await x(t,a))[0],A=g?O(g.body_hash):"",v=Ie(l,Te(A));v&&p("length","error",v),l.some(m=>m.deliver==="preview")&&!Ne.test(a.preview??"")&&p("preview-shape","error",`a rule delivers this document at preview, so the preview must read "When <situation in the reader's work>, read this because <what the reader gains>"`);const D=new Set((await t.prepare("SELECT written_as FROM edges WHERE type = 'links-to' AND src = ?").all(a.id)).map(m=>m.written_as));for(const m of K(A)){if(D.has(m))continue;const h=await X(t,o,m);"remote"in h||"object"in h||"deleted"in h||p("links","warning",`[[${m}]] names no object in your view`)}if(l.some(m=>m.on==="workspace-open"&&m.deliver==="content")){const m=await ae(t,a);m.type==="repo"&&!w.has("workspace-open")&&f.set(m.name,[...f.get(m.name)??[],d])}}for(const[a,c]of f)if(!(c.length<2))for(const w of c)r.push({name:w,severity:"warning",rule:"workspace-open",message:`repo ${a} has ${c.length} documents delivered at content on workspace-open (${c.join(", ")}); keep one and link the rest from it`});return{findings:r}})}u(Fe,"lint");const $=u((n,e=200)=>async t=>({status:e,body:await n(t)}),"ok"),et=[{method:"POST",pattern:"/v1/docs",handler:$(Oe,201)},{method:"PATCH",pattern:"/v1/docs/:ref",handler:$(De)},{method:"POST",pattern:"/v1/docs/:ref/move",handler:$(Re)},{method:"DELETE",pattern:"/v1/docs/:ref",handler:$(Ce)},{method:"GET",pattern:"/v1/docs/:ref/history",handler:$(Se)},{method:"POST",pattern:"/v1/docs/lint",handler:$(Fe)}];export{j as canonicalName,et as docRoutes,N as repoChange,T as syncChanged};
@@ -1 +1 @@
1
- var b=Object.defineProperty;var o=(e,t)=>b(e,"name",{value:t,configurable:!0});import{validatePageTicketResponses as v}from"../../../core/human/tickets.js";import{getRequest as p}from"../../../core/human/requests.js";import{RunError as u}from"../../../core/runs/operations.js";import{getQuestion as R,questionObject as l,questionResult as A,questionStatus as m}from"../../../core/runs/questions.js";import{answerPageTicket as O,cancelHumanTicket as S}from"../../human/finish.js";import{requireRun as T}from"../router.js";const H=new Set(["question.get","question.answer","question.cancel"]),w=o(()=>new u(404,"not_found","question not found"),"missing");async function q(e,t,n){const s=await R(t);if(s===null)throw w();if(s.run===null){if(s.approval?.grantee!==e.grantee||s.approval.sub!==e.sub)throw w()}else try{await T(e,s.run.run_id,n)}catch(r){throw r instanceof u&&r.code==="not_found"?w():r}return s}o(q,"reachable");async function d(e){const t=await p(e);return new u(409,"run_settled",`question ${e} is already ${m(t)}`,void 0,{state:m(t),result:A(t)})}o(d,"settled");function j(e){return/slot(?: id)? "([^"]+)"/.exec(e)?.[1]??"responses"}o(j,"slotParam");async function U(e,t,n,s){if(e==="question.get"){const{request:a,run:c,approval:_}=await q(t,n,!1);return{status:200,body:await l(a,c,_)}}const{request:r,run:y,approval:g}=await q(t,n,!0);if(r.state==="settled")throw await d(n);if(e==="question.cancel"){if((await S(r.request_id,{reason:"dismissed from the app",actor:t.grantee,disposition:"dismissed"})).outcome==="already_settled")throw await d(n);return{status:200,body:await l(await p(n),y,g)}}const i=s&&typeof s=="object"&&!Array.isArray(s)?s.responses:void 0;if(!i||typeof i!="object"||Array.isArray(i))throw new u(400,"invalid_request","responses must be an object keyed by slot id","responses");try{await v(r.request_dir,i)}catch(a){const c=a instanceof Error?a.message:String(a);throw new u(400,"invalid_request",c,j(c))}const f=await O(r.request_id,i,{actor:t.grantee});if(f.outcome==="already_settled")throw await d(n);const h=f.result;return{status:200,body:{question_id:r.request_id,status:"answered",answer:h.responses}}}o(U,"handleQuestionOperation");export{H as QUESTION_OPERATIONS,U as handleQuestionOperation};
1
+ var b=Object.defineProperty;var o=(e,t)=>b(e,"name",{value:t,configurable:!0});import{validateFinalPageResponses as v}from"../../../core/human/tickets.js";import{getRequest as p}from"../../../core/human/requests.js";import{RunError as u}from"../../../core/runs/operations.js";import{getQuestion as O,manifestOf as R,questionObject as l,questionResult as A,questionStatus as m}from"../../../core/runs/questions.js";import{answerPageTicket as S,cancelHumanTicket as j}from"../../human/finish.js";import{requireRun as E}from"../router.js";const H=new Set(["question.get","question.answer","question.cancel"]),f=o(()=>new u(404,"not_found","question not found"),"missing");async function q(e,t,n){const s=await O(t);if(s===null)throw f();if(s.run===null){if(s.approval?.grantee!==e.grantee||s.approval.sub!==e.sub)throw f()}else try{await E(e,s.run.run_id,n)}catch(r){throw r instanceof u&&r.code==="not_found"?f():r}return s}o(q,"reachable");async function w(e){const t=await p(e);return new u(409,"run_settled",`question ${e} is already ${m(t)}`,void 0,{state:m(t),result:A(t)})}o(w,"settled");function P(e){return/slot(?: id)? "([^"]+)"/.exec(e)?.[1]??"responses"}o(P,"slotParam");async function U(e,t,n,s){if(e==="question.get"){const{request:a,run:c,approval:_}=await q(t,n,!1);return{status:200,body:await l(a,c,_)}}const{request:r,run:y,approval:g}=await q(t,n,!0);if(r.state==="settled")throw await w(n);if(e==="question.cancel"){if((await j(r.request_id,{reason:"dismissed from the app",actor:t.grantee,disposition:"dismissed"})).outcome==="already_settled")throw await w(n);return{status:200,body:await l(await p(n),y,g)}}const i=s&&typeof s=="object"&&!Array.isArray(s)?s.responses:void 0;if(!i||typeof i!="object"||Array.isArray(i))throw new u(400,"invalid_request","responses must be an object keyed by slot id","responses");try{v(R(r),i)}catch(a){const c=a instanceof Error?a.message:String(a);throw new u(400,"invalid_request",c,P(c))}const d=await S(r.request_id,i,{actor:t.grantee});if(d.outcome==="already_settled")throw await w(n);const h=d.result;return{status:200,body:{question_id:r.request_id,status:"answered",answer:h.responses}}}o(U,"handleQuestionOperation");export{H as QUESTION_OPERATIONS,U as handleQuestionOperation};
@@ -1,3 +1,4 @@
1
+ import type { NodeMeta } from '../../core/canvas/types.js';
1
2
  import { type Cron } from '../../core/canvas/crons.js';
2
3
  export type CronEnd = {
3
4
  kind: 'expired';
@@ -5,6 +6,8 @@ export type CronEnd = {
5
6
  kind: 'canceled';
6
7
  by: string;
7
8
  };
9
+ /** Can this node still take waking mail: not being deleted (nor its run root), and either has a cycle ahead or is parked. */
10
+ export declare function canTakeMail(node: NodeMeta): Promise<boolean>;
8
11
  /** Delete `c` and, in the same write, mail its node sink the reason. Returns
9
12
  * whether this call deleted the row. `end: null` deletes silently (the sink
10
13
  * node canceled it itself). The notice is skipped when the sink is not a node
@@ -1,2 +1,2 @@
1
- var p=Object.defineProperty;var r=(e,n)=>p(e,"name",{value:n,configurable:!0});import{getNode as c}from"../../core/canvas/canvas.js";import{withCanvasWrite as f}from"../../core/canvas/db.js";import{CRON_RUN_RING as h,cancelCron as m,listCronRuns as y,parseCronSink as g}from"../../core/canvas/crons.js";import{hasNoNaturalCycle as $}from"../../core/runtime/revive-all.js";import{isParked as w}from"../../core/runtime/lifecycle.js";import{enqueueNodeMail as _}from"../../core/feed/messages.js";import{cadenceDisplay as x}from"../../core/wake.js";const v=/^delivered: /,k=r(e=>e.exit_code===0&&(e.stdout_head??"")==="","EMPTY_OK");function b(e){return e.filter(n=>n.exit_code!==null||!(n.delivered??"").startsWith("skipped"))}r(b,"realRuns");function N(e,n,l){const t=b(l),i=l.length>=h,d=t.find(u=>v.test(u.delivered??"")),a=n.kind==="expired"?`expired at ${e.expires_at} and was deleted${e.held?" while held by exit 75; the pending occurrence never fired":""}`:`was canceled by ${n.by} and deleted`,s=[`Cron "${e.name}" (${e.cron_id}, ${e.recur!=null?x(e.recur):"one-shot"}) ${a}. It was delivering to you, so nothing will wake you from it any more.`,"",`- runs made: ${i?`at least ${t.length} (the run log keeps the last ${h} entries)`:t.length}`,d!==void 0?`- last delivery to you: ${d.finished}`:`- last delivery to you: ${i?`none in its last ${t.length} runs`:"never delivered"}`],o=t[0];if(o!==void 0){const u=o.exit_code===null?"no exit observed":o.exit_code===-1?"timed out":`exit ${o.exit_code}`;s.push(`- last run: ${o.finished} \u2014 ${u} \u2014 ${o.delivered??""}`)}return t.length>0&&t.every(k)&&s.push("","Every recorded run exited 0 with empty stdout, so the check may have been failing without saying so. A pipeline exits with its last stage, so `probe 2>&1 | grep \u2026 | jq \u2026` hides a failed probe unless the command starts with `set -o pipefail`."),{label:`\u23F0 cron ${e.name} ${n.kind==="expired"?"expired":"canceled"}`,body:s.join(`
2
- `)}}r(N,"endedMessage");async function C(e){return e.deleting_at!=null||e.run_id!=null&&(await c(e.run_id))?.deleting_at!=null}r(C,"runIsDeleting");async function K(e,n,l){const t=g(e.sink),i=t?.kind==="node"?await c(t.node):null,d=n!==null&&i!==null&&!await C(i)&&(!$(i)||w(i));return await f(async()=>{const a=d?await y(e.cron_id):[];if(!await m(e.cron_id,l))return!1;if(d){const{label:s,body:o}=N(e,n,a);await _(i.node_id,{kind:"message",label:s,senderNodeId:null,bodyJson:{body:o},urgency:"normal",wake:1,idempotencyKey:`cron-ended:${e.cron_id}`})}return!0})}r(K,"endCron");export{K as endCron};
1
+ var f=Object.defineProperty;var r=(e,n)=>f(e,"name",{value:n,configurable:!0});import{getNode as c}from"../../core/canvas/canvas.js";import{withCanvasWrite as h}from"../../core/canvas/db.js";import{CRON_RUN_RING as p,cancelCron as y,listCronRuns as m,parseCronSink as g}from"../../core/canvas/crons.js";import{hasNoNaturalCycle as w}from"../../core/runtime/revive-all.js";import{isParked as $}from"../../core/runtime/lifecycle.js";import{enqueueNodeMail as _}from"../../core/feed/messages.js";import{cadenceDisplay as x}from"../../core/wake.js";const v=/^delivered: /,k=r(e=>e.exit_code===0&&(e.stdout_head??"")==="","EMPTY_OK");function b(e){return e.filter(n=>n.exit_code!==null||!(n.delivered??"").startsWith("skipped"))}r(b,"realRuns");function N(e,n,d){const t=b(d),o=d.length>=p,a=t.find(u=>v.test(u.delivered??"")),l=n.kind==="expired"?`expired at ${e.expires_at} and was deleted${e.held?" while held by exit 75; the pending occurrence never fired":""}`:`was canceled by ${n.by} and deleted`,s=[`Cron "${e.name}" (${e.cron_id}, ${e.recur!=null?x(e.recur):"one-shot"}) ${l}. It was delivering to you, so nothing will wake you from it any more.`,"",`- runs made: ${o?`at least ${t.length} (the run log keeps the last ${p} entries)`:t.length}`,a!==void 0?`- last delivery to you: ${a.finished}`:`- last delivery to you: ${o?`none in its last ${t.length} runs`:"never delivered"}`],i=t[0];if(i!==void 0){const u=i.exit_code===null?"no exit observed":i.exit_code===-1?"timed out":`exit ${i.exit_code}`;s.push(`- last run: ${i.finished} \u2014 ${u} \u2014 ${i.delivered??""}`)}return t.length>0&&t.every(k)&&s.push("","Every recorded run exited 0 with empty stdout, so the check may have been failing without saying so. A pipeline exits with its last stage, so `probe 2>&1 | grep \u2026 | jq \u2026` hides a failed probe unless the command starts with `set -o pipefail`."),{label:`\u23F0 cron ${e.name} ${n.kind==="expired"?"expired":"canceled"}`,body:s.join(`
2
+ `)}}r(N,"endedMessage");async function C(e){return e.deleting_at!=null||e.run_id!=null&&(await c(e.run_id))?.deleting_at!=null}r(C,"runIsDeleting");async function R(e){return!await C(e)&&(!w(e)||$(e))}r(R,"canTakeMail");async function K(e,n,d){const t=g(e.sink),o=t?.kind==="node"?await c(t.node):null,a=n!==null&&o!==null&&await R(o);return await h(async()=>{const l=a?await m(e.cron_id):[];if(!await y(e.cron_id,d))return!1;if(a){const{label:s,body:i}=N(e,n,l);await _(o.node_id,{kind:"message",label:s,senderNodeId:null,bodyJson:{body:i},urgency:"normal",wake:1,idempotencyKey:`cron-ended:${e.cron_id}`})}return!0})}r(K,"endCron");export{R as canTakeMail,K as endCron};
@@ -0,0 +1,5 @@
1
+ import type { Cron } from '../../core/canvas/crons.js';
2
+ import type { CronSinkResolution } from './sinks.js';
3
+ /** The notice for a paused cron: mail to the node that armed it while that node can still take mail,
4
+ * otherwise a human ticket. `message` is the refusal, which names the scope the grant lost. */
5
+ export declare function narrowedNotice(c: Cron, message: string): Promise<CronSinkResolution>;
@@ -0,0 +1,2 @@
1
+ var i=Object.defineProperty;var a=(e,o)=>i(e,"name",{value:o,configurable:!0});import{getNode as s}from"../../core/canvas/canvas.js";import{newNodeId as c}from"../../core/runtime/nodes.js";import{resolveLiveProfileName as l}from"./armed-context.js";import{canTakeMail as u}from"./ended.js";async function g(e,o){const n=e.created_by===null?null:await s(e.created_by),r=[`Cron "${e.name}" (${e.cron_id}) was paused: ${o}.`,"","Its scheduled fire was refused because the cron was armed with that scope and the grant it runs under no longer offers it. The cron will not fire again until you act. Nothing was widened.","",`- To keep the scope: restore it on the grant, then \`crtr cron resume ${e.cron_id}\`.`,`- To run without it: \`crtr cron cancel ${e.cron_id}\` and arm the cron again. A cron armed by a node with no scope limit now follows the grant's current scopes.`].join(`
2
+ `),t=`\u23F0 cron ${e.name} paused: scope no longer granted`,d=e.cancel_on_wake&&e.anchor_node===n?.node_id;return n!==null&&!d&&await u(n)?{kind:"node",destinationNodeId:n.node_id,senderNodeId:null,label:t,stdout:r,urgency:"urgent"}:{kind:"action",adapter:{adapter:"cron_human",cronId:e.cron_id,requestId:await c(),title:t,summary:"A scheduled cron was paused because its grant lost a scope.",sessionName:`cron ${e.name} (${e.cron_id})`,profileName:l(e)??null},bodyJson:{stdout:r}}}a(g,"narrowedNotice");export{g as narrowedNotice};
@@ -1 +1 @@
1
- var p=Object.defineProperty;var c=(e,u)=>p(e,"name",{value:u,configurable:!0});import{consumeCron as _,recordCronRunLocked as y,releaseCronRunLease as w,setCronState as I}from"../../core/canvas/crons.js";import{withCanvasWrite as b}from"../../core/canvas/db.js";import{enqueueDaemonAction as k}from"../../core/canvas/daemon-actions.js";import{enqueueNodeMail as v}from"../../core/feed/messages.js";import{emitEvent as g}from"../../core/events/emit.js";import{disposeSettledRun as C}from"./disposition.js";function h(e){return typeof e=="object"&&(e.kind==="park"||e.kind==="redue")}c(h,"occurrenceStaysOwed");async function M(e,u){const{outOfBand:a,outcome:t,record:n,lease:s}=u;let o,i,d;if(t.kind==="disposed"){const r=await C(e,t.ctx,a,t.selfCanceled??!1);o=r.delivered,i=r.paused,d=r.obligation}else t.kind==="refused"?(o=`refused: ${t.reason}`,i=!a&&e.recur==null,i&&await I(e.cron_id,"paused"),g({level:"warn",event:"cron.fire.refused",fields:{cron_id:e.cron_id,execution_id:n.runId,grant_id:e.grant_id,reason:t.reason,out_of_band:a}})):(o=t.delivered,i=t.paused??!1);const f=await b(async r=>{d!==void 0&&(d.kind==="node"?o=`delivered: ${(await v(d.destinationNodeId,{kind:"message",label:d.label,senderNodeId:d.senderNodeId,bodyJson:{body:d.stdout},urgency:d.urgency,wake:1,idempotencyKey:n.runId})).deliveryId}`:d.kind==="action"&&(o=`delivered: ${await k({adapter:d.adapter,bodyJson:d.bodyJson,idempotencyKey:n.runId})}`));const l={run_id:n.runId,cron_id:e.cron_id,started:n.startedAtIso,finished:new Date(n.finishedAtMs).toISOString(),duration_ms:n.durationMs,exit_code:n.exitCode,stdout_head:n.stdoutHead,stderr_head:n.stderrHead,delivered:o};return await y(r,l),l});return s!=="none"&&await w(e.cron_id,s==="release"?void 0:s),!a&&e.recur==null&&!i&&!h(s)&&await _(e.cron_id),f}c(M,"settleRun");export{M as settleRun};
1
+ var p=Object.defineProperty;var c=(e,u)=>p(e,"name",{value:u,configurable:!0});import{consumeCron as w,recordCronRunLocked as _,releaseCronRunLease as y,setCronState as k}from"../../core/canvas/crons.js";import{withCanvasWrite as I}from"../../core/canvas/db.js";import{enqueueDaemonAction as b}from"../../core/canvas/daemon-actions.js";import{enqueueNodeMail as v}from"../../core/feed/messages.js";import{emitEvent as g}from"../../core/events/emit.js";import{narrowedNotice as C}from"./narrowed.js";import{disposeSettledRun as h}from"./disposition.js";function x(e){return typeof e=="object"&&(e.kind==="park"||e.kind==="redue")}c(x,"occurrenceStaysOwed");async function $(e,u){const{outOfBand:t,outcome:d,record:o,lease:s}=u;let i,a,n;if(d.kind==="disposed"){const r=await h(e,d.ctx,t,d.selfCanceled??!1);i=r.delivered,a=r.paused,n=r.obligation}else if(d.kind==="refused"){i=`refused: ${d.reason}`;const r=!t&&d.reason==="scope_narrowed";a=!t&&(e.recur==null||r),a&&await k(e.cron_id,"paused"),r&&(n=await C(e,d.message)),g({level:"warn",event:"cron.fire.refused",fields:{cron_id:e.cron_id,execution_id:o.runId,grant_id:e.grant_id,reason:d.reason,out_of_band:t}})}else i=d.delivered,a=d.paused??!1;const m=await I(async r=>{if(n!==void 0){if(n.kind==="node"){const f=await v(n.destinationNodeId,{kind:"message",label:n.label,senderNodeId:n.senderNodeId,bodyJson:{body:n.stdout},urgency:n.urgency,wake:1,idempotencyKey:o.runId});d.kind!=="refused"&&(i=`delivered: ${f.deliveryId}`)}else if(n.kind==="action"){const f=await b({adapter:n.adapter,bodyJson:n.bodyJson,idempotencyKey:o.runId});d.kind!=="refused"&&(i=`delivered: ${f}`)}}const l={run_id:o.runId,cron_id:e.cron_id,started:o.startedAtIso,finished:new Date(o.finishedAtMs).toISOString(),duration_ms:o.durationMs,exit_code:o.exitCode,stdout_head:o.stdoutHead,stderr_head:o.stderrHead,delivered:i};return await _(r,l),l});return s!=="none"&&await y(e.cron_id,s==="release"?void 0:s),!t&&e.recur==null&&!a&&!x(s)&&await w(e.cron_id),m}c($,"settleRun");export{$ as settleRun};
@@ -15,7 +15,7 @@ A provider is a service the person's runtime calls for your app. Your app asks t
15
15
  | `context` | Ask about the person from their memory, and look up public profiles | `context:<group>` | [Context](/docs/providers/context) |
16
16
  | `device` | The person's Mac: browser, files, Messages and shell | `device:<group>` | [Device](/docs/providers/device) |
17
17
 
18
- > **Want your app to have its own number and text its users?** The `sms` provider texts only the signed-in person's own phone. For your own number, use Comms: [the Comms guide for coding agents](https://comms.northlight.ai/llms.txt), and the SDK [`@north-light/comms`](https://www.npmjs.com/package/@north-light/comms) on npm.
18
+ > **Want your app to have its own number and text its users?** The `sms` provider texts only the signed-in person's own phone. For your own number, use Comms: [the Comms guide for coding agents](https://comms.northlight.ai/llms.txt), and the SDK [`@north-light/comms`](https://www.npmjs.com/package/@north-light/comms) on npm. Other apps that offer a service to developers are listed under [Services you can build on](/docs/services).
19
19
 
20
20
  ## Where names come from
21
21
 
@@ -5,7 +5,7 @@ description: Text the signed-in person's own verified phone from your app with t
5
5
 
6
6
  # Texting (sms)
7
7
 
8
- > **Want your app to have its own number and text its users?** The `sms` provider cannot: it texts only the signed-in person's own phone. Use Comms instead: [the Comms guide for coding agents](https://comms.northlight.ai/llms.txt) covers fake mode, testers and production, and the SDK is [`@north-light/comms`](https://www.npmjs.com/package/@north-light/comms) on npm.
8
+ > **Want your app to have its own number and text its users?** The `sms` provider cannot: it texts only the signed-in person's own phone. Use Comms instead: [the Comms guide for coding agents](https://comms.northlight.ai/llms.txt) covers fake mode, testers and production, and the SDK is [`@north-light/comms`](https://www.npmjs.com/package/@north-light/comms) on npm. See also [Services you can build on](/docs/services).
9
9
 
10
10
  The `sms` provider sends a text from Northlight's number to the verified phone of the person who signed in to your app. It has no recipient argument, so an app cannot text anyone else.
11
11
 
@@ -9,6 +9,11 @@ Changes to `@north-light/sdk` that can change what an existing app sees. Newest
9
9
 
10
10
  ## 0.3.533
11
11
 
12
+ A cron armed by a caller with no scope limit follows the grant, and a narrowed scope pauses it.
13
+
14
+ - `POST /v1/crons` stores and returns `scopes: null` for a cron armed by a node, cron or app token with no scope limit, where it used to return a copy of the grant's scopes. A cron with `scopes: null` runs under the grant's current scopes at every fire, including scopes the grant gains later. A cron armed with an explicit scope list keeps it and is refused if the grant stops covering any of it.
15
+ - A scheduled fire refused `scope_narrowed` now pauses the cron, recurring ones included (`state: paused` in `GET /v1/crons`), and notifies whoever armed it. Resume it with `POST /v1/crons/:id/resume` once the grant covers the scope again.
16
+
12
17
  Three reads take optional narrowing. Additive: no request or response an existing app sends or reads changes.
13
18
 
14
19
  - `GET /v1/canvas/roster` node rows carry an optional `description`. A runtime that predates the field omits it.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@north-light/cli",
3
- "version": "0.3.533",
3
+ "version": "0.3.534",
4
4
  "description": "crtr — agent runtime with memory, plugins, and marketplaces",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -140,7 +140,7 @@
140
140
  },
141
141
  "license": "UNLICENSED",
142
142
  "dependencies": {
143
- "@north-light/identity": "0.3.533",
143
+ "@north-light/identity": "0.3.534",
144
144
  "@earendil-works/pi-agent-core": "1.1.0",
145
145
  "@earendil-works/pi-ai": "1.1.0",
146
146
  "@earendil-works/pi-coding-agent": "1.1.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@north-light/identity",
3
- "version": "0.3.533",
3
+ "version": "0.3.534",
4
4
  "description": "Northlight access-token verification and scope rules",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
package/runtime.lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@north-light/cli",
3
- "version": "0.3.533",
3
+ "version": "0.3.534",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@north-light/cli",
9
- "version": "0.3.533",
9
+ "version": "0.3.534",
10
10
  "hasInstallScript": true,
11
11
  "license": "UNLICENSED",
12
12
  "workspaces": [
@@ -24,7 +24,7 @@
24
24
  "@milkdown/preset-gfm": "^7.22.3",
25
25
  "@milkdown/prose": "^7.22.3",
26
26
  "@milkdown/utils": "^7.22.3",
27
- "@north-light/identity": "0.3.533",
27
+ "@north-light/identity": "0.3.534",
28
28
  "cron-parser": "^5.6.0",
29
29
  "esbuild": "^0.27.7",
30
30
  "jose": "^6.2.12",
@@ -12455,17 +12455,17 @@
12455
12455
  },
12456
12456
  "packages/crouter-api": {
12457
12457
  "name": "@north-light/api",
12458
- "version": "0.3.533",
12458
+ "version": "0.3.534",
12459
12459
  "license": "UNLICENSED"
12460
12460
  },
12461
12461
  "packages/crouter-env-docker": {
12462
12462
  "name": "@north-light/env-docker",
12463
- "version": "0.3.533",
12463
+ "version": "0.3.534",
12464
12464
  "license": "UNLICENSED"
12465
12465
  },
12466
12466
  "packages/crouter-identity": {
12467
12467
  "name": "@north-light/identity",
12468
- "version": "0.3.533",
12468
+ "version": "0.3.534",
12469
12469
  "license": "UNLICENSED",
12470
12470
  "dependencies": {
12471
12471
  "jose": "^6.2.1"
@@ -12473,20 +12473,20 @@
12473
12473
  },
12474
12474
  "packages/crouter-plugin": {
12475
12475
  "name": "@north-light/plugin",
12476
- "version": "0.3.533",
12476
+ "version": "0.3.534",
12477
12477
  "license": "UNLICENSED",
12478
12478
  "dependencies": {
12479
- "@north-light/api": "0.3.533",
12480
- "@north-light/identity": "0.3.533"
12479
+ "@north-light/api": "0.3.534",
12480
+ "@north-light/identity": "0.3.534"
12481
12481
  }
12482
12482
  },
12483
12483
  "packages/crouter-sdk": {
12484
12484
  "name": "@north-light/sdk",
12485
- "version": "0.3.533",
12485
+ "version": "0.3.534",
12486
12486
  "license": "UNLICENSED",
12487
12487
  "dependencies": {
12488
- "@north-light/api": "0.3.533",
12489
- "@north-light/identity": "0.3.533",
12488
+ "@north-light/api": "0.3.534",
12489
+ "@north-light/identity": "0.3.534",
12490
12490
  "jose": "^6.2.1"
12491
12491
  },
12492
12492
  "bin": {