@north-light/crouter 0.3.354 → 0.3.355
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/dto/docs.d.ts +2 -0
- package/dist/api/dto/objects.d.ts +1 -0
- package/dist/api/dto/reviews.d.ts +6 -4
- package/dist/builtin-memory/02-turn-lifecycle/02-resident.md +1 -0
- package/dist/builtin-memory/04-orchestration-kernel.md +10 -31
- package/dist/builtin-memory/internal/memory-loading.md +1 -1
- package/dist/clients/attach/overlays/file-review.d.ts +1 -0
- package/dist/clients/attach/overlays/file-review.js +2 -2
- package/dist/clients/attach/render/chat-view.d.ts +6 -0
- package/dist/clients/attach/render/chat-view.js +2 -2
- package/dist/clients/attach/render/doc-links.d.ts +34 -0
- package/dist/clients/attach/render/doc-links.js +1 -0
- package/dist/clients/attach/render/markdown-source.js +5 -5
- package/dist/clients/attach/session/doc-materialize.d.ts +53 -0
- package/dist/clients/attach/session/doc-materialize.js +3 -0
- package/dist/clients/attach/session/document-links.d.ts +21 -0
- package/dist/clients/attach/session/document-links.js +1 -0
- package/dist/clients/attach/session/file-links.d.ts +2 -0
- package/dist/clients/attach/session/file-links.js +1 -1
- package/dist/clients/attach/viewer.js +622 -620
- package/dist/clients/inbox/review/review-client.d.ts +5 -1
- package/dist/clients/inbox/review/review-client.js +1 -1
- package/dist/commands/canvas/read.js +4 -4
- package/dist/commands/doc/write.js +1 -1
- package/dist/commands/node/lifecycle.js +2 -2
- package/dist/core/graph/deletion.d.ts +5 -4
- package/dist/core/graph/deletion.js +3 -3
- package/dist/core/graph/documents.d.ts +8 -1
- package/dist/core/graph/documents.js +9 -6
- package/dist/core/graph/edges.d.ts +3 -2
- package/dist/core/graph/edges.js +1 -1
- package/dist/core/graph/events.d.ts +2 -2
- package/dist/core/graph/events.js +5 -4
- package/dist/core/graph/names.d.ts +3 -1
- package/dist/core/graph/names.js +3 -3
- package/dist/core/graph/repo-sync/exchange.js +2 -2
- package/dist/core/runs/operations.js +5 -5
- package/dist/core/runtime/broker-persona-guidance.js +2 -2
- package/dist/core/runtime/persona.js +2 -2
- package/dist/core/runtime/roadmap.d.ts +3 -0
- package/dist/core/runtime/roadmap.js +8 -4
- package/dist/core/substrate/delivery/deliver.js +1 -1
- package/dist/core/substrate/delivery/listings.d.ts +3 -0
- package/dist/core/substrate/delivery/listings.js +5 -2
- package/dist/core/substrate/delivery/render-boot.js +26 -26
- package/dist/core/substrate/delivery/render-event.d.ts +13 -6
- package/dist/core/substrate/delivery/render-event.js +6 -4
- package/dist/daemon/api/handlers/docs.js +1 -1
- package/dist/daemon/api/handlers/objects.js +5 -5
- package/dist/daemon/api/handlers/reviews.d.ts +4 -1
- package/dist/daemon/api/handlers/reviews.js +1 -1
- package/dist/shared/inbox-entry-body.js +7 -7
- package/package.json +2 -2
- package/packages/crouter-identity/package.json +1 -1
- package/runtime.lock.json +11 -11
- package/scripts/lib/isolation-env.mjs +8 -2
|
@@ -14,10 +14,14 @@ export interface ResolvedReview {
|
|
|
14
14
|
}
|
|
15
15
|
export interface ReviewClient {
|
|
16
16
|
byId(reviewId: string): Promise<ResolvedReview>;
|
|
17
|
+
/** Exactly one of a file path or a document ref. */
|
|
17
18
|
createInline(p: {
|
|
18
19
|
originNodeId: string;
|
|
20
|
+
} & ({
|
|
19
21
|
file: string;
|
|
20
|
-
}
|
|
22
|
+
} | {
|
|
23
|
+
document: string;
|
|
24
|
+
})): Promise<ResolvedReview>;
|
|
21
25
|
comments(reviewId: string): CommentAuthority;
|
|
22
26
|
submit(reviewId: string): Promise<SubmitDisposition>;
|
|
23
27
|
cancel(reviewId: string, reason: string): Promise<void>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
var u=Object.defineProperty;var r=(e,t)=>u(e,"name",{value:t,configurable:!0});import{randomUUID as l}from"node:crypto";import{ApiError as
|
|
1
|
+
var u=Object.defineProperty;var r=(e,t)=>u(e,"name",{value:t,configurable:!0});import{randomUUID as l}from"node:crypto";import{ApiError as w}from"../../../api/errors.js";import{cliClient as a}from"../../../commands/api-client.js";import{createCommentAuthority as p}from"./comments-client.js";class s extends Error{static{r(this,"ReviewTerminalError")}state;resultPath;reason;constructor(t,n){if(t==="approved"){if(n===void 0||n==="")throw new Error("approved review omitted its canonical result path");super(`review is already approved; see ${n}`),this.resultPath=n}else super(n===void 0||n===""?"review is already canceled":`review is already canceled: ${n}`),n!==void 0&&n!==""&&(this.reason=n);this.name="ReviewTerminalError",this.state=t}}function b(e={}){const t=r(async n=>{try{return await n()}catch(i){const o=h(i);throw o!==void 0&&e.onTerminalConflict?.(o),i}},"request");return{async byId(n){const i=await t(()=>a().getReview(n));return await c(i,i.companion_node_id,t)},async createInline(n){const i=await t(()=>a().createReview({origin_kind:"inline",origin_node_id:n.originNodeId,..."file"in n?{file:n.file}:{document:n.document},idempotency_key:l()}));return await c(i,i.companion_node_id,t)},comments(n){return p(n,t)},async submit(n){const i=await t(()=>a().submitReview(n));return i.outcome==="awaiting_companion"?{outcome:"awaiting_companion"}:{outcome:"approved",changed:m(i.review),resultPath:d(i.review)}},async cancel(n,i){await t(()=>a().cancelReview(n,{reason:i,actor:"human",disposition:"dismissed"}))}}}r(b,"createReviewClient");async function c(e,t,n){if(e.state!=="open")throw f(e);const i=await n(()=>a().getNode(t));return{reviewId:e.review_id,companionNodeId:t,file:e.file,cwd:i.cwd}}r(c,"resolveReview");function f(e){if(e.state!=="approved"&&e.state!=="canceled")throw new Error(`terminal review ${e.review_id} has invalid state ${e.state}`);return e.state==="approved"?new s("approved",d(e)):new s("canceled",e.cancel_reason)}r(f,"terminalError");function m(e){if(typeof e.changed!="boolean")throw new Error(`approved review ${e.review_id} omitted its changed disposition`);return e.changed}r(m,"requireChanged");function d(e){if(typeof e.result_path!="string"||e.result_path==="")throw new Error(`terminal review ${e.review_id} omitted its canonical result path`);return e.result_path}r(d,"requireResultPath");function T(e){return e.state==="approved"?`This review was already approved. Result: ${e.resultPath??"unknown"}`:e.reason===void 0||e.reason===""?"This review was canceled.":`This review was canceled: ${e.reason}`}r(T,"terminalRefusalNotice");function h(e){if(!(e instanceof w)||e.status!==409||e.code!=="review_terminal_conflict")return;const t=e.details?.review;if(t!==void 0){if(t.state==="approved")return t.result_path===void 0?"This review was already approved by someone else.":`This review was already approved by someone else. Result: ${t.result_path}`;if(t.state==="canceled")return t.cancel_reason===void 0||t.cancel_reason===""?"This review was canceled before this submission landed.":`This review was canceled before this submission landed: ${t.cancel_reason}`}}r(h,"terminalConflictNotice");export{s as ReviewTerminalError,b as createReviewClient,h as terminalConflictNotice,T as terminalRefusalNotice};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
var
|
|
2
|
-
`)}const n=e.object,t=[];switch(e.type){case"document":{e.attachments&&t.push(e.attachments,""),t.push(
|
|
1
|
+
var m=Object.defineProperty;var o=(e,n)=>m(e,"name",{value:n,configurable:!0});import{defineLeaf as u}from"../../core/command.js";import{cliClient as p,rethrowAsCliError as l}from"../api-client.js";const b="A name (the `[[ ]]` brackets are optional) or a raw object id. A name is `<owner-handle>/<path>`; a node or other owner is also named by its id or handle.";function a(e,n){return n==null||n===""?"":` ${e}="${String(n).replace(/"/g,""")}"`}o(a,"attr");function y(e){const n=e.kind?`${e.type}/${e.kind}`:e.type,t=e.owner?` owner=${e.owner.handle}`:"",i=e.deleted_at?" (deleted)":"";return`- ${e.id} [[${e.name}]] ${n}${t}${i} \u2014 ${e.preview}`}o(y,"objectRow");function r(e,n,t=""){return`<${e}${a("type",n.type)}${a("kind",n.kind)}${a("id",n.id)}${a("name",n.name)}${a("owner",n.owner?.handle)}${t}>`}o(r,"header");function s(e,n){return n.length===0?[]:[`${e}:`,...n.map(y)]}o(s,"section");function w(e){if(e.state==="remote")return`No object named ${e.remote} on this runtime. It may be on another runtime, or the name may be misspelled.`;if(e.state==="deleted"){const i=e.deleted;return[`${r("deleted",i,a("deleted_at",i.deleted_at))}`,`Last preview: ${i.preview}`,"</deleted>"].join(`
|
|
2
|
+
`)}const n=e.object,t=[];switch(e.type){case"document":{if(e.attachments&&t.push(e.attachments,""),t.push(r("object",n,a("privacy",n.privacy)+a("revision",e.revision))),t.push(`Preview: ${n.preview}`),e.summary&&t.push(`Summary: ${e.summary}`),e.fields!==void 0&&t.push(`<fields>
|
|
3
3
|
${JSON.stringify(e.fields,null,2)}
|
|
4
|
-
</fields>`),e.needs_merge?(e.heads.forEach((i,
|
|
5
|
-
`)}o(
|
|
4
|
+
</fields>`),e.needs_merge?(e.heads.forEach((i,c)=>{t.push(`<head${a("rev",i.rev_id)}${a("number",i.number)}${a("author",i.author_id)}>`,e.bodies[c]??"","</head>")}),t.push(`This document has ${e.heads.length} heads. Your next \`crtr doc edit\` writes the merged body and joins them into one revision.`)):t.push("",e.bodies[0]??""),e.listing&&e.listing.length>0){const i=e.listing.some(d=>d.owner?.id===n.owner?.id&&d.name.startsWith(`${n.name}/`)),c=e.listing.some(d=>d.owner?.id!==n.owner?.id||!d.name.startsWith(`${n.name}/`)),h=i&&c?`Names under ${n.name}/ and documents it links to`:i?`Names under ${n.name}/`:"Documents it links to";t.push("",...s(h,e.listing))}break}case"node":t.push(r("object",n,a("status",e.status))),t.push(`Mandate: ${e.mandate_first_line}`),t.push(...s("Children",e.children),...s("Owns",e.owned),...s("Jobs",e.jobs));break;case"job":t.push(r("object",n,a("status",e.status)+a("exit_code",e.exit_code))),t.push(`Command: ${e.command}`,`Log: ${e.log_path}`);break;default:t.push(r("object",n)),t.push(`Handle: ${e.handle}`,`Display name: ${e.display_name}`,`Identity: ${e.identity.scheme}:${e.identity.external_id}`)}return t.push("</object>"),t.join(`
|
|
5
|
+
`)}o(w,"renderRead");const g=u({name:"read",description:"read one object \u2014 a document, node, job or owner \u2014 by name or id",whenToUse:"you need the content or state of an object you can name: a document\u2019s body, a node\u2019s status and what it owns, a job\u2019s result. Reading makes you a watcher of the object. Find the name first with `crtr canvas search` or `crtr canvas list` when you do not know it.",help:{name:"canvas read",summary:"read one object by name or id and become a watcher of it",params:[{kind:"positional",name:"ref",required:!0,constraint:b},{kind:"flag",name:"fields",type:"bool",required:!1,constraint:"When present, a document read also returns its delivery rules, why-it-exists, unlisted, lint-ignore and plugin extension fields."}],output:[{name:"object",type:"string",required:!0,constraint:"`<object type kind id name owner \u2026>` header. A document adds privacy and revision, then its preview, summary and body; several heads show one `<head rev number author>` block each, and your next `crtr doc edit` merges them. A document lists the names under it and previews the documents it links to, as one section. It may be preceded by one `<auto-loaded-context>` block carrying the document\u2019s enclosing listings and the documents its `document-read` rules deliver; in a node, each is delivered once per context. A node shows its status, the first line of its mandate, its children, what it owns and its jobs; a job its command, status, exit code and log path; an owner its handle and identity."},{name:"deleted",type:"string",required:!1,constraint:"`<deleted \u2026>` with the last preview when the object was deleted."},{name:"remote",type:"string",required:!1,constraint:"A line saying no object has that name on this runtime: it may be on another runtime, or the name may be misspelled."}],outputKind:"object",effects:["A live read records a watch on the object for you (every event subject) and publishes a read event to its other watchers. A deleted or remote answer records nothing."]},run:o(async e=>{try{return{...await p().objects.read(e.ref,e.fields===!0?{fields:!0}:void 0)}}catch(n){l(n)}},"run"),render:o(e=>w(e),"render")});export{b as REF_CONSTRAINT,a as attr,y as objectRow,g as readLeaf,w as renderRead};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
var o=Object.defineProperty;var r=(e,n)=>o(e,"name",{value:n,configurable:!0});import{defineLeaf as a}from"../../core/command.js";import{cliClient as s,rethrowAsCliError as d}from"../api-client.js";import{FIELD_FLAGS as t,fieldsFromInput as m,ownerFromInput as
|
|
1
|
+
var o=Object.defineProperty;var r=(e,n)=>o(e,"name",{value:n,configurable:!0});import{defineLeaf as a}from"../../core/command.js";import{cliClient as s,rethrowAsCliError as d}from"../api-client.js";import{FIELD_FLAGS as t,fieldsFromInput as m,ownerFromInput as l,ownerParam as u,renderLinkReport as c,ruleParam as h}from"./shared.js";const b=a({name:"write",description:"write a new document from a stdin body",whenToUse:"a spec, plan, map, finding, knowledge or preference should exist as a document other nodes can read; creation only \u2014 revise an existing one with `crtr doc edit`",help:{name:"doc write",summary:"write a new document from a stdin body",guide:"Most documents need only a name, a preview and a body: the writing node owns it, it has no delivery rules, and other nodes read it when pointed at it as `[[<name printed below>]]`, and each `[[link]]` in its body attaches that document's preview wherever this one is delivered in full. Give --owner or --rule only when the document must outlive this node or load on its own; each has focused help. Read, list and search documents with `crtr canvas read|list|search`.",params:[{kind:"positional",name:"name",required:!0,constraint:"Name under its owner: segments of letters, digits, `_` and `-`, joined by `/`. Must be free under that owner."},{kind:"stdin",name:"body",required:!0,constraint:"Document body (markdown), piped on stdin and saved byte for byte."},{...t.preview("One line shown wherever the document is listed or delivered without its body, telling a reader whether to open it."),required:!0},t.kind,t.summary,t.privacy("Who beyond the owner may read it; default public. A repo-owned document must be public."),u("Whose document it is; default the writing node."),h("A delivery rule that loads the document into nodes by their activity."),t.whyItExists,t.unlisted("Keep it out of listings; reads, links and rules still work."),t.lintIgnore,t.extension],output:[{name:"name",type:"string",required:!0,constraint:"Full name, `<owner-handle>/<name>`; point at it as `[[name]]`."},{name:"id",type:"string",required:!0,constraint:"Object id."},{name:"unresolved_links",type:"string[]",required:!1,constraint:"`[[links]]` in the body that name no document yet; absent when every link resolves."},{name:"remote_links",type:"string[]",required:!1,constraint:"`[[links]]` whose first segment is no owner on this runtime: on another runtime, or misspelled."}],outputKind:"object",effects:["Creates the document at revision 1 and makes the writer a watcher, so it is pushed later edits."]},run:r(async e=>{const n={name:e.name,body:e.body,...m(e),...l(e),cwd:process.cwd()};try{return{...await s().docs.write(n)}}catch(i){d(i)}},"run"),render:r(e=>`<written name="${String(e.name)}" id="${String(e.id)}">${c(e)}`,"render")});export{b as writeLeaf};
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
var C=Object.defineProperty;var l=(e,t)=>C(e,"name",{value:t,configurable:!0});import{envNodeId as k}from"../../shared/env.js";import{defineBranch as
|
|
2
|
-
`)},"render")}),H=y({name:"close",description:"close a node + cascade-cancel its exclusive subtree",whenToUse:"you want to tear a node down WITHOUT finishing it, cascade-cancelling every descendant it exclusively owns: abandoning a line of work, killing a stuck or wrong-turn subtree, clearing a branch you no longer need. A node that never produced substantive assistant output is deleted and cannot be revived; streaming nodes and nodes with an open managed worktree are retained. Other closed nodes keep their pi session and can be revived later (`node lifecycle revive`). Use `node lifecycle recycle` instead to FINISH the agent in your pane with a final report, and `push final` when a worker should end its own work normally (Alt+C \u2192 x)",help:{name:"node lifecycle close",summary:"close a node and cascade-cancel its subtree \u2014 kill its tmux window plus those of every descendant it EXCLUSIVELY owns (down the subscribes_to spine), mark retained nodes canceled, and leave them a notice they read on resume. A descendant still subscribed to by a manager outside the subtree is left running. A node that never produced substantive assistant output is deleted and cannot be revived; streaming nodes and nodes with an open managed worktree are retained. Other closed nodes keep their pi session and can be revived later (`crtr node lifecycle revive`)",params:[{kind:"flag",name:"node",type:"string",required:!1,constraint:"Node to close. Defaults to the node occupying --pane (or your current pane)."},{kind:"flag",name:"pane",type:"string",required:!1,constraint:"tmux pane id whose node to close. Defaults to $TMUX_PANE / your current pane. The Alt+C menu passes this for you."}],output:[{name:"node_id",type:"string",required:!1,constraint:"The node that was closed \u2014 the cascade root."},{name:"count",type:"number",required:!1,constraint:"How many nodes were closed (root + cascaded descendants)."},{name:"closed_ids",type:"string[]",required:!1,constraint:"All closed node ids, kill order (leaves first, root last)."},{name:"spared",type:"string[]",required:!1,constraint:"Descendants left alive because a manager outside the subtree still subscribes to them."}],outputKind:"object",effects:["Marks retained nodes and their exclusive descendants `canceled` and clears intent (the daemon never revives a canceled node).","Deletes a non-streaming node with no substantive assistant output unless it has an open managed worktree; a deleted node cannot be revived.","Kills each closed node's tmux window; retained nodes keep their pi sessions and canvas edges for a later revive.","Appends a cancellation notice to each retained node's inbox, surfaced on its next resume."]},run:l(async e=>{const t=e.pane??process.env.TMUX_PANE;let n=e.node;if((n===void 0||n==="")&&(n=x(t)),n===void 0||n==="")throw new d({error:"no_node",message:"no node found in this pane to close",next:"Pass --node <id>, or run from inside the agent's pane."});const i=await f().closeNode(n,process.ppid>0?{caller_pid:process.ppid}:{}).catch(s=>p(s,"List nodes with `crtr node inspect list`."));return{node_id:i.root,count:i.closed.length,closed_ids:i.closed,spared:i.spared}},"run"),render:l(e=>{const t=e.spared?.length??0;return`Closed ${e.node_id} and its exclusive subtree \u2014 ${e.count} node(s) closed${t>0?`, ${t} spared (still managed from outside the subtree)`:""}.`},"render")}),A="Model change first: a running broker, including one still starting right after `node new`, switches over its socket; a dormant node persists the durable model override and launch recipe for its next revive.",ee=y({name:"config",description:"reconfigure a node's settings headlessly \u2014 model, lifecycle (terminal/resident), kind, mode, name; repair a missing cwd",whenToUse:"you want to reconfigure a node's settings headlessly after spawn \u2014 swap its model/provider, flip it terminal\u2194resident (interactable), respecialize its kind, switch base\u2194orchestrator mode, or rename it \u2014 without opening its viewer. It also repairs a dormant node whose recorded cwd is gone, starting its next revive in a fresh conversation. The durable attribute setter; reuses the same recipes a revive reads. Use `node promote` for the opinionated self-promotion path, and `node lifecycle` for teardown actions (recycle/close/demote).",help:{name:"node config",summary:"reconfigure a node's settings headlessly \u2014 model, lifecycle, kind, mode, name; repair a missing cwd",params:[{kind:"flag",name:"node",type:"string",required:!1,constraint:"Target node id. Defaults to the node in --pane, else CRTR_NODE_ID."},{kind:"flag",name:"pane",type:"string",required:!1,constraint:"tmux pane id to resolve the node from. Defaults to $TMUX_PANE."},{kind:"flag",name:"model",type:"string",required:!1,constraint:"Durable model change. Omit unless the task or governing guidance requires a different model.",focusedHelp:{whenToUse:"Use only when intentionally changing this node\u2019s durable model because the user, task, or governing agent guidance requires a specific provider, model, capability tier, or family. Omit it to keep the current model; do not use it merely to inspect available models or experiment with a live node.",value:`Accepted durable forms: ${O}. A live broker also accepts a free-text substring matched against its loaded model registry; a dormant node cannot resolve a substring and requires a durable form.`,effects:[A,"A concrete provider/id runs exactly that model on every launch and revive. A portable provider/tier, bare tier, or family alias keeps its tier: when that model's provider is rate-limited or rejects its credential, the node moves to the next model in its route: another provider's model at the same tier first, then the configured fallback tier (strong by default)."]}},{kind:"flag",name:"lifecycle",type:"enum",choices:["terminal","resident"],required:!1,constraint:"Set lifecycle headlessly without closing the viewer. terminal owes a final and reaps when done; resident stays interactable while attached or awaiting something live."},{kind:"flag",name:"kind",type:"string",required:!1,constraint:"Persona kind. The <kinds> list below names every top-level installable kind and when to use each; a registered sub-kind is valid too by exact path but not listed here."},{kind:"flag",name:"mode",type:"enum",choices:["base","orchestrator"],required:!1,constraint:"Set persona mode headlessly. base is hands-on; orchestrator holds a roadmap and delegates. orchestrator creates your document `roadmap` if absent."},{kind:"flag",name:"name",type:"string",required:!1,constraint:"Rename the node; if the node has a live window, also rename that viewer window."},{kind:"flag",name:"nice",type:"int",required:!1,constraint:`Scheduler niceness every command this node's bash tool starts at, 0\u2013${v} (default ${N}). ${N} keeps the node's builds and test suites below the daemon, the brokers, the viewers, and the person's own shell; 0 lets them compete on equal terms with everything else on the host; ${v} makes them yield to it. Applies to the node's very next command, foreground and background alike.`},{kind:"flag",name:"listen",type:"string",required:!1,constraint:`The node's default listening configuration, as JSON mapping an event level ("1".."10") to how events at or above it reach the node: interrupt, steer, after-turn, or next-turn (e.g. '{"3":"next-turn"}' \u2014 events of level 3+ wait for its next turn and never wake it). Applies to every watch with no configuration of its own. Pass \`null\` to reset to the global default.`},{kind:"flag",name:"cwd",type:"string",required:!1,constraint:"Repair-only replacement launch directory. Must be a non-empty absolute existing directory and used alone; the daemon accepts it only for a dormant, non-forked node with no open managed worktree whose recorded cwd is gone. Clears the saved Pi session, so the next revive starts fresh while durable node work remains."}],output:[{name:"node_id",type:"string",required:!1,constraint:"The reconfigured node, present for ordinary config changes."},{name:"model",type:"string",required:!1,constraint:"The resolved model, present only when --model changed it."},{name:"lifecycle",type:"string",required:!1,constraint:"The new lifecycle, present only when --lifecycle changed it."},{name:"kind",type:"string",required:!1,constraint:"The new kind, present only when --kind changed it."},{name:"mode",type:"string",required:!1,constraint:"The new mode, present only when --mode changed it."},{name:"name",type:"string",required:!1,constraint:"The new name, present only when --name changed it."},{name:"nice",type:"number",required:!1,constraint:"The new bash niceness, present only when --nice changed it."},{name:"listen",type:"object",required:!1,constraint:"The new default listening configuration (null = global default), present only when --listen changed it."},{name:"cwd",type:"string",required:!1,constraint:"The lexically resolved replacement directory, present only after a cwd repair."}],outputKind:"object",effects:[A,"Kind, mode, and lifecycle changes rebuild the launch spec from the fresh node meta; setting mode to orchestrator creates your document `roadmap` if absent.","Name changes update the row and, when the node already has a live window, rename that viewer window to the new full name.","A cwd repair writes only the lexically resolved cwd and clears saved Pi session pointers; the old transcript and durable node work remain untouched.","A niceness change is stored with the node's own bash-job state, so it takes effect on the next command without a revive and survives one.","A listen change sets the node's default listening configuration; it never changes which watches it holds.","Every successful change publishes a `configured` event on the node (level 1) naming the changed settings, so its watchers can see it."]},helpState:{leaf:_},run:l(async e=>{const t=e.pane??process.env.TMUX_PANE,n=await E(e,t),i=e.cwd;if(i!==void 0&&["model","kind","lifecycle","mode","name","nice","listen"].some(w=>e[w]!==void 0))throw new d({error:"cwd_not_exclusive",message:"cwd repair cannot be combined with another config flag",field:"cwd",next:"Pass --cwd alone."});const s=e.model?.trim();if(s==="")throw new d({error:"empty_spec",message:"a model spec is required",field:"model",next:"Run `crtr node config --model -h` and read the focused contract before retrying."});const o=e.kind?.trim(),a=e.lifecycle?.trim(),c=e.mode?.trim(),T=e.name,u=e.nice,b=e.listen?.trim();let q;if(b!==void 0)try{q=JSON.parse(b)}catch{throw new d({error:"invalid_listen",message:`--listen is not JSON: ${b}`,field:"listen",next:`Pass a JSON object such as '{"3":"next-turn"}', or null.`})}if(u!==void 0&&(u<0||u>v))throw new d({error:"bad_nice",message:`niceness must be between 0 and ${v}: ${u}`,field:"nice",next:`A negative niceness needs root, so pass 0\u2013${v}.`});if(a!==void 0&&a.toLowerCase()!=="terminal"&&a.toLowerCase()!=="resident")throw new d({error:"bad_lifecycle",message:`invalid lifecycle: ${a}`,field:"lifecycle",next:"Pass `terminal` or `resident`."});if(c!==void 0&&c.toLowerCase()!=="base"&&c.toLowerCase()!=="orchestrator")throw new d({error:"bad_mode",message:`invalid mode: ${c}`,field:"mode",next:"Pass `base` or `orchestrator`."});const r=[],h={};if(s!==void 0&&(h.model=s,r.push("model")),o!==void 0&&(h.kind=o,r.push("kind")),a!==void 0&&(h.lifecycle=a.toLowerCase(),r.push("lifecycle")),c!==void 0&&(h.mode=c.toLowerCase(),r.push("mode")),T!==void 0&&(h.name=T,r.push("name")),i!==void 0&&(h.cwd=i,r.push("cwd")),b!==void 0&&(h.listen=q??null,r.push("listen")),u!==void 0){const w=await f().getNode(n).catch(S=>p(S));if(D(w.paths.context_dir,u),r.push("nice"),r.length===1)return{node_id:n,nice:u}}if(r.length===0)throw new d({error:"no_change",message:"no config changes requested",next:"Pass at least one of --model, --lifecycle, --kind, --mode, --name, --nice, --listen, --cwd."});const g=await f().patchConfig(n,h).catch(w=>p(w,"List nodes with `crtr node inspect list`."));if(i!==void 0)return{cwd:g.cwd};const m={node_id:n};return r.includes("model")&&(m.model=g.model_override??s),r.includes("lifecycle")&&(m.lifecycle=g.lifecycle),r.includes("kind")&&(m.kind=g.kind),r.includes("mode")&&(m.mode=g.mode),r.includes("name")&&(m.name=g.name),r.includes("nice")&&(m.nice=u),r.includes("listen")&&(m.listen=h.listen),m},"run"),render:l(e=>{if(e.cwd!==void 0)return`Repaired cwd: ${e.cwd}.`;const t=[];return e.model!==void 0&&t.push(`model=${e.model}`),e.lifecycle!==void 0&&t.push(`lifecycle=${e.lifecycle}`),e.kind!==void 0&&t.push(`kind=${e.kind}`),e.mode!==void 0&&t.push(`mode=${e.mode}`),e.name!==void 0&&t.push(`name=${e.name}`),e.nice!==void 0&&t.push(`nice=${e.nice}`),e.listen!==void 0&&t.push(`listen=${JSON.stringify(e.listen)}`),`Configured ${e.node_id} \u2014 ${t.join(", ")}`},"render")}),ne=y({name:"promote",description:"become an orchestrator of a chosen kind",whenToUse:"the remaining work contains independent units that can run in parallel, and the task is large enough that coordinating and integrating them will materially improve intelligence, productivity, or elapsed throughput. Become a long-lived, roadmap-holding orchestrator whose primary job is steering those children across refreshes (`node yield`). Keep sequential or tightly coupled work in base mode even when it spans phases or context windows; spawning one helper does not itself require promotion",tier:"important",help:{name:"node promote",summary:"promote yourself to an orchestrator when worthwhile parallel delegation should make coordination your primary job; task duration, sequential phases, or one helper do not qualify. Mode only \u2014 lifecycle stays as-is unless you pass --resident",params:[{kind:"flag",name:"kind",type:"string",required:!1,constraint:"Specialize as this kind of orchestrator. The <kinds> list below names every top-level installable kind and when to use each; a registered sub-kind is valid too by exact path but not listed here. For an exact composed persona list, use `crtr sys context prompt-review --list --kind <kind>` with the target kind. Defaults to your current kind. Promoting from a generic kind? CHOOSE a concrete one \u2014 it sets the orchestrator persona you revive into."},{kind:"flag",name:"resident",type:"bool",required:!1,constraint:"ALSO flip lifecycle\u2192resident: make the node interactable \u2014 it is never forced to submit a final, and unattended grace parks it after a summary turn instead, leaving it revivable fresh against its goal and roadmap. Omit to stay terminal/orchestrator (delegates + holds a roadmap, but still owes a final up the spine and reaps when done)."},{kind:"flag",name:"model",type:"enum",choices:["ultra","strong"],required:!1,constraint:"Change the model you run on, by capability tier: `ultra` (frontier \u2014 reserve for high-taste judgment or enormous work: speccing, planning something large, e2e-testing something hard to test) or `strong` (opus \u2014 regular dev work). Omit to keep your current model. An orchestrator steering work is never weaker than opus, so these are the only two choices; the change is durable across future revives."},{kind:"flag",name:"node",type:"string",required:!1,constraint:"Node to promote. Defaults to the caller (CRTR_NODE_ID)."}],output:[{name:"node_id",type:"string",required:!0,constraint:"The promoted node."},{name:"kind",type:"string",required:!0,constraint:"The kind it now orchestrates as."},{name:"model",type:"string",required:!1,constraint:"The model tier it now runs on, present only when you changed it with --model."},{name:"lifecycle",type:"string",required:!0,constraint:'"resident" if you passed --resident, else unchanged (typically "terminal").'},{name:"roadmap_written",type:"boolean",required:!0,constraint:"True if the `roadmap` document was created by this call."},{name:"roadmap_ref",type:"string",required:!0,constraint:"Your roadmap document (`<node>/roadmap`) \u2014 author your plan with `crtr doc edit roadmap`."},{name:"goal_path",type:"string",required:!0,constraint:"Absolute path to your goal doc (context/initial-prompt.md) \u2014 the mandate you were spawned with."}],outputKind:"object",effects:["Flips mode\u2192orchestrator + kind\u2192chosen (lifecycle unchanged unless --resident, which also flips lifecycle\u2192resident); --model durably sets the model; rewrites the launch spec to that kind's orchestrator persona; creates your document `roadmap` (rule `on: boot`, `deliver: content`) if absent.","The next model request is gated on appending and persisting your new-role guidance. If a fresh context starts first, its rebuilt baseline carries the new role."]},helpState:{leaf:_},run:l(async e=>{const t=e.node??k();if(t===void 0||t==="")throw new d({error:"no_node",message:"no node to promote (set CRTR_NODE_ID or pass --node)",next:"Run from inside a node, or pass --node <id>."});const n=e.kind,i=e.model,s=e.resident===!0,o=await f().promoteNode(t,{...n!==void 0?{kind:n}:{},...s?{resident:!0}:{},...i!==void 0?{model:i}:{}}).catch(a=>p(a));return{node_id:o.node_id,kind:o.kind,lifecycle:o.lifecycle,...i!==void 0&&o.model_override!=null?{model:o.model_override}:{},roadmap_written:o.roadmap_written??!1,roadmap_ref:o.roadmap_ref??"",goal_path:o.goal_path??""}},"run")});async function E(e,t){let n=e.node;if((n===void 0||n==="")&&(n=x(t)),(n===void 0||n==="")&&(n=k()),n===void 0||n==="")throw new d({error:"no_node",message:"no node (set CRTR_NODE_ID, pass --node, or run from the agent's pane)",next:"Run from inside a node, pass --node <id>, or --pane <pane>."});if(await $(n)===null)throw new d({error:"not_found",message:`no node: ${n}`,next:"List nodes with `crtr node inspect list`."});return n}l(E,"resolveLifecycleNode");const K=y({name:"demote",description:"demote a node to terminal in place; it stays focused and running but now owes a final; --detach also closes its viewer (broker keeps running off-screen)",whenToUse:"you are watching a resident/interactive node and want to put it on a finishing track WITHOUT disturbing it: flip it terminal IN PLACE \u2014 it keeps its viewer and your focus, keeps running, but now owes a final report up the spine and reaps when done. The friendly counterpart to `node promote`. You can also let go entirely and close its viewer so the agent finishes off-screen on its broker. Use `node lifecycle recycle` instead to FINISH it now and reboot a fresh root in its pane, and `node config --lifecycle` for the orthogonal low-level flip (incl. terminal\u2192resident)",help:{name:"node lifecycle demote",summary:"demote a node to terminal IN PLACE \u2014 it stays focused and running but now owes a final up the spine and reaps when done. Pairs with `node promote` (mode\u2191); `--detach` also closes its viewer (the broker engine keeps running off-screen)",params:[{kind:"flag",name:"node",type:"string",required:!1,constraint:"Node to demote. Defaults to the node in --pane, else the caller (CRTR_NODE_ID)."},{kind:"flag",name:"pane",type:"string",required:!1,constraint:"tmux pane id whose node to demote, when --node is omitted. Defaults to $TMUX_PANE. The Alt+C menu passes this for you."},{kind:"flag",name:"detach",type:"bool",required:!1,constraint:"After flipping terminal, stop foregrounding the agent by closing its viewer pane. The broker engine keeps generating off-screen and \u2014 now terminal \u2014 pushes a final up the spine when done; a later `focus` reattaches a viewer. The Alt+C \u2192 D move."}],output:[{name:"node_id",type:"string",required:!0,constraint:"The demoted node."},{name:"detached",type:"boolean",required:!1,constraint:"True when --detach closed the agent's viewer pane (broker keeps running)."}],outputKind:"object",effects:["Flips the node's lifecycle\u2192terminal and rebuilds its launch spec so a future revive boots terminal \u2014 it stays focused and running, now owing a final up the spine.","The next model request is gated on appending and persisting the role transition. If a fresh context starts first, its rebuilt baseline carries the new lifecycle.","With --detach: closes the agent's viewer pane WITHOUT touching the broker engine \u2014 it keeps generating off-screen, reconnectable by a later `focus`."]},run:l(async e=>{const t=e.pane??process.env.TMUX_PANE,n=await E(e,t),i=await f().patchConfig(n,{lifecycle:"terminal"}).catch(o=>p(o));let s=!1;if(e.detach===!0){const o=M(n)??t;o!==void 0&&o!==""&&U(o)&&(s=R(o))}return{node_id:i.node_id,detached:s}},"run"),render:l(e=>`Demoted ${e.node_id} \u2014 lifecycle now terminal (in place)${e.detached===!0?", viewer closed (broker still running off-screen)":""}.`,"render")}),te=P({name:"lifecycle",description:"lifecycle actions \u2014 recycle, close, demote, or revive a node",whenToUse:"you are ending, transitioning, or bringing back one node you can name: finish the agent in your pane and reboot a fresh root (`recycle`), tear a node + its exclusive subtree down without finishing (`close`), flip a node onto a finishing track in place (`demote`), or bring a dormant node back (`revive`). To merely SET the terminal/resident attribute headlessly use `node config --lifecycle`; to FINISH from inside a node's own turn use `crtr push final`; to mass-reconnect EVERY disconnected node at once use `canvas revive --all`.",help:{name:"node lifecycle",summary:"end, transition, or revive a node \u2014 recycle the pane, close + cascade-cancel a subtree, demote to terminal in place, or revive a dormant node",model:"Lifecycle actions preserve or replace an existing node identity. Finishing is permanent, closing is revivable, demotion changes the completion obligation in place, and revival relaunches a dormant broker. Teardown cascades only through descendants exclusively managed by the affected subtree."},children:[F,H,K,I]}),oe=y({name:"yield",description:"refresh your context, same mode, against your roadmap or goal",whenToUse:"your context is filling but the mandate remains; refresh in the same mode, or combine the refresh with promotion when meaningful independent work can run in parallel and coordinating it should become your primary job",help:{name:"node yield",summary:"request a fresh context window against your roadmap or original goal; mode stays unchanged unless --promote is passed",guide:"The yield aborts your turn the instant this command returns: no text or tool call you planned after it will run. So anything the user is owed \u2014 an answer, a status \u2014 must already be a visible message in the conversation before you call it; never plan to reply after yielding. Before invoking: (1) Bring your document `roadmap` current with `crtr doc edit roadmap` \u2014 it, not the yield note, carries the goal into the fresh window. Keep its seeded sections (Goal, Exit criteria, Phases) and keep it short: strategy and present state only \u2014 what is settled, what remains, the current phase\u2019s one-line status, plus dead ends worth not repeating. Delete completed items instead of marking them done; the roadmap should shrink as work completes. (2) Move anything worth remembering but not needed next window into a document (`crtr doc write -h`): one you own for lessons specific to this node\u2019s task, `--owner repo` for lessons that would benefit other agents working in this repo. (3) Send any pending human question; in-flight asks survive the refresh.",params:[{kind:"flag",name:"promote",type:"bool",required:!1,constraint:"Also become an orchestrator as you refresh. Pass only when (1) the remaining work contains independent units that can run in parallel and (2) the task is large enough that parallel execution will materially improve intelligence, productivity, or elapsed throughput after coordination and synthesis. Task length, context exhaustion, sequential phases, or one helper are not enough; keep hands-on work in base mode and yield again as needed. Promote when coordinating and integrating children becomes your primary job. Already an orchestrator? This is not an error: your mode stays orchestrator and a missing roadmap is seeded."},{kind:"flag",name:"kind",type:"string",required:!1,constraint:"Respecialize as you refresh. The <kinds> list below names every top-level available kind and when to choose it; a registered sub-kind is valid too by exact path but not listed here. Defaults to your current kind. With --promote it sets the orchestrator kind you become; without --promote it changes the kind while preserving your current mode."},{kind:"flag",name:"model",type:"enum",choices:["ultra","strong"],required:!1,constraint:"Change the model your fresh revive runs on, by capability tier: `ultra` (frontier \u2014 reserve for high-taste judgment or enormous work: speccing, planning something large, e2e-testing something hard to test) or `strong` (opus \u2014 regular dev work). Omit to keep your current model. A node steering work is never weaker than opus, so these are the only two choices; the change is durable across future revives."},{kind:"stdin",name:"message",required:!0,constraint:"A note to your future self \u2014 what you just tried, where things stand, and the first move on waking: enough to continue efficiently, no more. If the user asked something you have not answered in a visible message, say so and quote the question. Never describe a reply as sent unless it is visible in the conversation above this call. Durable strategy belongs in the roadmap, not here. Surfaced as <yield-message> in the next revive. Pass as a positional or pipe via heredoc."}],output:[{name:"node_id",type:"string",required:!0,constraint:"The yielding node."},{name:"model",type:"string",required:!1,constraint:"The model tier your fresh revive will run on, present only when you changed it with --model."}],outputKind:"object",effects:["Aborts your current turn as soon as this command returns \u2014 nothing after it runs \u2014 then the daemon revives the node fresh.","--kind and/or --model durably reshape the node (kind respecialization, model change) before the refresh-revive; --promote additionally flips mode\u2192orchestrator and creates your document `roadmap` \u2014 mode is otherwise left exactly as it was."]},helpState:{leaf:_},run:l(async e=>{const t=k();if(t===void 0||t==="")throw new d({error:"no_node",message:"no node to yield (CRTR_NODE_ID unset)",next:"Run from inside a node."});const n=e.kind,i=e.model,s=e.promote===!0,o=typeof e.message=="string"?e.message.trim():"";if(o==="")throw new d({error:"empty_message",message:"a yield message is required (stdin or positional)",next:"Pass a note to your future self as an argument or pipe it on stdin."});const a=await f().yieldNode(t,{note:o,...n!==void 0?{kind:n}:{},...i!==void 0?{model:i}:{},...s?{promote:!0}:{}}).catch(c=>p(c));return{node_id:a.node_id,...i!==void 0&&a.model_override!=null?{model:a.model_override}:{}}},"run")});export{ee as nodeConfig,te as nodeLifecycle,ne as nodePromote,oe as nodeYield};
|
|
1
|
+
var C=Object.defineProperty;var l=(e,t)=>C(e,"name",{value:t,configurable:!0});import{envNodeId as k}from"../../shared/env.js";import{defineBranch as $,defineLeaf as y}from"../../core/command.js";import{InputError as d}from"../../core/io.js";import{cliClient as f,getNodeOrNull as P,rethrowAsCliError as p}from"../api-client.js";import{DEFAULT_BASH_NICENESS as N,MAX_BASH_NICENESS as v,writeBashNiceness as D}from"../../core/bash-jobs.js";import{nodeReviveLeaf as I}from"../node-lifecycle-revive.js";import{kindsStateBlock as _}from"../profile/daemon-profiles.js";import{MODEL_SPEC_FORMS as O}from"../../core/runtime/model-selection.js";import{closePane as R,paneExists as U,respawnPaneSync as L,viewerEnv as j,viewerPaneOf as M,nodeInPane as x}from"../surface/node/placement.js";const F=y({name:"recycle",description:"finish the agent in your pane + recycle it into a fresh root",whenToUse:"you are at an agent's pane and done with it: finish it cleanly and recycle the pane in one move \u2014 push its last message as a final report to everyone waiting on it, mark it done, then boot a fresh crtr root in the same pane to keep working. The hands-on way to end an agent and immediately start over in place. Use `node lifecycle demote` instead to put it on a finishing track IN PLACE (flip terminal, keep it running) without ending it now, `node lifecycle close` to tear a node and its subtree down WITHOUT finishing (no report, revivable), and `push final` when the agent should finish ITSELF from inside its own turn",help:{name:"node lifecycle recycle",summary:"finish the agent in your current pane and recycle the pane \u2014 push its last message as a final report to everyone waiting on it, mark it done, then boot a fresh crtr root in the same pane",params:[{kind:"flag",name:"node",type:"string",required:!1,constraint:"Node to finish. Defaults to the node occupying --pane (or your current pane)."},{kind:"flag",name:"pane",type:"string",required:!1,constraint:"tmux pane id to recycle. Defaults to $TMUX_PANE / your current pane. The Alt+C menu passes this for you."}],output:[{name:"recycled",type:"boolean",required:!0,constraint:"True when the pane was recycled into a fresh root."},{name:"node_id",type:"string",required:!1,constraint:"The finished node."},{name:"new_root",type:"string",required:!1,constraint:"The fresh root node booted into the pane."}],outputKind:"object",effects:["Pushes a final report from the node (fans out to all subscribers) and marks it done.","Kills the agent's pi and respawns a fresh resident root in the same tmux pane."]},run:l(async e=>{const t=e.pane??process.env.TMUX_PANE;let n=e.node;if((n===void 0||n==="")&&(n=x(t)),n===void 0||n==="")throw new d({error:"no_node",message:"no node found in this pane to finish",next:"Pass --node <id>, or run from inside the agent's pane."});const i=await f().getNode(n).catch(c=>p(c,"List nodes with `crtr node inspect list`.")),o=(await f().recycleNode(n).catch(p)).node_id;let a=!1;return t!==void 0&&t!==""&&(a=L({pane:t,cwd:i.cwd,env:j(),command:"crtr"})),{recycled:a,node_id:n,new_root:o}},"run"),render:l(e=>{if(e.recycled!==!0)return e.new_root?`Finished ${e.node_id} and spawned a fresh broker root (${e.new_root}), but its viewer did not attach \u2014 the new broker did not come up. Re-focus the root to view it.`:`Recycle failed for ${e.node_id??"?"} \u2014 not in tmux, or no agent in this pane.`;const t=[`Recycled the pane \u2014 finished ${e.node_id}.`];return e.new_root&&t.push(`- new root: ${e.new_root}`),t.join(`
|
|
2
|
+
`)},"render")}),H=y({name:"close",description:"close a node + cascade-cancel its exclusive subtree",whenToUse:"you want to tear a node down WITHOUT finishing it, cascade-cancelling every descendant it exclusively owns: abandoning a line of work, killing a stuck or wrong-turn subtree, clearing a branch you no longer need. A node that never produced substantive assistant output is deleted and cannot be revived; streaming nodes and nodes with an open managed worktree are retained. Other closed nodes keep their pi session and can be revived later (`node lifecycle revive`). Use `node lifecycle recycle` instead to FINISH the agent in your pane with a final report, and `push final` when a worker should end its own work normally (Alt+C \u2192 x)",help:{name:"node lifecycle close",summary:"close a node and cascade-cancel its subtree \u2014 kill its tmux window plus those of every descendant it EXCLUSIVELY owns (down the subscribes_to spine), mark retained nodes canceled, and leave them a notice they read on resume. A descendant still subscribed to by a manager outside the subtree is left running. A node that never produced substantive assistant output is deleted and cannot be revived; streaming nodes and nodes with an open managed worktree are retained. Other closed nodes keep their pi session and can be revived later (`crtr node lifecycle revive`)",params:[{kind:"flag",name:"node",type:"string",required:!1,constraint:"Node to close. Defaults to the node occupying --pane (or your current pane)."},{kind:"flag",name:"pane",type:"string",required:!1,constraint:"tmux pane id whose node to close. Defaults to $TMUX_PANE / your current pane. The Alt+C menu passes this for you."}],output:[{name:"node_id",type:"string",required:!1,constraint:"The node that was closed \u2014 the cascade root."},{name:"count",type:"number",required:!1,constraint:"How many nodes were closed (root + cascaded descendants)."},{name:"closed_ids",type:"string[]",required:!1,constraint:"All closed node ids, kill order (leaves first, root last)."},{name:"spared",type:"string[]",required:!1,constraint:"Descendants left alive because a manager outside the subtree still subscribes to them."}],outputKind:"object",effects:["Marks retained nodes and their exclusive descendants `canceled` and clears intent (the daemon never revives a canceled node).","Deletes a non-streaming node with no substantive assistant output unless it has an open managed worktree; a deleted node cannot be revived.","Kills each closed node's tmux window; retained nodes keep their pi sessions and canvas edges for a later revive.","Appends a cancellation notice to each retained node's inbox, surfaced on its next resume."]},run:l(async e=>{const t=e.pane??process.env.TMUX_PANE;let n=e.node;if((n===void 0||n==="")&&(n=x(t)),n===void 0||n==="")throw new d({error:"no_node",message:"no node found in this pane to close",next:"Pass --node <id>, or run from inside the agent's pane."});const i=await f().closeNode(n,process.ppid>0?{caller_pid:process.ppid}:{}).catch(s=>p(s,"List nodes with `crtr node inspect list`."));return{node_id:i.root,count:i.closed.length,closed_ids:i.closed,spared:i.spared}},"run"),render:l(e=>{const t=e.spared?.length??0;return`Closed ${e.node_id} and its exclusive subtree \u2014 ${e.count} node(s) closed${t>0?`, ${t} spared (still managed from outside the subtree)`:""}.`},"render")}),A="Model change first: a running broker, including one still starting right after `node new`, switches over its socket; a dormant node persists the durable model override and launch recipe for its next revive.",ee=y({name:"config",description:"reconfigure a node's settings headlessly \u2014 model, lifecycle (terminal/resident), kind, mode, name; repair a missing cwd",whenToUse:"you want to reconfigure a node's settings headlessly after spawn \u2014 swap its model/provider, flip it terminal\u2194resident (interactable), respecialize its kind, switch base\u2194orchestrator mode, or rename it \u2014 without opening its viewer. It also repairs a dormant node whose recorded cwd is gone, starting its next revive in a fresh conversation. The durable attribute setter; reuses the same recipes a revive reads. Use `node promote` for the opinionated self-promotion path, and `node lifecycle` for teardown actions (recycle/close/demote).",help:{name:"node config",summary:"reconfigure a node's settings headlessly \u2014 model, lifecycle, kind, mode, name; repair a missing cwd",params:[{kind:"flag",name:"node",type:"string",required:!1,constraint:"Target node id. Defaults to the node in --pane, else CRTR_NODE_ID."},{kind:"flag",name:"pane",type:"string",required:!1,constraint:"tmux pane id to resolve the node from. Defaults to $TMUX_PANE."},{kind:"flag",name:"model",type:"string",required:!1,constraint:"Durable model change. Omit unless the task or governing guidance requires a different model.",focusedHelp:{whenToUse:"Use only when intentionally changing this node\u2019s durable model because the user, task, or governing agent guidance requires a specific provider, model, capability tier, or family. Omit it to keep the current model; do not use it merely to inspect available models or experiment with a live node.",value:`Accepted durable forms: ${O}. A live broker also accepts a free-text substring matched against its loaded model registry; a dormant node cannot resolve a substring and requires a durable form.`,effects:[A,"A concrete provider/id runs exactly that model on every launch and revive. A portable provider/tier, bare tier, or family alias keeps its tier: when that model's provider is rate-limited or rejects its credential, the node moves to the next model in its route: another provider's model at the same tier first, then the configured fallback tier (strong by default)."]}},{kind:"flag",name:"lifecycle",type:"enum",choices:["terminal","resident"],required:!1,constraint:"Set lifecycle headlessly without closing the viewer. terminal owes a final and reaps when done; resident stays interactable while attached or awaiting something live."},{kind:"flag",name:"kind",type:"string",required:!1,constraint:"Persona kind. The <kinds> list below names every top-level installable kind and when to use each; a registered sub-kind is valid too by exact path but not listed here."},{kind:"flag",name:"mode",type:"enum",choices:["base","orchestrator"],required:!1,constraint:"Set persona mode headlessly. base is hands-on; orchestrator holds a roadmap and delegates. orchestrator creates your document `roadmap` if absent."},{kind:"flag",name:"name",type:"string",required:!1,constraint:"Rename the node; if the node has a live window, also rename that viewer window."},{kind:"flag",name:"nice",type:"int",required:!1,constraint:`Scheduler niceness every command this node's bash tool starts at, 0\u2013${v} (default ${N}). ${N} keeps the node's builds and test suites below the daemon, the brokers, the viewers, and the person's own shell; 0 lets them compete on equal terms with everything else on the host; ${v} makes them yield to it. Applies to the node's very next command, foreground and background alike.`},{kind:"flag",name:"listen",type:"string",required:!1,constraint:`The node's default listening configuration, as JSON mapping an event level ("1".."10") to how events at or above it reach the node: interrupt, steer, after-turn, or next-turn (e.g. '{"3":"next-turn"}' \u2014 events of level 3+ wait for its next turn and never wake it). Applies to every watch with no configuration of its own. Pass \`null\` to reset to the global default.`},{kind:"flag",name:"cwd",type:"string",required:!1,constraint:"Repair-only replacement launch directory. Must be a non-empty absolute existing directory and used alone; the daemon accepts it only for a dormant, non-forked node with no open managed worktree whose recorded cwd is gone. Clears the saved Pi session, so the next revive starts fresh while durable node work remains."}],output:[{name:"node_id",type:"string",required:!1,constraint:"The reconfigured node, present for ordinary config changes."},{name:"model",type:"string",required:!1,constraint:"The resolved model, present only when --model changed it."},{name:"lifecycle",type:"string",required:!1,constraint:"The new lifecycle, present only when --lifecycle changed it."},{name:"kind",type:"string",required:!1,constraint:"The new kind, present only when --kind changed it."},{name:"mode",type:"string",required:!1,constraint:"The new mode, present only when --mode changed it."},{name:"name",type:"string",required:!1,constraint:"The new name, present only when --name changed it."},{name:"nice",type:"number",required:!1,constraint:"The new bash niceness, present only when --nice changed it."},{name:"listen",type:"object",required:!1,constraint:"The new default listening configuration (null = global default), present only when --listen changed it."},{name:"cwd",type:"string",required:!1,constraint:"The lexically resolved replacement directory, present only after a cwd repair."}],outputKind:"object",effects:[A,"Kind, mode, and lifecycle changes rebuild the launch spec from the fresh node meta; setting mode to orchestrator creates your document `roadmap` if absent.","Name changes update the row and, when the node already has a live window, rename that viewer window to the new full name.","A cwd repair writes only the lexically resolved cwd and clears saved Pi session pointers; the old transcript and durable node work remain untouched.","A niceness change is stored with the node's own bash-job state, so it takes effect on the next command without a revive and survives one.","A listen change sets the node's default listening configuration; it never changes which watches it holds.","Every successful change publishes a `configured` event on the node (level 1) naming the changed settings, so its watchers can see it."]},helpState:{leaf:_},run:l(async e=>{const t=e.pane??process.env.TMUX_PANE,n=await E(e,t),i=e.cwd;if(i!==void 0&&["model","kind","lifecycle","mode","name","nice","listen"].some(w=>e[w]!==void 0))throw new d({error:"cwd_not_exclusive",message:"cwd repair cannot be combined with another config flag",field:"cwd",next:"Pass --cwd alone."});const s=e.model?.trim();if(s==="")throw new d({error:"empty_spec",message:"a model spec is required",field:"model",next:"Run `crtr node config --model -h` and read the focused contract before retrying."});const o=e.kind?.trim(),a=e.lifecycle?.trim(),c=e.mode?.trim(),T=e.name,u=e.nice,b=e.listen?.trim();let q;if(b!==void 0)try{q=JSON.parse(b)}catch{throw new d({error:"invalid_listen",message:`--listen is not JSON: ${b}`,field:"listen",next:`Pass a JSON object such as '{"3":"next-turn"}', or null.`})}if(u!==void 0&&(u<0||u>v))throw new d({error:"bad_nice",message:`niceness must be between 0 and ${v}: ${u}`,field:"nice",next:`A negative niceness needs root, so pass 0\u2013${v}.`});if(a!==void 0&&a.toLowerCase()!=="terminal"&&a.toLowerCase()!=="resident")throw new d({error:"bad_lifecycle",message:`invalid lifecycle: ${a}`,field:"lifecycle",next:"Pass `terminal` or `resident`."});if(c!==void 0&&c.toLowerCase()!=="base"&&c.toLowerCase()!=="orchestrator")throw new d({error:"bad_mode",message:`invalid mode: ${c}`,field:"mode",next:"Pass `base` or `orchestrator`."});const r=[],h={};if(s!==void 0&&(h.model=s,r.push("model")),o!==void 0&&(h.kind=o,r.push("kind")),a!==void 0&&(h.lifecycle=a.toLowerCase(),r.push("lifecycle")),c!==void 0&&(h.mode=c.toLowerCase(),r.push("mode")),T!==void 0&&(h.name=T,r.push("name")),i!==void 0&&(h.cwd=i,r.push("cwd")),b!==void 0&&(h.listen=q??null,r.push("listen")),u!==void 0){const w=await f().getNode(n).catch(S=>p(S));if(D(w.paths.context_dir,u),r.push("nice"),r.length===1)return{node_id:n,nice:u}}if(r.length===0)throw new d({error:"no_change",message:"no config changes requested",next:"Pass at least one of --model, --lifecycle, --kind, --mode, --name, --nice, --listen, --cwd."});const g=await f().patchConfig(n,h).catch(w=>p(w,"List nodes with `crtr node inspect list`."));if(i!==void 0)return{cwd:g.cwd};const m={node_id:n};return r.includes("model")&&(m.model=g.model_override??s),r.includes("lifecycle")&&(m.lifecycle=g.lifecycle),r.includes("kind")&&(m.kind=g.kind),r.includes("mode")&&(m.mode=g.mode),r.includes("name")&&(m.name=g.name),r.includes("nice")&&(m.nice=u),r.includes("listen")&&(m.listen=h.listen),m},"run"),render:l(e=>{if(e.cwd!==void 0)return`Repaired cwd: ${e.cwd}.`;const t=[];return e.model!==void 0&&t.push(`model=${e.model}`),e.lifecycle!==void 0&&t.push(`lifecycle=${e.lifecycle}`),e.kind!==void 0&&t.push(`kind=${e.kind}`),e.mode!==void 0&&t.push(`mode=${e.mode}`),e.name!==void 0&&t.push(`name=${e.name}`),e.nice!==void 0&&t.push(`nice=${e.nice}`),e.listen!==void 0&&t.push(`listen=${JSON.stringify(e.listen)}`),`Configured ${e.node_id} \u2014 ${t.join(", ")}`},"render")}),ne=y({name:"promote",description:"become an orchestrator of a chosen kind",whenToUse:"the remaining work contains independent units that can run in parallel, and the task is large enough that coordinating and integrating them will materially improve intelligence, productivity, or elapsed throughput. Become a long-lived, roadmap-holding orchestrator whose primary job is steering those children across refreshes (`node yield`). Keep sequential or tightly coupled work in base mode even when it spans phases or context windows; spawning one helper does not itself require promotion",tier:"important",help:{name:"node promote",summary:"promote yourself to an orchestrator when worthwhile parallel delegation should make coordination your primary job; task duration, sequential phases, or one helper do not qualify. Mode only \u2014 lifecycle stays as-is unless you pass --resident",params:[{kind:"flag",name:"kind",type:"string",required:!1,constraint:"Specialize as this kind of orchestrator. The <kinds> list below names every top-level installable kind and when to use each; a registered sub-kind is valid too by exact path but not listed here. For an exact composed persona list, use `crtr sys context prompt-review --list --kind <kind>` with the target kind. Defaults to your current kind. Promoting from a generic kind? CHOOSE a concrete one \u2014 it sets the orchestrator persona you revive into."},{kind:"flag",name:"resident",type:"bool",required:!1,constraint:"ALSO flip lifecycle\u2192resident: make the node interactable \u2014 it is never forced to submit a final, and unattended grace parks it after a summary turn instead, leaving it revivable fresh against its goal and roadmap. Omit to stay terminal/orchestrator (delegates + holds a roadmap, but still owes a final up the spine and reaps when done)."},{kind:"flag",name:"model",type:"enum",choices:["ultra","strong"],required:!1,constraint:"Change the model you run on, by capability tier: `ultra` (frontier \u2014 reserve for high-taste judgment or enormous work: speccing, planning something large, e2e-testing something hard to test) or `strong` (opus \u2014 regular dev work). Omit to keep your current model. An orchestrator steering work is never weaker than opus, so these are the only two choices; the change is durable across future revives."},{kind:"flag",name:"node",type:"string",required:!1,constraint:"Node to promote. Defaults to the caller (CRTR_NODE_ID)."}],output:[{name:"node_id",type:"string",required:!0,constraint:"The promoted node."},{name:"kind",type:"string",required:!0,constraint:"The kind it now orchestrates as."},{name:"model",type:"string",required:!1,constraint:"The model tier it now runs on, present only when you changed it with --model."},{name:"lifecycle",type:"string",required:!0,constraint:'"resident" if you passed --resident, else unchanged (typically "terminal").'},{name:"roadmap_written",type:"boolean",required:!0,constraint:"True if the `roadmap` document was created by this call."},{name:"roadmap_ref",type:"string",required:!0,constraint:"Your roadmap document (`<node>/roadmap`) \u2014 author your plan with `crtr doc edit roadmap`."},{name:"goal_path",type:"string",required:!0,constraint:"Absolute path to your goal doc (context/initial-prompt.md) \u2014 the mandate you were spawned with."}],outputKind:"object",effects:["Flips mode\u2192orchestrator + kind\u2192chosen (lifecycle unchanged unless --resident, which also flips lifecycle\u2192resident); --model durably sets the model; rewrites the launch spec to that kind's orchestrator persona; creates your document `roadmap` (rule `on: boot`, `deliver: content`) if absent.","The next model request is gated on appending and persisting your new-role guidance. If a fresh context starts first, its rebuilt baseline carries the new role."]},helpState:{leaf:_},run:l(async e=>{const t=e.node??k();if(t===void 0||t==="")throw new d({error:"no_node",message:"no node to promote (set CRTR_NODE_ID or pass --node)",next:"Run from inside a node, or pass --node <id>."});const n=e.kind,i=e.model,s=e.resident===!0,o=await f().promoteNode(t,{...n!==void 0?{kind:n}:{},...s?{resident:!0}:{},...i!==void 0?{model:i}:{}}).catch(a=>p(a));return{node_id:o.node_id,kind:o.kind,lifecycle:o.lifecycle,...i!==void 0&&o.model_override!=null?{model:o.model_override}:{},roadmap_written:o.roadmap_written??!1,roadmap_ref:o.roadmap_ref??"",goal_path:o.goal_path??""}},"run")});async function E(e,t){let n=e.node;if((n===void 0||n==="")&&(n=x(t)),(n===void 0||n==="")&&(n=k()),n===void 0||n==="")throw new d({error:"no_node",message:"no node (set CRTR_NODE_ID, pass --node, or run from the agent's pane)",next:"Run from inside a node, pass --node <id>, or --pane <pane>."});if(await P(n)===null)throw new d({error:"not_found",message:`no node: ${n}`,next:"List nodes with `crtr node inspect list`."});return n}l(E,"resolveLifecycleNode");const W=y({name:"demote",description:"demote a node to terminal in place; it stays focused and running but now owes a final; --detach also closes its viewer (broker keeps running off-screen)",whenToUse:"you are watching a resident/interactive node and want to put it on a finishing track WITHOUT disturbing it: flip it terminal IN PLACE \u2014 it keeps its viewer and your focus, keeps running, but now owes a final report up the spine and reaps when done. The friendly counterpart to `node promote`. You can also let go entirely and close its viewer so the agent finishes off-screen on its broker. Use `node lifecycle recycle` instead to FINISH it now and reboot a fresh root in its pane, and `node config --lifecycle` for the orthogonal low-level flip (incl. terminal\u2192resident)",help:{name:"node lifecycle demote",summary:"demote a node to terminal IN PLACE \u2014 it stays focused and running but now owes a final up the spine and reaps when done. Pairs with `node promote` (mode\u2191); `--detach` also closes its viewer (the broker engine keeps running off-screen)",params:[{kind:"flag",name:"node",type:"string",required:!1,constraint:"Node to demote. Defaults to the node in --pane, else the caller (CRTR_NODE_ID)."},{kind:"flag",name:"pane",type:"string",required:!1,constraint:"tmux pane id whose node to demote, when --node is omitted. Defaults to $TMUX_PANE. The Alt+C menu passes this for you."},{kind:"flag",name:"detach",type:"bool",required:!1,constraint:"After flipping terminal, stop foregrounding the agent by closing its viewer pane. The broker engine keeps generating off-screen and \u2014 now terminal \u2014 pushes a final up the spine when done; a later `focus` reattaches a viewer. The Alt+C \u2192 D move."}],output:[{name:"node_id",type:"string",required:!0,constraint:"The demoted node."},{name:"detached",type:"boolean",required:!1,constraint:"True when --detach closed the agent's viewer pane (broker keeps running)."}],outputKind:"object",effects:["Flips the node's lifecycle\u2192terminal and rebuilds its launch spec so a future revive boots terminal \u2014 it stays focused and running, now owing a final up the spine.","The next model request is gated on appending and persisting the role transition. If a fresh context starts first, its rebuilt baseline carries the new lifecycle.","With --detach: closes the agent's viewer pane WITHOUT touching the broker engine \u2014 it keeps generating off-screen, reconnectable by a later `focus`."]},run:l(async e=>{const t=e.pane??process.env.TMUX_PANE,n=await E(e,t),i=await f().patchConfig(n,{lifecycle:"terminal"}).catch(o=>p(o));let s=!1;if(e.detach===!0){const o=M(n)??t;o!==void 0&&o!==""&&U(o)&&(s=R(o))}return{node_id:i.node_id,detached:s}},"run"),render:l(e=>`Demoted ${e.node_id} \u2014 lifecycle now terminal (in place)${e.detached===!0?", viewer closed (broker still running off-screen)":""}.`,"render")}),te=$({name:"lifecycle",description:"lifecycle actions \u2014 recycle, close, demote, or revive a node",whenToUse:"you are ending, transitioning, or bringing back one node you can name: finish the agent in your pane and reboot a fresh root (`recycle`), tear a node + its exclusive subtree down without finishing (`close`), flip a node onto a finishing track in place (`demote`), or bring a dormant node back (`revive`). To merely SET the terminal/resident attribute headlessly use `node config --lifecycle`; to FINISH from inside a node's own turn use `crtr push final`; to mass-reconnect EVERY disconnected node at once use `canvas revive --all`.",help:{name:"node lifecycle",summary:"end, transition, or revive a node \u2014 recycle the pane, close + cascade-cancel a subtree, demote to terminal in place, or revive a dormant node",model:"Lifecycle actions preserve or replace an existing node identity. Finishing is permanent, closing is revivable, demotion changes the completion obligation in place, and revival relaunches a dormant broker. Teardown cascades only through descendants exclusively managed by the affected subtree."},children:[F,H,W,I]}),oe=y({name:"yield",description:"refresh your context, same mode, against your roadmap or goal",whenToUse:"your context is filling but the mandate remains; refresh in the same mode, or combine the refresh with promotion when meaningful independent work can run in parallel and coordinating it should become your primary job",help:{name:"node yield",summary:"request a fresh context window against your roadmap or original goal; mode stays unchanged unless --promote is passed",guide:"The yield aborts your turn the instant this command returns: no text or tool call you planned after it will run. So anything the user is owed \u2014 an answer, a status \u2014 must already be a visible message in the conversation before you call it; never plan to reply after yielding. Before invoking: (1) If you hold a document `roadmap`, bring it current (`crtr doc edit roadmap`). It, not the yield note, carries your task into the fresh window. It has four sections: Goal (its first line is also the roadmap\u2019s preview, so set it with `--preview` whenever the goal changes), Exit criteria, Where things stand, and Working set. Keep it at your own level of abstraction. Detail below your level belongs to the node or document that holds it: link to it, don\u2019t copy it. Where things stand is present tense; delete what is done instead of marking it. Name units by what they are and how they fit, never by child node id or status: your bearings already list your children with their status. Working set holds `[[links]]`. Their previews load with the roadmap, so write no note beside a link. When a unit lands, delete its links: its evidence belongs in its report. (2) Give content its own document when it lives on a different timescale from the roadmap (exploration that outlives the current work), goes to a different reader (a brief for a child), or matters only when something specific happens. Link each such document from the Working set. Anything you had to re-derive in this window that a future window will need again belongs in a document: one you own for this task, `--owner repo` for lessons that would help others in the repo. A document that only points at other documents is not worth the extra hop. (3) Actions waiting on one specific event (\u201Cwhen X reports, send Y first\u201D) go in the yield note, not the roadmap. They are spent once they fire. (4) Send any pending human question; asks already in flight survive the refresh.",params:[{kind:"flag",name:"promote",type:"bool",required:!1,constraint:"Also become an orchestrator as you refresh. Pass only when (1) the remaining work contains independent units that can run in parallel and (2) the task is large enough that parallel execution will materially improve intelligence, productivity, or elapsed throughput after coordination and synthesis. Task length, context exhaustion, sequential phases, or one helper are not enough; keep hands-on work in base mode and yield again as needed. Promote when coordinating and integrating children becomes your primary job. Already an orchestrator? This is not an error: your mode stays orchestrator and a missing roadmap is seeded."},{kind:"flag",name:"kind",type:"string",required:!1,constraint:"Respecialize as you refresh. The <kinds> list below names every top-level available kind and when to choose it; a registered sub-kind is valid too by exact path but not listed here. Defaults to your current kind. With --promote it sets the orchestrator kind you become; without --promote it changes the kind while preserving your current mode."},{kind:"flag",name:"model",type:"enum",choices:["ultra","strong"],required:!1,constraint:"Change the model your fresh revive runs on, by capability tier: `ultra` (frontier \u2014 reserve for high-taste judgment or enormous work: speccing, planning something large, e2e-testing something hard to test) or `strong` (opus \u2014 regular dev work). Omit to keep your current model. A node steering work is never weaker than opus, so these are the only two choices; the change is durable across future revives."},{kind:"stdin",name:"message",required:!0,constraint:"A note to your future self \u2014 what you just tried, where things stand, the first move on waking, and any action waiting on a specific event: enough to continue efficiently, no more. Never describe a reply as sent unless it is visible in the conversation above this call. Your task and its state belong in the roadmap. Surfaced as <yield-message> in the next revive. Pass as a positional or pipe via heredoc."}],output:[{name:"node_id",type:"string",required:!0,constraint:"The yielding node."},{name:"model",type:"string",required:!1,constraint:"The model tier your fresh revive will run on, present only when you changed it with --model."}],outputKind:"object",effects:["Aborts your current turn as soon as this command returns \u2014 nothing after it runs \u2014 then the daemon revives the node fresh.","--kind and/or --model durably reshape the node (kind respecialization, model change) before the refresh-revive; --promote additionally flips mode\u2192orchestrator and creates your document `roadmap` \u2014 mode is otherwise left exactly as it was."]},helpState:{leaf:_},run:l(async e=>{const t=k();if(t===void 0||t==="")throw new d({error:"no_node",message:"no node to yield (CRTR_NODE_ID unset)",next:"Run from inside a node."});const n=e.kind,i=e.model,s=e.promote===!0,o=typeof e.message=="string"?e.message.trim():"";if(o==="")throw new d({error:"empty_message",message:"a yield message is required (stdin or positional)",next:"Pass a note to your future self as an argument or pipe it on stdin."});const a=await f().yieldNode(t,{note:o,...n!==void 0?{kind:n}:{},...i!==void 0?{model:i}:{},...s?{promote:!0}:{}}).catch(c=>p(c));return{node_id:a.node_id,...i!==void 0&&a.model_override!=null?{model:a.model_override}:{}}},"run")});export{ee as nodeConfig,te as nodeLifecycle,ne as nodePromote,oe as nodeYield};
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/** Deleting a node with what it owns (spec §6 "Deletion follows the owner", A9). Rows are kept
|
|
2
|
-
* with `deleted_at` so every pointer resolves to "deleted" with the last preview
|
|
3
|
-
*
|
|
2
|
+
* with `deleted_at` so every pointer resolves to "deleted" with the last preview. A document a
|
|
3
|
+
* live document outside the deletion links to is kept: it stays owned by the deleted node, under
|
|
4
|
+
* its name and rules, and is collected when its last link goes (`collectUnlinked`). */
|
|
4
5
|
import type { DatabaseSync } from 'node:sqlite';
|
|
5
6
|
import type { ObjectRow } from './types.js';
|
|
6
|
-
/** Mark the node and everything it owns deleted
|
|
7
|
-
* drop the node's watches (both ends) and its `nodes` row. One savepoint, so it runs as one
|
|
7
|
+
/** Mark the node and everything it owns deleted except kept documents, which stay owned by the
|
|
8
|
+
* deleted node; drop the node's watches (both ends) and its `nodes` row. One savepoint, so it runs as one
|
|
8
9
|
* transaction or inside the caller's. Refuses a node whose managed worktree is unreconciled. */
|
|
9
10
|
export declare function deleteNodeWithOwned(db: DatabaseSync, node: ObjectRow): void;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
var
|
|
1
|
+
var p=Object.defineProperty;var d=(e,o)=>p(e,"name",{value:o,configurable:!0});import{hasUnreconciledWorktree as u}from"../canvas/worktrees.js";import{deleteDocument as R}from"./documents.js";import{levelFor as s,publish as c}from"./events.js";import{getByRowid as w}from"./objects.js";let O=0;function T(e,o){if(o.type!=="node")throw new Error(`deleteNodeWithOwned: ${o.id} is a ${o.type}, not a node`);if(u(o.id))throw new Error(`refusing to delete node ${o.id}: its managed worktree is not reconciled`);const r=`graph_delete_${++O}`;e.exec(`SAVEPOINT ${r}`);try{f(e,o),e.exec(`RELEASE ${r}`)}catch(i){throw e.exec(`ROLLBACK TO ${r}`),e.exec(`RELEASE ${r}`),i}}d(T,"deleteNodeWithOwned");function f(e,o){const r=e.prepare(`WITH RECURSIVE sub(rowid) AS (
|
|
2
2
|
SELECT rowid FROM objects WHERE owner = ?
|
|
3
3
|
UNION SELECT o.rowid FROM objects o JOIN sub ON o.owner = sub.rowid)
|
|
4
|
-
SELECT o.* FROM objects o JOIN sub ON sub.rowid = o.rowid WHERE o.deleted_at IS NULL`).all(o.rowid),
|
|
5
|
-
WHERE e.type = 'links-to' AND e.dst = ? AND s.deleted_at IS NULL`)
|
|
4
|
+
SELECT o.* FROM objects o JOIN sub ON sub.rowid = o.rowid WHERE o.deleted_at IS NULL`).all(o.rowid),i=new Set(r.map(t=>t.rowid)),l=e.prepare(`SELECT e.src FROM edges e JOIN objects s ON s.rowid = e.src
|
|
5
|
+
WHERE e.type = 'links-to' AND e.dst = ? AND s.deleted_at IS NULL`);for(let t=!0;t;){t=!1;for(const n of r)n.type!=="document"||!i.has(n.rowid)||l.all(n.rowid).some(({src:a})=>!i.has(a))&&(i.delete(n.rowid),t=!0)}const E=new Date().toISOString();for(const t of r)if(i.has(t.rowid)){if(t.type==="document"){R(e,null,t);continue}e.prepare("UPDATE objects SET deleted_at = ? WHERE rowid = ?").run(E,t.rowid),c(e,{event:"deleted",object:w(e,t.rowid),actor:null,level:s("deleted",void 0,t.type),attrs:{}}),e.prepare("DELETE FROM watches WHERE object = ? OR watcher = ?").run(t.rowid,t.rowid)}e.prepare("UPDATE objects SET deleted_at = ? WHERE rowid = ?").run(E,o.rowid),c(e,{event:"deleted",object:w(e,o.rowid),actor:null,level:s("deleted",void 0,"node"),attrs:{}}),e.prepare("DELETE FROM watches WHERE object = ? OR watcher = ?").run(o.rowid,o.rowid),e.prepare("DELETE FROM nodes WHERE node_id = ?").run(o.id)}d(f,"deleteInside");export{T as deleteNodeWithOwned};
|
|
@@ -123,8 +123,15 @@ export declare function moveDocument(db: DatabaseSync, reader: Reader | null, do
|
|
|
123
123
|
owner?: ObjectRow;
|
|
124
124
|
}): ObjectRow;
|
|
125
125
|
/** Mark the document deleted; the row stays with its last preview. Publishes `deleted`
|
|
126
|
-
* (level 8), then ends the watches on it and drops its rules. A null actor is repo sync
|
|
126
|
+
* (level 8), then ends the watches on it and drops its rules. A null actor is repo sync or
|
|
127
|
+
* node deletion. What it linked to is re-checked by {@link collectUnlinked}. */
|
|
127
128
|
export declare function deleteDocument(db: DatabaseSync, reader: Reader | null, doc: ObjectRow, reposFn?: ReposFn): ObjectRow;
|
|
129
|
+
/** Delete each of `targets` that is a kept document nothing links to any more: a live document
|
|
130
|
+
* whose owner is deleted (node deletion kept it because a live document linked to it) and that
|
|
131
|
+
* no other live document links to. Each one deleted re-checks what it linked to, so chains of
|
|
132
|
+
* kept documents collapse; a cycle of kept documents keeps itself. Call it after a document
|
|
133
|
+
* stops linking to `targets` — it was deleted, or an edit dropped the links. */
|
|
134
|
+
export declare function collectUnlinked(db: DatabaseSync, targets: Iterable<number>, reposFn?: ReposFn): void;
|
|
128
135
|
export interface ImportedRevision {
|
|
129
136
|
rev_id?: string;
|
|
130
137
|
body_hash: string;
|
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
var
|
|
2
|
-
WHERE rp.child = ? ORDER BY p.rev_id`).all(n.rowid);return{rowid:n.rowid,rev_id:n.rev_id,number:n.number,body_hash:n.body_hash,author:n.author===null?null:
|
|
3
|
-
AND NOT EXISTS (SELECT 1 FROM revision_parents p WHERE p.parent = r.rowid) ORDER BY r.number DESC, r.rev_id`).all(n.rowid).map(r=>K(t,r))}a(H,"heads");function
|
|
4
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?) RETURNING *`).get(n.rowid,e.rev_id??
|
|
1
|
+
var ee=Object.defineProperty;var a=(t,n)=>ee(t,"name",{value:n,configurable:!0});import{randomUUID as te}from"node:crypto";import{CrtrError as A,notFound as v,usage as c}from"../errors.js";import{ExitCode as j}from"../../types.js";import{canChange as L,canRead as U,ownerChain as ne}from"./access.js";import{bodyOf as re,get as oe,put as F}from"./bodies.js";import{recordActedOn as R,setLinks as ie}from"./edges.js";import{publish as S}from"./events.js";import{resolveName as se}from"./names.js";import{createObject as ae,getById as ue,getByRowid as w}from"./objects.js";import{replaceRules as J,setRulesOwner as ce}from"./rules.js";import{recordWatch as k}from"./watches.js";function E(t){return t.class==="node"?t.node:t.class==="app"?t.app:t.person}a(E,"actorOf");let pe=0;function h(t,n){const e=`graph_doc_${++pe}`;t.exec(`SAVEPOINT ${e}`);try{const r=n();return t.exec(`RELEASE ${e}`),r}catch(r){throw t.exec(`ROLLBACK TO ${e}`),t.exec(`RELEASE ${e}`),r}}a(h,"atomic");function le(t,n){return new A("conflict",t,j.GENERAL,n)}a(le,"conflict");function _(t){if(t.type!=="document")throw c(`${t.type} ${t.id} is not a document`)}a(_,"assertDocument");function B(t,n){const e=ne(t,n);return(e.length?e[e.length-1]:n).type}a(B,"spaceType");function I(t,n,e){if(e!=="public"&&B(t,n)==="repo")throw c(`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>}`)."})}a(I,"assertPublicInRepo");function P(t,n,e,r){if(e.trim()===""||e!==e.trim())throw c(`invalid document name ${JSON.stringify(e)}`);const o=t.prepare("SELECT id FROM objects WHERE type = 'document' AND owner = ? AND name = ? AND deleted_at IS NULL").get(n.rowid,e);if(o&&o.id!==r?.id)throw le(`${n.name} already has a document named ${e}`,{name:e,holder:o.id})}a(P,"assertNameFree");function $(t,n,e){if(!U(t,n,e))throw v(`no document ${e.id}`)}a($,"readable");function O(t,n,e){if($(t,n,e),!L(t,n,e))throw new A("forbidden",`you cannot change ${e.name}`,j.GENERAL,{id:e.id})}a(O,"changeable");function K(t,n){const e=t.prepare(`SELECT p.rev_id FROM revision_parents rp JOIN revisions p ON p.rowid = rp.parent
|
|
2
|
+
WHERE rp.child = ? ORDER BY p.rev_id`).all(n.rowid);return{rowid:n.rowid,rev_id:n.rev_id,number:n.number,body_hash:n.body_hash,author:n.author===null?null:w(t,n.author)?.id??null,rationale:n.rationale,verbatim:n.verbatim===1,created_at:n.created_at,parents:e.map(r=>r.rev_id)}}a(K,"toRevision");function H(t,n){return t.prepare(`SELECT * FROM revisions r WHERE r.document = ?
|
|
3
|
+
AND NOT EXISTS (SELECT 1 FROM revision_parents p WHERE p.parent = r.rowid) ORDER BY r.number DESC, r.rev_id`).all(n.rowid).map(r=>K(t,r))}a(H,"heads");function me(t){return t.map(n=>n.rev_id).sort().join(",")}a(me,"headsKeyOf");function D(t,n){const e=t.prepare("SELECT * FROM documents WHERE object = ?").get(n.rowid);if(!e)throw v(`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}}a(D,"documentRecord");function x(t,n,e){const r=t.prepare(`INSERT INTO revisions (document, rev_id, number, body_hash, author, rationale, verbatim, created_at)
|
|
4
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?) RETURNING *`).get(n.rowid,e.rev_id??te(),e.number,e.body_hash,e.author?.rowid??null,e.rationale,e.verbatim?1:0,e.created_at??new Date().toISOString()),o=t.prepare("INSERT INTO revision_parents (child, parent) VALUES (?, ?)");for(const i of e.parents)o.run(r.rowid,i);return r}a(x,"insertRevision");function de(t){const n=new Set;for(const e of t.matchAll(/\[\[([^[\]\n]+?)\]\]/g))n.add(e[1].trim());return[...n]}a(de,"linksIn");function G(t,n,e,r){const o=[],i=[];for(const u of de(r)){const s=se(t,n,u);"object"in s?o.push({written_as:u,dst:s.object}):"deleted"in s?o.push({written_as:u,dst:s.deleted}):"remote"in s||i.push(u)}return Q(t,ie(t,e,o)),i}a(G,"setRevisionLinks");function we(t,n,e){return n.type==="node"?n.id:t.prepare(`SELECT 1 FROM objects o JOIN objects d ON d.owner = o.rowid
|
|
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,e)?`${n.type}:${n.name}`:n.name}a(
|
|
7
|
-
WHERE e.type = 'links-to' AND e.src = ? AND e.written_as IS NOT NULL`).all(n.rowid);if(r.length===0)return e;const o=new Map;for(const i of r){if(i.deleted_at||i.written_as===i.id)continue;const u=!i.written_as.includes("/");if(u&&i.name===i.written_as)continue;const s=i.owner===null?null:
|
|
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}a(we,"ownerPrefix");function V(t,n,e){const r=t.prepare(`SELECT e.written_as, o.* FROM edges e JOIN objects o ON o.rowid = e.dst
|
|
7
|
+
WHERE e.type = 'links-to' AND e.src = ? AND e.written_as IS NOT NULL`).all(n.rowid);if(r.length===0)return e;const o=new Map;for(const i of r){if(i.deleted_at||i.written_as===i.id)continue;const u=!i.written_as.includes("/");if(u&&i.name===i.written_as)continue;const s=i.owner===null?null:w(t,i.owner),p=s?`${we(t,s,i.name)}/${i.name}`:i.name,l=u&&i.owner===n.owner?i.name:p;l!==i.written_as&&o.set(i.written_as,l)}return o.size===0?e:e.replace(/\[\[([^[\]\n]+?)\]\]/g,(i,u)=>{const s=o.get(u.trim());return s===void 0?i:`[[${s}]]`})}a(V,"withCurrentNames");function C(t,n,e){if(e.preview!==void 0){if(e.preview.trim()==="")throw c("a document needs a preview",{field:"preview"});t.prepare("UPDATE objects SET preview = ? WHERE rowid = ?").run(e.preview,n.rowid)}e.privacy!==void 0&&(I(t,w(t,n.owner),e.privacy),t.prepare("UPDATE objects SET privacy = ? WHERE rowid = ?").run(e.privacy,n.rowid));const r=a((i,u)=>t.prepare(`UPDATE documents SET ${i} = ? WHERE object = ?`).run(u,n.rowid),"set");e.summary!==void 0&&r("summary",e.summary),e.whyItExists!==void 0&&r("why_it_exists",e.whyItExists),e.unlisted!==void 0&&r("unlisted",e.unlisted?1:0),e.lintIgnore!==void 0&&r("lint_ignore_json",e.lintIgnore===null?null:JSON.stringify(e.lintIgnore)),e.extensions!==void 0&&r("extensions_json",e.extensions===null?null:JSON.stringify(e.extensions));const o=w(t,n.rowid);return e.rules!==void 0&&J(t,o,e.rules),o}a(C,"applyDocumentFields");function Y(t,n){if(n.owner.deleted_at)throw c(`${n.owner.name} is deleted`);if(n.preview.trim()==="")throw c("a document needs a preview",{field:"preview"});I(t,n.owner,n.privacy),P(t,n.owner,n.name);const e=ae(t,{...n.id?{id:n.id}:{},type:"document",kind:n.kind,owner:n.owner.rowid,privacy:n.privacy,name:n.name,preview:n.preview});return t.prepare("INSERT INTO documents (object, heads_key, revision) VALUES (?, '', 0)").run(e.rowid),C(t,e,{...n,preview:void 0,privacy:void 0,rules:n.rules??[]})}a(Y,"createDocument");function W(t,n){const e=H(t,n),r=me(e),o=Math.max(0,...e.map(i=>i.number));return t.prepare("UPDATE documents SET heads_key = ?, revision = ? WHERE object = ?").run(r,o,n.rowid),{heads_key:r,revision:o,headCount:e.length}}a(W,"setHeads");function Ae(t,n,e,r,o){const i=F(r);return h(t,()=>{const u=E(n),s=Y(t,e),p=x(t,s,{number:1,body_hash:i,author:u,rationale:null,parents:[]}),{heads_key:l}=W(t,s);if(!L(t,n,s))throw v(`no owner ${e.owner.name} you can write to`);const d=G(t,n,s,r);return R(t,u,s),k(t,u,s,"write",{readHeadsKey:l}),S(t,{event:"created",object:s,actor:u,level:4,attrs:{revision:1},pointer:p.rev_id},o),{object:s,revision:1,unresolved:d}})}a(Ae,"writeDocument");function je(t,n,e){return t.prepare("SELECT read_heads_key FROM watches WHERE watcher = ? AND object = ?").get(E(n).rowid,e.rowid)?.read_heads_key??null}a(je,"readBase");function fe(t,n){let e=t;for(const{old:r,new:o}of n){if(r==="")throw c("a replacement needs non-empty old text",{field:"replace"});const i=e.indexOf(r);if(i<0)throw c(`old text not found: ${JSON.stringify(r.slice(0,120))}`,{field:"replace",old:r});if(e.indexOf(r,i+1)>=0)throw c(`old text occurs more than once: ${JSON.stringify(r.slice(0,120))}`,{field:"replace",old:r});e=e.slice(0,i)+o+e.slice(i+r.length)}return e}a(fe,"applyReplacements");function Le(t,n,e,r,o,i,u={},s){if(_(e),O(t,n,e),e.deleted_at)throw c(`${e.name} is deleted`);if(i===null)throw new A("read_first",`read ${e.name} before editing it`,j.GENERAL,{id:e.id});if(o.trim()==="")throw c("an edit needs a rationale",{field:"rationale"});const p=D(t,e);if(p.heads_key!==i)return{stale:{heads_key:p.heads_key,revision:p.revision}};const l=H(t,e);let d;if("body"in r)d=r.body;else{if(l.length!==1)throw c(`${e.name} has ${l.length} heads and needs a merge; pass the whole merged body`,{heads:l.length});d=fe(V(t,e,oe(l[0].body_hash)),r.replace)}const m=F(d);try{return h(t,()=>{const f=E(n),N=1+Math.max(0,...l.map(y=>y.number)),M=x(t,e,{number:N,body_hash:m,author:f,rationale:o,verbatim:r.verbatim===!0,parents:l.map(y=>y.rowid)}),T=M.rev_id,Z=t.prepare("UPDATE documents SET heads_key = ?, revision = ? WHERE object = ? AND heads_key = ?").run(T,N,e.rowid,i);if(Number(Z.changes)===0){const y=D(t,e);throw new X(y.heads_key,y.revision)}const g=C(t,e,u),b=G(t,n,g,d);return R(t,f,g),k(t,f,g,"write",{readHeadsKey:T}),S(t,{event:"edited",object:g,actor:f,level:8,pointer:M.rev_id,attrs:{revision:N,from_revision:p.revision,rationale:o}},s),{revision:N,heads_key:T,unresolved:b}})}catch(f){if(f instanceof X)return{stale:{heads_key:f.heads_key,revision:f.revision}};throw f}}a(Le,"editDocument");class X extends Error{static{a(this,"StaleEdit")}heads_key;revision;constructor(n,e){super("stale"),this.heads_key=n,this.revision=e}}function ke(t,n,e,r){return _(e),n&&O(t,n,e),h(t,()=>{const o=C(t,e,r);return n&&R(t,E(n),o),o})}a(ke,"setDocumentRecord");function he(t,n,e,r){const o=E(n),{heads_key:i}=D(t,e);k(t,o,e,"read",{readHeadsKey:i}),S(t,{event:"read",object:e,actor:o,level:1,attrs:{}},r)}a(he,"recordRead");function Ie(t,n,e,r){return _(e),$(t,n,e),h(t,()=>{const o=D(t,e),i=H(t,e).map(s=>({...s,body:V(t,e,re(s.body_hash,o.source_path))})),u=t.prepare("SELECT dst, written_as FROM edges WHERE type = 'links-to' AND src = ? ORDER BY written_as").all(e.rowid).map(s=>({written_as:s.written_as??"",object:w(t,s.dst)})).filter(s=>s.object&&U(t,n,s.object)).map(s=>({...s,deleted:s.object.deleted_at!==null}));return e.deleted_at||he(t,n,e,r),{object:e,record:o,heads:i,needsMerge:i.length>1,links:u}})}a(Ie,"readDocument");function $e(t,n,e){return _(e),$(t,n,e),t.prepare("SELECT * FROM revisions WHERE document = ? ORDER BY number DESC, created_at DESC, rowid DESC").all(e.rowid).map(o=>K(t,o))}a($e,"history");function He(t,n,e,r){if(_(e),e.deleted_at)throw c(`${e.name} is deleted`);return n&&O(t,n,e),h(t,()=>{const o=r.owner??w(t,e.owner),i=r.name??e.name;P(t,o,i,e),I(t,o,e.privacy),t.prepare("UPDATE objects SET name = ?, owner = ? WHERE rowid = ?").run(i,o.rowid,e.rowid);const u=w(t,e.rowid);if(o.rowid!==e.owner&&(ce(t,u),n&&!L(t,n,u)))throw v(`no owner ${o.name} you can move documents to`);return n&&R(t,E(n),u),u})}a(He,"moveDocument");function Ce(t,n,e,r){return _(e),e.deleted_at?e:(n&&O(t,n,e),h(t,()=>{const o=q(t,n,e,r);return Q(t,z(t,e.rowid),r),o}))}a(Ce,"deleteDocument");function q(t,n,e,r){t.prepare("UPDATE objects SET deleted_at = ? WHERE rowid = ?").run(new Date().toISOString(),e.rowid);const o=w(t,e.rowid),i=n?E(n):null;return i&&R(t,i,o),S(t,{event:"deleted",object:o,actor:i,level:8,attrs:{}},r),t.prepare("DELETE FROM watches WHERE object = ?").run(e.rowid),J(t,o,[]),o}a(q,"markDeleted");function z(t,n){return t.prepare("SELECT dst FROM edges WHERE type = 'links-to' AND src = ?").all(n).map(({dst:e})=>e)}a(z,"linkTargets");function Q(t,n,e){const r=t.prepare(`SELECT 1 FROM objects d JOIN objects w ON w.rowid = d.owner
|
|
8
|
+
WHERE d.rowid = ? AND d.type = 'document' AND d.deleted_at IS NULL AND w.deleted_at IS NOT NULL
|
|
9
|
+
AND NOT EXISTS (SELECT 1 FROM edges e JOIN objects s ON s.rowid = e.src
|
|
10
|
+
WHERE e.type = 'links-to' AND e.dst = d.rowid AND e.src != d.rowid AND s.deleted_at IS NULL)`),o=[...n];for(;o.length>0;){const i=o.pop();r.get(i)!==void 0&&(q(t,null,w(t,i),e),o.push(...z(t,i)))}}a(Q,"collectUnlinked");function We(t,n){if(n.revisions.length===0)throw c(`import of ${n.name}: no revisions`);return h(t,()=>{const e=Y(t,n);t.prepare("UPDATE objects SET created_at = ? WHERE rowid = ?").run(n.revisions[0].created_at,e.rowid);let r=null;return n.revisions.forEach((o,i)=>{r=x(t,e,{...o,number:i+1,parents:r===null?[]:[r]}).rowid}),W(t,e),w(t,e.rowid)})}a(We,"importDocument");function Me(t,n,e){if(_(n),n.privacy!=="public"&&B(t,n)==="repo")throw c(`a repo owns only public documents; refusing revisions of ${n.name}`);return h(t,()=>{const r=t.prepare("SELECT rowid, number, document FROM revisions WHERE rev_id = ?");let o=e.filter(s=>!r.get(s.rev_id));for(;o.length>0;){const s=[];for(const p of o){const l=p.parents.map(m=>r.get(m));if(l.some(m=>m===void 0)){s.push(p);continue}const d=l;if(d.some(m=>m.document!==n.rowid))throw c(`revision ${p.rev_id} names a parent of another document`);x(t,n,{...p,author:null,number:1+Math.max(0,...d.map(m=>m.number)),parents:d.map(m=>m.rowid)})}if(s.length===o.length)throw c(`revisions of ${n.name} name unknown parents: ${s.map(p=>p.rev_id).join(", ")}`);o=s}const{heads_key:i,headCount:u}=W(t,n);return{heads_key:i,headCount:u}})}a(Me,"insertImportedRevisions");function Ue(t,n){const e=ue(t,n);if(!e||e.type!=="document")throw v(`no document ${n}`);return e}a(Ue,"documentById");export{E as actorOf,C as applyDocumentFields,fe as applyReplacements,Q as collectUnlinked,Ce as deleteDocument,Ue as documentById,D as documentRecord,Le as editDocument,H as heads,$e as history,We as importDocument,Me as insertImportedRevisions,de as linksIn,He as moveDocument,je as readBase,Ie as readDocument,he as recordRead,ke as setDocumentRecord,G as setRevisionLinks,V as withCurrentNames,Ae as writeDocument};
|
|
@@ -11,11 +11,12 @@ export interface Edge {
|
|
|
11
11
|
}
|
|
12
12
|
/** One row per actor and object however many times the actor acts. */
|
|
13
13
|
export declare function recordActedOn(db: DatabaseSync, actor: ObjectRow, object: ObjectRow): void;
|
|
14
|
-
/** Replace the document's `links-to` edges with the links of its current revision.
|
|
14
|
+
/** Replace the document's `links-to` edges with the links of its current revision. Answers the
|
|
15
|
+
* rowids it no longer links to, so the caller can collect a kept document that lost its last link. */
|
|
15
16
|
export declare function setLinks(db: DatabaseSync, doc: ObjectRow, links: ReadonlyArray<{
|
|
16
17
|
written_as: string;
|
|
17
18
|
dst: ObjectRow;
|
|
18
|
-
}>):
|
|
19
|
+
}>): number[];
|
|
19
20
|
/** Documents whose body links to `object`. */
|
|
20
21
|
export declare function backlinks(db: DatabaseSync, reader: Reader, object: ObjectRow): Edge[];
|
|
21
22
|
export declare function edgesOf(db: DatabaseSync, reader: Reader, object: ObjectRow, opts?: {
|
package/dist/core/graph/edges.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
var
|
|
1
|
+
var l=Object.defineProperty;var a=(t,n)=>l(t,"name",{value:n,configurable:!0});import{canRead as y}from"./access.js";import{getByRowid as R}from"./objects.js";function k(t,n,e){t.prepare("INSERT OR IGNORE INTO edges (type, src, dst, written_as, created_at) VALUES ('acted-on', ?, ?, NULL, ?)").run(n.rowid,e.rowid,new Date().toISOString())}a(k,"recordActedOn");function L(t,n,e){const o=t.prepare("SELECT dst FROM edges WHERE type = 'links-to' AND src = ?").all(n.rowid).map(({dst:s})=>s);t.prepare("DELETE FROM edges WHERE type = 'links-to' AND src = ?").run(n.rowid);const c=t.prepare("INSERT OR IGNORE INTO edges (type, src, dst, written_as, created_at) VALUES ('links-to', ?, ?, ?, ?)"),d=new Date().toISOString();for(const s of e)c.run(n.rowid,s.dst.rowid,s.written_as,d);const i=new Set(e.map(s=>s.dst.rowid));return o.filter(s=>!i.has(s))}a(L,"setLinks");function _(t,n,e){return O(t,n,e,{type:"links-to",direction:"in"})}a(_,"backlinks");function O(t,n,e,o={}){if(!y(t,n,e))return[];const c=o.direction??"both",d=o.type?[o.type]:["acted-on","links-to"],i=[];for(const r of d)c!=="in"&&i.push(...t.prepare("SELECT * FROM edges WHERE type = ? AND src = ?").all(r,e.rowid)),c!=="out"&&i.push(...t.prepare("SELECT * FROM edges WHERE type = ? AND dst = ?").all(r,e.rowid));const s=new Set,E=[];for(const r of i){const f=`${r.type}|${r.src}|${r.dst}`;if(s.has(f))continue;s.add(f);const p=r.src===e.rowid?e:R(t,r.src),w=r.dst===e.rowid?e:R(t,r.dst);if(!p||!w)continue;const u=p===e?w:p;u!==e&&!y(t,n,u)||E.push({type:r.type,src:p,dst:w,written_as:r.written_as,created_at:r.created_at})}return E}a(O,"edgesOf");export{_ as backlinks,O as edgesOf,k as recordActedOn,L as setLinks};
|
|
@@ -14,8 +14,8 @@ export declare function resolveDelivery(configs: readonly ListenConfig[], level:
|
|
|
14
14
|
/** A subscription's own config, else the watcher node's default, else the global default. */
|
|
15
15
|
export declare function listenConfigFor(db: DatabaseSync, subscription: Pick<Subscription, 'config'>, watcher: ObjectRow): ListenConfig;
|
|
16
16
|
/**
|
|
17
|
-
* Push `ev` to every node watching the object
|
|
18
|
-
*
|
|
17
|
+
* Push `ev` to every node watching the object itself whose subscriptions receive it — a watch on
|
|
18
|
+
* an owner hears that owner's own events, not those of the objects it owns — inside the caller's write transaction. Document and job events reach
|
|
19
19
|
* only live (active|idle) watchers; node events also reach done ones without waking them. The
|
|
20
20
|
* actor is never pushed its own act, access is re-checked per watcher, and apps, persons and
|
|
21
21
|
* other non-node watchers get nothing. Writes a `messages` row only when a delivery results.
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
var
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
var C=Object.defineProperty;var i=(t,e)=>C(t,"name",{value:e,configurable:!0});import{evalCondition as M}from"../predicate.js";import{insertEventMail as j,listenDeliveryTier as W}from"../feed/messages.js";import{ownerChain as k,readCheck as H}from"./access.js";import{publishLookups as J,readerForNode as $}from"./reader.js";import{configFromJson as g}from"./watches.js";import{DEFAULT_LISTEN as N}from"./types.js";const x={created:4,read:1,watched:1,edited:8,messaged:2,sent:1,configured:1,"report.update":5,"report.final":7,"report.result":5,exited:7,killed:7};function ne(t,e,o){if(e!==void 0)return e;if(t==="deleted")return o==="document"?8:5;if(t.startsWith("notice."))throw new Error(`${t} has no default level; pass the notice's tier as a level`);return x[t]}i(ne,"levelFor");function B(t,e){const o=t.split("."),r=e.split(".");for(let s=0;s<o.length;s++){if(o[s]===">")return s===o.length-1&&r.length>s;if(s>=r.length||o[s]!=="*"&&o[s]!==r[s])return!1}return o.length===r.length}i(B,"matchSubject");const b={"next-turn":1,"after-turn":2,steer:3,interrupt:4};function U(t,e){let o=null;for(const r of t){let s=null;for(const d of r)d.threshold<=e&&(!s||d.threshold>s.threshold)&&(s=d);s&&(!o||b[s.delivery]>b[o])&&(o=s.delivery)}return o}i(U,"resolveDelivery");function oe(t,e,o){if(e.config)return e.config;const r=t.prepare("SELECT listen_default_json FROM nodes WHERE node_id = ?").get(o.id);return r?.listen_default_json?g(r.listen_default_json):N}i(oe,"listenConfigFor");const V=new Set(["messaged","sent","configured","report.update","report.final","report.result","notice.crash","notice.deadline","notice.close"]);function G(t){return t.event==="report.final"?"final":t.event==="report.result"?"completed":t.level>=7?"urgent":"update"}i(G,"inboxKind");function K(t,e){return e.type==="node"?e.id:t.prepare(`SELECT 1 FROM objects WHERE type IN ('person','app','repo','profile')
|
|
2
|
+
AND name = ? AND rowid != ? LIMIT 1`).get(e.name,e.rowid)?`${e.type}:${e.name}`:e.name}i(K,"ownerHandleOf");function X(t,e,o){const r=e.object.owner===null?null:t.prepare("SELECT * FROM objects WHERE rowid = ?").get(e.object.owner),s=r?.id??null,d=r?K(t,r):null;return{event:e.event,level:e.level,object:{id:e.object.id,type:e.object.type,kind:e.object.kind,owner:s,owner_handle:d,name:e.object.name},actor:e.actor?.id??null,attrs:o,...e.pointer===void 0?{}:{pointer:e.pointer}}}i(X,"pushFields");function O(t,e,o){const r=X(t,e,o),s=e.messageBody!==void 0&&(e.event.startsWith("report.")||e.event.startsWith("notice.")),d=s?typeof e.messageBody=="object"&&e.messageBody!==null&&!Array.isArray(e.messageBody)?{...e.messageBody,push:r}:{body:e.messageBody,push:r}:{push:r},l=s&&e.event.startsWith("report.")?e.messageBody:null;return{kind:G(e),label:l?.label??`${e.event} ${e.object.type} ${e.object.name}`,...l?.bodyRef===void 0?{}:{bodyRef:l.bodyRef},...l?.disposition===void 0?{}:{disposition:l.disposition},senderNodeId:e.actor?.type==="node"?e.actor.id:null,senderName:e.actor?.name??null,bodyJson:d,event:e.event,objectId:e.object.id,level:e.level,actorId:e.actor?.id??null}}i(O,"mailInput");function q(t){const e=t.attrs.revision,o=t.attrs.from_revision??(typeof e=="number"?e-1:null),r=t.attrs.rationale;return{...t.attrs,from_revision:o,revision:e,authors:t.actor?[t.actor.id]:[],rationales:typeof r=="string"?[r]:[]}}i(q,"editAttrs");function z(t,e){const o=[...t.authors??[],...e.authors.filter(r=>!(t.authors??[]).includes(r))];return{...e,from_revision:t.from_revision??null,authors:o,rationales:[...t.rationales??[],...e.rationales]}}i(z,"mergeEdits");function re(t,e,o=()=>[]){const r=k(t,e.object),s=V.has(e.event),d="w.object = ? AND w.watcher != ?",l="w.watcher, o.id AS watcher_id, n.status, n.listen_default_json, s.pattern, s.filter_json, s.config_json",m=t.prepare(s?`SELECT ${l} FROM watches w JOIN watch_subscriptions s ON s.watch = w.rowid
|
|
3
|
+
JOIN objects o ON o.rowid = w.watcher AND o.type = 'node' JOIN nodes n ON n.node_id = o.id WHERE ${d}`:`SELECT ${l} FROM nodes n CROSS JOIN objects o CROSS JOIN watches w CROSS JOIN watch_subscriptions s
|
|
4
|
+
WHERE n.status IN ('active','idle') AND o.id = n.node_id AND w.watcher = o.rowid AND s.watch = w.rowid AND ${d}`).all(e.object.rowid,e.actor?.rowid??-1);if(m.length===0)return{delivered:0};const R={event:e.event,level:e.level,id:e.object.id,type:e.object.type,kind:e.object.kind,actor:e.actor?.id??null,...e.attrs},p=new Map;for(const n of m){if(!B(n.pattern,e.event)||n.filter_json!==null&&!M(JSON.parse(n.filter_json),R))continue;const c=n.config_json?g(n.config_json):n.listen_default_json?g(n.listen_default_json):N,a=p.get(n.watcher)??{id:n.watcher_id,status:n.status,configs:[]};a.configs.push(c),p.set(n.watcher,a)}const S=(r.length?r[r.length-1]:e.object).type==="repo",D=i(()=>[],"noRepos"),A=H(t,e.object),I=J(t),u=[];for(const n of p.values()){const c=U(n.configs,e.level);if(c===null||!A($(t,n.id,S?o:D,I)))continue;const a=W(c),f=n.status==="active"||n.status==="idle";u.push({nodeId:n.id,urgency:a.urgency,wake:f?a.wake:0})}if(u.length===0)return{delivered:0};if(e.event!=="edited")return j(t,O(t,e,e.attrs),u),{delivered:u.length};const E=q(e),T=t.prepare(`SELECT d.delivery_id, d.message_id, m.body_json FROM deliveries d
|
|
4
5
|
JOIN messages m ON m.message_id = d.message_id
|
|
5
6
|
WHERE d.destination_kind = 'node_inbox' AND d.destination_id = ? AND d.state = 'pending' AND d.canceled_at IS NULL
|
|
6
|
-
AND m.event = 'edited' AND m.object_id = ?`),h=new Map,
|
|
7
|
+
AND m.event = 'edited' AND m.object_id = ?`),h=new Map,_=new Set,L=t.prepare("DELETE FROM deliveries WHERE delivery_id = ?");for(const n of u){const c=T.all(n.nodeId,e.object.id),a=c.at(-1);for(const w of c)L.run(w.delivery_id),_.add(w.message_id);const f=a?.message_id??null,y=h.get(f)??{prior:a?.body_json?JSON.parse(a.body_json).push?.attrs??null:null,destinations:[]};y.destinations.push(n),h.set(f,y)}for(const n of h.values())j(t,O(t,e,n.prior?z(n.prior,E):E),n.destinations);const F=t.prepare("DELETE FROM messages WHERE message_id = ? AND NOT EXISTS (SELECT 1 FROM deliveries WHERE message_id = ?)");for(const n of _)F.run(n,n);return{delivered:u.length}}i(re,"publish");export{ne as levelFor,oe as listenConfigFor,B as matchSubject,re as publish,U as resolveDelivery};
|
|
@@ -18,7 +18,9 @@ export declare function viewOf(db: DatabaseSync, reader: Reader): ObjectRow[];
|
|
|
18
18
|
/** The owner types that carry a handle; `<type>:<handle>/<name>` names one exactly. */
|
|
19
19
|
export declare const HANDLE_TYPES: readonly ["person", "app", "repo", "profile"];
|
|
20
20
|
/** Every owner a handle names: a node by id, and each handle-carrying owner type
|
|
21
|
-
* holding that handle (handles are unique per type, not across types).
|
|
21
|
+
* holding that handle (handles are unique per type, not across types). Any owner
|
|
22
|
+
* also answers to its object id, so `<owner object id>/<name>` names a document
|
|
23
|
+
* whatever its owner's type. */
|
|
22
24
|
export declare function ownersByHandle(db: DatabaseSync, handle: string, type?: string): ObjectRow[];
|
|
23
25
|
/** `name` may be wrapped in `[[ ]]`, a raw object id, a bare document name,
|
|
24
26
|
* `<owner handle>/<name>`, or `<type>:<owner handle>/<name>`. Under a handle
|
package/dist/core/graph/names.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
var
|
|
2
|
-
AND deleted_at IS ${o?"NOT NULL ORDER BY deleted_at DESC":"NULL"}`).all(
|
|
3
|
-
OR (type IN ('person','app','repo','profile') AND name = ?)`).all(
|
|
1
|
+
var A=Object.defineProperty;var a=(t,e)=>A(t,"name",{value:e,configurable:!0});import{isSafeNodeId as L}from"../canvas/paths.js";import{canRead as S,toolApps as j}from"./access.js";import{getById as x}from"./objects.js";function I(t,e){const r=t.prepare("SELECT * FROM objects WHERE type = 'person' AND name = 'user'").get(),o=j(t,e),u=e.class==="node"?[e.node,...e.repos,e.profile,e.app]:e.class==="app"?[e.app]:[],s=new Set;return[...u,r,...o].filter(i=>!!i&&!s.has(i.rowid)&&!!s.add(i.rowid))}a(I,"viewOf");function d(t,e,r,o){return t.prepare(`SELECT * FROM objects WHERE type = 'document' AND owner = ? AND name = ?
|
|
2
|
+
AND deleted_at IS ${o?"NOT NULL ORDER BY deleted_at DESC":"NULL"}`).all(e.rowid,r)}a(d,"documentsNamed");const h=["person","app","repo","profile"],O=/^(person|app|repo|profile|node):([^/]+)\/(.+)$/;function T(t,e,r){return r==="node"?t.prepare("SELECT * FROM objects WHERE type = 'node' AND id = ?").all(e):r?t.prepare("SELECT * FROM objects WHERE type = ? AND (name = ? OR id = ?)").all(r,e,e):t.prepare(`SELECT * FROM objects WHERE (type = 'node' AND id = ?)
|
|
3
|
+
OR (type IN ('person','app','repo','profile') AND (name = ? OR id = ?))`).all(e,e,e)}a(T,"ownersByHandle");function F(t,e,r){let o=r.trim().replace(/^\[\[(.*)\]\]$/,"$1").trim();const u=/^([^:/]+):reports\/(.+?)(?:\.md)?$/.exec(o);u&&!O.test(o)&&(o=`${u[1]}/reports/${u[2]}`);const s=a(c=>S(t,e,c),"readable"),i=x(t,o);if(i&&s(i))return i.deleted_at?{deleted:i}:{object:i};const R=I(t,e);for(const c of R){const p=d(t,c,o,!1).find(s);if(p)return{object:p}}const f=O.exec(o),N=o.indexOf("/");if(f||N>0){const c=f?f[2]:o.slice(0,N),p=f?f[3]:o.slice(N+1),E=T(t,c,f?.[1]),w=a(n=>{const l=R.findIndex(y=>y.rowid===n.rowid);return l<0?1/0:l},"rank"),g=E.filter(n=>w(n)<1/0).sort((n,l)=>w(n)-w(l));for(const n of g){const l=d(t,n,p,!1).find(s);if(l)return{object:l}}const m=E.flatMap(n=>d(t,n,p,!1)).filter(s);if(m.length===1)return{object:m[0]};if(m.length>1)return{ambiguous:m};const D=E.flatMap(n=>d(t,n,p,!0)).find(s);return D?{deleted:D}:!f&&E.length===0&&L(c)?{remote:o}:{missing:!0}}for(const c of R){const p=d(t,c,o,!0).find(s);if(p)return{deleted:p}}return{missing:!0}}a(F,"resolveName");export{h as HANDLE_TYPES,T as ownersByHandle,F as resolveName,I as viewOf};
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
var A=Object.defineProperty;var f=(e,t)=>A(e,"name",{value:t,configurable:!0});import{get as B,put as T}from"../bodies.js";import{setLinks as j}from"../edges.js";import{deleteDocument as S,documentRecord as D,heads as M,importDocument as
|
|
2
|
-
WHERE e.type = 'links-to' AND e.src = ? ORDER BY e.written_as, o.id`),d=new Map;for(const n of l){const m=D(e,n),y=new Map((i.get(n.id)?.revisions??[]).map(s=>[s.rev_id,s])),h=new Set(M(e,n).map(s=>s.rev_id)),g=o.all(n.rowid).map(s=>({written_as:s.written_as??"",dst:s.id})),v=u.all(n.rowid).map(s=>y.get(s.rev_id)??{rev_id:s.rev_id,number:s.number,parents:p.all(s.rowid).map(O=>O.rev_id),body:B(s.body_hash),body_hash:s.body_hash,author:s.author===null?null:
|
|
1
|
+
var A=Object.defineProperty;var f=(e,t)=>A(e,"name",{value:t,configurable:!0});import{get as B,put as T}from"../bodies.js";import{setLinks as j}from"../edges.js";import{collectUnlinked as H,deleteDocument as S,documentRecord as D,heads as M,importDocument as W,insertImportedRevisions as F,moveDocument as J,setDocumentRecord as P}from"../documents.js";import{publish as $}from"../events.js";import{getById as C,getByRowid as Y,setObjectKind as K}from"../objects.js";import{rulesOf as z}from"../rules.js";import{SPACES as U}from"../spaces/index.js";import{docPath as V,parseDoc as E,serializeDoc as q}from"./ref-format.js";function I(e){const t=U[e.type];if(e.owner!==null||!t||t.sync!=="repo-ref")throw new Error(`${e.type} ${e.id} is not a space that syncs through a repo ref`)}f(I,"assertRepoRefSpace");function G(e){const t=new Map;for(const[r,i]of e??[]){const l=E(r,i);l.ok&&t.set(l.doc.id,l.doc)}return t}f(G,"parseMap");function de(e,t,r){I(t);const i=G(r),l=e.prepare("SELECT * FROM objects WHERE type = 'document' AND owner = ? AND privacy = 'public' ORDER BY rowid").all(t.rowid),u=e.prepare("SELECT rowid, rev_id, number, body_hash, author, rationale, created_at FROM revisions WHERE document = ? ORDER BY number, rev_id"),p=e.prepare("SELECT p.rev_id FROM revision_parents rp JOIN revisions p ON p.rowid = rp.parent WHERE rp.child = ?"),o=e.prepare(`SELECT e.written_as, o.id FROM edges e JOIN objects o ON o.rowid = e.dst
|
|
2
|
+
WHERE e.type = 'links-to' AND e.src = ? ORDER BY e.written_as, o.id`),d=new Map;for(const n of l){const m=D(e,n),y=new Map((i.get(n.id)?.revisions??[]).map(s=>[s.rev_id,s])),h=new Set(M(e,n).map(s=>s.rev_id)),g=o.all(n.rowid).map(s=>({written_as:s.written_as??"",dst:s.id})),v=u.all(n.rowid).map(s=>y.get(s.rev_id)??{rev_id:s.rev_id,number:s.number,parents:p.all(s.rowid).map(O=>O.rev_id),body:B(s.body_hash),body_hash:s.body_hash,author:s.author===null?null:Y(e,s.author)?.id??null,rationale:s.rationale,created_at:s.created_at,links:h.has(s.rev_id)?g:[]});if(v.length===0)continue;const R=z(e,n).map(s=>({position:s.position,on:s.on,match:s.match??null,match_frontmatter:s.matchFrontmatter??null,if:s.if??null,deliver:s.deliver})),_={id:n.id,name:n.name,kind:n.kind??"document",privacy:"public",preview:n.preview,summary:m.summary,why_it_exists:m.why_it_exists,unlisted:m.unlisted,lint_ignore:m.lint_ignore,extensions:m.extensions,created_at:n.created_at,deleted_at:n.deleted_at,rules:R,revisions:v};d.set(V(n.id),q(_))}return d}f(de,"exportRepo");function Q(e){return[...e].sort((t,r)=>t.position-r.position).map(t=>({on:t.on,deliver:t.deliver,...t.match!==null&&t.match!==void 0?{match:t.match}:{},...t.match_frontmatter!==null&&t.match_frontmatter!==void 0?{matchFrontmatter:t.match_frontmatter}:{},...t.if!==null&&t.if!==void 0?{if:t.if}:{}}))}f(Q,"toRuleSpecs");const X=f((e,t)=>JSON.stringify(e??null)===JSON.stringify(t??null),"same");function N(e,t){const r={},i=f(l=>!e||!X(e[l],t[l]),"differs");return i("preview")&&(r.preview=t.preview),i("summary")&&(r.summary=t.summary),i("why_it_exists")&&(r.whyItExists=t.why_it_exists),i("unlisted")&&(r.unlisted=t.unlisted),i("lint_ignore")&&(r.lintIgnore=t.lint_ignore),i("extensions")&&(r.extensions=t.extensions),i("rules")&&(r.rules=Q(t.rules)),r}f(N,"changedFields");let Z=0;function L(e,t){const r=`repo_sync_import_${++Z}`;e.exec(`SAVEPOINT ${r}`);try{const i=t();return e.exec(`RELEASE ${r}`),i}catch(i){throw e.exec(`ROLLBACK TO ${r}`),e.exec(`RELEASE ${r}`),i}}f(L,"atomic");function le(e,t,r,i,l){I(t);const u={created:0,revisions:0,deleted:0,needsMerge:[],refused:[]},p=[];for(const[o,d]of i){if(r.get(o)===d)continue;const n=E(o,d);if(!n.ok){u.refused.push({path:o,reason:n.reason});continue}const m=r.has(o)?E(o,r.get(o)):null;p.push({path:o,prior:m?.ok?m.doc:null,doc:n.doc})}for(const[o,d]of r){if(i.has(o))continue;const n=E(o,d);n.ok&&p.push({path:o,prior:n.doc,doc:null,priorId:n.doc.id})}for(const o of p)for(const d of o.doc?.revisions??[])T(d.body);return L(e,()=>{for(const o of p)try{L(e,()=>b(e,t,o.prior,o.doc,o.priorId??o.doc.id,u,l))}catch(d){u.refused.push({path:o.path,reason:d.message})}}),u}f(le,"importTree");function b(e,t,r,i,l,u,p){const o=C(e,l);if(o&&(o.type!=="document"||o.owner!==t.rowid))throw new Error(`id ${l} is held by ${o.type} ${o.name} outside ${t.name}`);if(!i){o&&!o.deleted_at&&(S(e,null,o,p),u.deleted++);return}const d=e.prepare("SELECT count(*) AS n FROM revisions WHERE document = ?");let n,m=!1,y=0,h=0;if(o){n=o,y=D(e,n).revision;const a=i.kind;(r?r.kind!==a:n.kind!==a)&&K(e,n,a);const c=N(r,i);Object.keys(c).length>0&&(n=P(e,null,n,c)),i.name!==n.name&&!n.deleted_at&&(n=J(e,null,n,{name:i.name}))}else{const c=i.revisions.filter(w=>w.parents.length===0).sort((w,x)=>w.number-x.number||w.rev_id.localeCompare(x.rev_id))[0];if(!c)throw new Error(`${i.name}: no root revision`);n=W(e,{id:i.id,owner:t,name:i.name,kind:i.kind,preview:i.preview,privacy:"public",...N(null,i),revisions:[{rev_id:c.rev_id,body_hash:c.body_hash,author:null,rationale:c.rationale,created_at:c.created_at}]}),m=!0,h=1}const g=d.get(n.rowid).n,{headCount:v}=F(e,n,i.revisions.map(a=>({rev_id:a.rev_id,parents:a.parents,body_hash:a.body_hash,rationale:a.rationale,created_at:a.created_at})));h+=d.get(n.rowid).n-g,u.revisions+=h;const _=M(e,n)[0],s=new Map(i.revisions.map(a=>[a.rev_id,a]));if(h>0){const a=(s.get(_.rev_id)?.links??[]).map(c=>({written_as:c.written_as,dst:C(e,c.dst)})).filter(c=>c.dst!==null);H(e,j(e,n,a),p)}if(v>1&&u.needsMerge.push(n.id),i.deleted_at&&!n.deleted_at){S(e,null,n,p),u.deleted++;return}if(m){u.created++,$(e,{event:"created",object:n,actor:null,level:4,attrs:{revision:_.number},pointer:_.rev_id},p);return}if(h===0)return;const k=i.revisions.filter(a=>!r?.revisions.some(c=>c.rev_id===a.rev_id)).sort((a,c)=>c.number-a.number||c.created_at.localeCompare(a.created_at))[0]??s.get(_.rev_id);$(e,{event:"edited",object:n,actor:null,level:8,pointer:_.rev_id,attrs:{revision:_.number,from_revision:y,rationale:k?.rationale??null,author:k?.author??null,heads:v,needs_merge:v>1}},p)}f(b,"importOne");export{de as exportRepo,le as importTree};
|