runlist 0.0.0 → 0.94.2
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/LICENSE +21 -0
- package/README.md +1112 -6
- package/assets/app/app.css +283 -0
- package/assets/app/app.mjs +381 -0
- package/assets/app/block-editor.mjs +182 -0
- package/assets/app/block-model.mjs +52 -0
- package/assets/app/document-create.mjs +86 -0
- package/assets/app/document-lifecycle.mjs +36 -0
- package/assets/app/document-stages.mjs +22 -0
- package/assets/app/document-views.mjs +33 -0
- package/assets/app/document-yardstick.mjs +258 -0
- package/assets/app/editor-navigation.mjs +51 -0
- package/assets/app/filing-navigation.mjs +138 -0
- package/assets/app/git-changes.mjs +80 -0
- package/assets/app/git-commit.mjs +46 -0
- package/assets/app/index.html +70 -0
- package/assets/app/library-navigation.mjs +93 -0
- package/assets/app/outline.mjs +18 -0
- package/assets/app/quick-navigation.mjs +52 -0
- package/assets/app/record-navigation.mjs +78 -0
- package/assets/app/recovery-center.mjs +53 -0
- package/assets/app/semantic-search.mjs +14 -0
- package/assets/app/settings.mjs +15 -0
- package/assets/app/shared.mjs +104 -0
- package/assets/app/stage-source.mjs +75 -0
- package/assets/app/template-editor.mjs +47 -0
- package/assets/app/theme.mjs +25 -0
- package/assets/app/transport.mjs +43 -0
- package/assets/contracts/source-editing-v1.md +232 -0
- package/assets/opencode/plugin.js +162 -0
- package/assets/schemas/runlist-record-v1.schema.json +912 -0
- package/bin/dotmd.mjs +5 -0
- package/bin/runlist.mjs +2714 -0
- package/package.json +53 -6
- package/plugins/runlist-codex/.codex-plugin/plugin.json +18 -0
- package/plugins/runlist-codex/bin/runlist-hook +86 -0
- package/plugins/runlist-codex/hooks/hooks.json +16 -0
- package/plugins/runlist-codex/skills/runlist/SKILL.md +25 -0
- package/runlist.config.example.mjs +370 -0
- package/scripts/postinstall.mjs +56 -0
- package/src/agent-context.mjs +201 -0
- package/src/ai.mjs +30 -0
- package/src/app-create.mjs +169 -0
- package/src/app-filing.mjs +139 -0
- package/src/app-git-commits.mjs +48 -0
- package/src/app-git.mjs +180 -0
- package/src/app-library.mjs +152 -0
- package/src/app-lifecycle-worker.mjs +12 -0
- package/src/app-lifecycle.mjs +79 -0
- package/src/app-records.mjs +134 -0
- package/src/app-search.mjs +80 -0
- package/src/app-semantic-search.mjs +105 -0
- package/src/app-service.mjs +208 -0
- package/src/app-yardstick.mjs +269 -0
- package/src/app.mjs +102 -0
- package/src/atomic-mutation.mjs +1736 -0
- package/src/baton.mjs +435 -0
- package/src/body-link.mjs +48 -0
- package/src/bulk-tag.mjs +174 -0
- package/src/check-collapse.mjs +75 -0
- package/src/claude-commands.mjs +75 -0
- package/src/cli-editor.mjs +8 -0
- package/src/code-refs.mjs +545 -0
- package/src/codex-integration.mjs +81 -0
- package/src/color.mjs +14 -0
- package/src/commands.mjs +374 -0
- package/src/completions.mjs +93 -0
- package/src/config-edit.mjs +606 -0
- package/src/config.mjs +705 -0
- package/src/decision-action.mjs +58 -0
- package/src/decision.mjs +220 -0
- package/src/decisions.mjs +787 -0
- package/src/deps.mjs +317 -0
- package/src/desktop-install.mjs +158 -0
- package/src/desktop-runtime.mjs +4 -0
- package/src/desktop.mjs +25 -0
- package/src/diff.mjs +101 -0
- package/src/doctor.mjs +591 -0
- package/src/durable-rename.mjs +55 -0
- package/src/errors-read.mjs +44 -0
- package/src/export.mjs +496 -0
- package/src/extractors.mjs +101 -0
- package/src/filing.mjs +255 -0
- package/src/fix-membership.mjs +134 -0
- package/src/fix-refs.mjs +174 -0
- package/src/flags.mjs +318 -0
- package/src/frontmatter-fix.mjs +195 -0
- package/src/frontmatter.mjs +252 -0
- package/src/git-commit-filters.mjs +32 -0
- package/src/git-commit-protocol.mjs +198 -0
- package/src/git-commit-signing.mjs +75 -0
- package/src/git-commit-store.mjs +26 -0
- package/src/git-commit-support.mjs +16 -0
- package/src/git-commit-worker.mjs +32 -0
- package/src/git.mjs +909 -0
- package/src/glossary-check.mjs +35 -0
- package/src/glossary.mjs +243 -0
- package/src/graph.mjs +298 -0
- package/src/guard.mjs +488 -0
- package/src/health.mjs +212 -0
- package/src/hints.mjs +129 -0
- package/src/host-integration.mjs +339 -0
- package/src/hub-membership.mjs +284 -0
- package/src/hub.mjs +296 -0
- package/src/hud.mjs +436 -0
- package/src/index-file.mjs +163 -0
- package/src/index.mjs +478 -0
- package/src/init.mjs +512 -0
- package/src/install.mjs +222 -0
- package/src/journal-read.mjs +116 -0
- package/src/journal.mjs +472 -0
- package/src/lifecycle.mjs +1444 -0
- package/src/lint.mjs +386 -0
- package/src/managed-path.mjs +192 -0
- package/src/markdown-code-spans.mjs +58 -0
- package/src/migrate-prompts.mjs +171 -0
- package/src/migrate-template.mjs +170 -0
- package/src/migrate.mjs +90 -0
- package/src/misuse-read.mjs +61 -0
- package/src/model-request.mjs +33 -0
- package/src/model.mjs +523 -0
- package/src/modules.mjs +355 -0
- package/src/naming.mjs +55 -0
- package/src/native-action.mjs +59 -0
- package/src/native-record.mjs +244 -0
- package/src/new.mjs +1321 -0
- package/src/output-identity.mjs +111 -0
- package/src/parent-plan.mjs +58 -0
- package/src/parse-cache.mjs +99 -0
- package/src/path-containment.mjs +15 -0
- package/src/pickup-card.mjs +293 -0
- package/src/pickup.mjs +671 -0
- package/src/prompt.mjs +25 -0
- package/src/prompts.mjs +585 -0
- package/src/public-inspection.mjs +19 -0
- package/src/query.mjs +977 -0
- package/src/record-action.mjs +11 -0
- package/src/record-create.mjs +82 -0
- package/src/reference-planner.mjs +388 -0
- package/src/reference-values.mjs +11 -0
- package/src/rename.mjs +107 -0
- package/src/render.mjs +678 -0
- package/src/roadmap.mjs +184 -0
- package/src/runlist.mjs +944 -0
- package/src/section.mjs +225 -0
- package/src/semantic-search-client.mjs +58 -0
- package/src/ship.mjs +166 -0
- package/src/show.mjs +101 -0
- package/src/skill-drift.mjs +98 -0
- package/src/source-editor.mjs +491 -0
- package/src/stages.mjs +73 -0
- package/src/state-migration.mjs +123 -0
- package/src/stats.mjs +200 -0
- package/src/status-metadata.mjs +87 -0
- package/src/statuses.mjs +721 -0
- package/src/summary.mjs +74 -0
- package/src/surfaces.mjs +28 -0
- package/src/sync-status.mjs +316 -0
- package/src/template-store.mjs +62 -0
- package/src/update.mjs +338 -0
- package/src/use.mjs +58 -0
- package/src/util.mjs +337 -0
- package/src/validate.mjs +795 -0
- package/src/watch.mjs +56 -0
- package/src/xref.mjs +406 -0
- package/src/yardstick-command.mjs +178 -0
- package/src/yardstick.mjs +112 -0
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
# Source editing v1
|
|
2
|
+
|
|
3
|
+
`src/source-editor.mjs` is the shared local core for an existing-plan narrative
|
|
4
|
+
editing loop. It supports unambiguous legacy plans and valid native v1 plans,
|
|
5
|
+
flags, and decisions. It does not itself start a web server or change existing
|
|
6
|
+
domain CLI writers. Trusted adapters may opt into configured legacy document
|
|
7
|
+
types with `legacyTypes` and allow read-only unconfigured/untyped sources with
|
|
8
|
+
`allowUnconfiguredRead`. These are host options, never browser request fields.
|
|
9
|
+
Prompts stay excluded; frontmatter and history protections apply to every type.
|
|
10
|
+
The working document application in `src/app.mjs` supplies a
|
|
11
|
+
trusted local human adapter and consumes this core.
|
|
12
|
+
|
|
13
|
+
## Actor and authority
|
|
14
|
+
|
|
15
|
+
The trusted local host constructs `createSourceEditor({config, authenticate,
|
|
16
|
+
authorize})`. Both callbacks are required and synchronous; asynchronous
|
|
17
|
+
authentication can happen at the host boundary before calling the core, with
|
|
18
|
+
the callback resolving a verified, revocable session from server-owned state.
|
|
19
|
+
The factory/config/callbacks/test hooks are never request data. This is an
|
|
20
|
+
in-process boundary, not protection against arbitrary code with filesystem
|
|
21
|
+
access.
|
|
22
|
+
|
|
23
|
+
`authenticate(context)` returns `{kind:'human'|'agent', id, label?, session_id?}`.
|
|
24
|
+
An agent requires its actual session ID. The core never reads the launching
|
|
25
|
+
process's identity from environment variables and ignores an actor supplied in
|
|
26
|
+
the request. A human browser session remains a human even when an agent started
|
|
27
|
+
the server. A browser adapter must authenticate its own session and
|
|
28
|
+
protect mutation requests against cross-site/unauthorized access.
|
|
29
|
+
|
|
30
|
+
`authorize({actor, action, path})` verifies authority against trusted runtime
|
|
31
|
+
policy. It returns `{allowed:true}` for an authorized human; an authorized agent
|
|
32
|
+
also requires a nonempty `grant_id`. The adapter must verify that the grant is
|
|
33
|
+
current, binds the actor/session and selected checkout/path, permits the exact
|
|
34
|
+
action, and has not expired or been revoked. A grant reference in Markdown is
|
|
35
|
+
evidence to look up, never authority by itself. The core rechecks identity and
|
|
36
|
+
policy after waiting for save locks, and policy again before source publication.
|
|
37
|
+
The receipt attributes the operation to the verified actor and initial grant.
|
|
38
|
+
|
|
39
|
+
Actions are `read`, `save`, `undo`, `read-draft`, `write-draft`, `discard-draft`,
|
|
40
|
+
`inspect-operation`, and `settle-operation`. Drafts and receipts are additionally
|
|
41
|
+
bound to their owner. Agents include their session in that binding. Inspecting,
|
|
42
|
+
settling, or undoing another actor's operation is not supported by this v1 API.
|
|
43
|
+
|
|
44
|
+
Future domain operations for rulings, delegation, resolution, lifecycle, task
|
|
45
|
+
allocation, relationships, and migration must use separate action permissions.
|
|
46
|
+
An agent ruling requires a verified delegation permitting that specific ruling,
|
|
47
|
+
not merely permission to edit narrative. Recommendations or accepted awareness
|
|
48
|
+
do not create execution authority. Those writers append shared provenance to
|
|
49
|
+
native metadata through domain operations; this API cannot edit those fields.
|
|
50
|
+
|
|
51
|
+
## Source, revisions, and conflicts
|
|
52
|
+
|
|
53
|
+
`read(context,{path})` returns `{path,source,revision,type,editable,diagnostics}`.
|
|
54
|
+
`revision` is `sha256:<64 lowercase hex>` over the exact UTF-8 bytes. It is not a
|
|
55
|
+
Git commit, timestamp, frontmatter `updated`, or inferred current revision.
|
|
56
|
+
CRLF, Unicode, unknown metadata, tables, fences, links, comments, and attachments
|
|
57
|
+
are preserved because the core writes the supplied source without serialization.
|
|
58
|
+
Invalid/unsupported records remain readable and draftable, with `editable:false`.
|
|
59
|
+
The core accepts valid UTF-8 documents up to 8 MiB, without NUL bytes.
|
|
60
|
+
|
|
61
|
+
`save(context,{path,operationId,expectedRevision,source})` requires an exact base
|
|
62
|
+
revision and a caller-generated lowercase UUID v4. Frontmatter, lifecycle
|
|
63
|
+
`Version History` sections, and the sequence of stable native item anchors must
|
|
64
|
+
remain byte-identical/identity-identical. Narrative and existing task text or
|
|
65
|
+
checkboxes may change. New/removal/reordering of anchored items belongs to future
|
|
66
|
+
domain operations. Every native result is validated before publication. No
|
|
67
|
+
source serializer drops unknown fields or silently repairs invalid metadata.
|
|
68
|
+
|
|
69
|
+
A stale save throws `SourceEditError` with `code:'revision-conflict'` and
|
|
70
|
+
`details:{expectedRevision,currentRevision,currentSource}`. The application keeps
|
|
71
|
+
the draft, displays base/draft/current source and diff, and asks the user to
|
|
72
|
+
review a merge or reload. It submits a new operation ID against the explicitly
|
|
73
|
+
reviewed current revision. The core does not silently merge, retry a stale save
|
|
74
|
+
against a new revision, or choose a timestamp winner. Read/modify/save is a
|
|
75
|
+
compare-and-swap operation under the existing atomic-mutation path locks.
|
|
76
|
+
|
|
77
|
+
Configured Markdown roots authorize paths. Symlink sources, escapes, and
|
|
78
|
+
unmanaged types are rejected. A save also locks the canonical CLI ownership
|
|
79
|
+
record and checks its generation before publication. Corrupt ownership or a
|
|
80
|
+
claim held by another actor blocks saving. A human browser does not borrow the
|
|
81
|
+
launching agent's claim; the owner coordinates/releases it through the CLI.
|
|
82
|
+
Even a dead claim is not implicitly taken over by this API. An agent may save
|
|
83
|
+
its own claimed plan with verified edit authority. Saves never claim, release,
|
|
84
|
+
archive, run lifecycle hooks, repair references, stage Git, or commit Git.
|
|
85
|
+
|
|
86
|
+
Trusted checkout adapters may opt into `allowStageEdits:true`. This permits only
|
|
87
|
+
a plan's `ships:` field to change alongside narrative edits; all other envelope
|
|
88
|
+
bytes, lifecycle history and stable item identities remain protected. Stage
|
|
89
|
+
values are checked against the configured vocabulary, with an unset choice
|
|
90
|
+
allowed. Generic editor adapters retain the byte-identical frontmatter default.
|
|
91
|
+
The same save request, receipts, claims, revision checks and recovery path apply.
|
|
92
|
+
Compensating undo restores the retained exact before-image, including its prior
|
|
93
|
+
stage, without validating that old value as a new stage assignment.
|
|
94
|
+
|
|
95
|
+
## Durable operations and repair
|
|
96
|
+
|
|
97
|
+
Before replacing source, the core durably writes a private `prepared` receipt
|
|
98
|
+
containing the operation ID, request kind, path, actor/grant, timestamp, exact
|
|
99
|
+
before/after source and revisions, and optional `undoOf`. The canonical file is
|
|
100
|
+
then atomically replaced with its existing mode and its directory synchronized.
|
|
101
|
+
Finally the receipt becomes `committed`. A successful result contains the
|
|
102
|
+
operation ID/state, resulting revision, actor/grant, and affected files.
|
|
103
|
+
|
|
104
|
+
The same operation ID with identical owner/path/kind/base/source (or undo target)
|
|
105
|
+
returns the retained outcome and does not write source again. Different payload
|
|
106
|
+
reuse is `operation-reused`. A replay also reports `currentRevision`, which may
|
|
107
|
+
have changed since the historical save; the host rereads before editing further.
|
|
108
|
+
|
|
109
|
+
After a crash, a prepared receipt is reconciled against current source:
|
|
110
|
+
|
|
111
|
+
| Current source | Result |
|
|
112
|
+
| --- | --- |
|
|
113
|
+
| Exact after-image | Mark committed/recovered; do not write source again. |
|
|
114
|
+
| Exact before-image | The original owner may retry the original request; a different operation is blocked as pending. |
|
|
115
|
+
| Neither image | Report `repair-required`; preserve all images and current source, write nothing to source. |
|
|
116
|
+
|
|
117
|
+
This is content-generation recovery. It cannot prove which external writer
|
|
118
|
+
published identical bytes, or reconstruct a missing/corrupt local receipt.
|
|
119
|
+
New saves inspect pending receipts for that document before obscuring their
|
|
120
|
+
publication evidence. Source/durability errors after preparation retain the
|
|
121
|
+
receipt rather than blindly rolling source back over a later external edit.
|
|
122
|
+
|
|
123
|
+
`inspectOperation(context,{operationId})` returns the retained images and current
|
|
124
|
+
revision to the authorized owner. After explicit inspection,
|
|
125
|
+
`settleOperation(context,{operationId,expectedRevision,disposition:'leave-current'})`
|
|
126
|
+
checks that revision and leaves source untouched. It records `committed` for
|
|
127
|
+
the after-image, `not-applied` for the before-image, or `settled-unknown` for a
|
|
128
|
+
third generation. Unknown is never relabeled success/failure. The settled
|
|
129
|
+
operation cannot be retried to write; further reviewed edits use a new ID.
|
|
130
|
+
Malformed state requires manual inspection and is never silently reset. These
|
|
131
|
+
receipts are separate from existing move/lifecycle transaction manifests and
|
|
132
|
+
are inspected through this API, not `doctor --transactions`.
|
|
133
|
+
|
|
134
|
+
The existing locking and filesystem authorization are cooperative local
|
|
135
|
+
controls. Uncooperative filesystem writes can still race the final checks and
|
|
136
|
+
rename; descriptor-relative I/O is not implemented. Do not describe this as a
|
|
137
|
+
filesystem security boundary or a cross-clone transaction. A lost receipt or
|
|
138
|
+
a checkout moved to a new local path needs explicit recovery review.
|
|
139
|
+
|
|
140
|
+
## Draft recovery
|
|
141
|
+
|
|
142
|
+
`putDraft(context,{path,draftId,expectedDraftRevision,baseSource,baseRevision,source})`
|
|
143
|
+
stores a private draft, including the exact base snapshot for a later three-way
|
|
144
|
+
review. The base source must match its revision, but need not be the current
|
|
145
|
+
source; autosaving a stale draft never overwrites the document. Use `null` for a
|
|
146
|
+
new draft's expected revision. Later writes require the returned draft revision,
|
|
147
|
+
so two tabs cannot silently replace each other's recovery data.
|
|
148
|
+
|
|
149
|
+
`readDraft(context,{path,draftId})` returns the owner's draft and `stale` when the
|
|
150
|
+
canonical source differs from its base. It never applies the draft automatically.
|
|
151
|
+
`discardDraft(context,{path,draftId,expectedDraftRevision})` durably replaces it
|
|
152
|
+
with a body-free tombstone after explicit discard. A discarded ID can be used
|
|
153
|
+
again with expected revision `null`. Saves do not implicitly delete drafts;
|
|
154
|
+
the host clears them only after receiving/verifying the committed outcome.
|
|
155
|
+
Draft persistence survives host reload/disconnection after the core acknowledges
|
|
156
|
+
the draft. The plan app also keeps browser recovery snapshots before its disk
|
|
157
|
+
autosave acknowledges, and retains pending save/undo operation IDs across reload.
|
|
158
|
+
Missing, moved, or deleted canonical files require manual
|
|
159
|
+
inspection of local state in this first version; no create/rename recovery API
|
|
160
|
+
is provided yet.
|
|
161
|
+
|
|
162
|
+
State lives under the checkout's `.runlist/editor/` (or existing legacy state
|
|
163
|
+
directory), in owner-scoped draft files and an operation ledger. Files are
|
|
164
|
+
created with mode 0600 and directories with 0700; Windows uses its normal user
|
|
165
|
+
filesystem access controls. State may not traverse symlinks. In a Git checkout,
|
|
166
|
+
storage must be ignored and untracked before any body/before-image is persisted;
|
|
167
|
+
the core does not edit ignore rules. This storage is local, not shared record
|
|
168
|
+
history. No automatic expiry discards recovery data. Cleanup/retention controls
|
|
169
|
+
and a disk-wide draft inventory UI remain further work; the first app discovers
|
|
170
|
+
its browser recovery entries for each selected plan.
|
|
171
|
+
|
|
172
|
+
## Compensating undo
|
|
173
|
+
|
|
174
|
+
`undo(context,{path,operationId,undoOf,expectedRevision})` creates a new receipt
|
|
175
|
+
and save operation referencing a retained committed operation. The requester
|
|
176
|
+
must own the original receipt and have current undo authority. The expected
|
|
177
|
+
revision must equal both the current file and the original result. The new
|
|
178
|
+
source reverses that operation's changes; its own exact before/after images and
|
|
179
|
+
actor are retained. If anything changed subsequently, undo reports a conflict
|
|
180
|
+
and requires explicit review. It does not merge over later edits or replay
|
|
181
|
+
claims, hooks, reference repairs, migrations, or Git operations. Undoing the
|
|
182
|
+
compensating operation is a redo with the same revision protections.
|
|
183
|
+
|
|
184
|
+
The first application exposes the reviewed diff, Save, preserved draft,
|
|
185
|
+
conflict review, and Undo through these operations. Git commit, sharing across
|
|
186
|
+
clones, lifecycle actions, and record migrations remain distinct operations.
|
|
187
|
+
|
|
188
|
+
## Local document adapter
|
|
189
|
+
|
|
190
|
+
`runlist app [document] [--port <port>]` selects the configured checkout at startup
|
|
191
|
+
and binds only to `127.0.0.1`. A random access capability in the printed URL
|
|
192
|
+
fragment establishes a separate human browser session. The fragment is removed
|
|
193
|
+
after connection. Sessions use a port-scoped HttpOnly/SameSite=Strict cookie,
|
|
194
|
+
expire after twelve hours, and can be revoked with Disconnect. The adapter uses
|
|
195
|
+
the local OS user identity, not an agent session inherited from the environment.
|
|
196
|
+
This is a local capability, not a remote multi-user login service.
|
|
197
|
+
|
|
198
|
+
API mutations require the authenticated cookie, exact allowed local Origin,
|
|
199
|
+
and the session's random CSRF token. Host checks reject DNS-rebinding hosts;
|
|
200
|
+
cross-origin API requests are refused. Static content has a restrictive CSP
|
|
201
|
+
and no external scripts/styles. Markdown is escaped and unsafe link schemes
|
|
202
|
+
are refused. No browser request can select a new repository/config, inject an
|
|
203
|
+
actor/policy, release a claim, or start a lifecycle/domain operation. The adapter
|
|
204
|
+
homes configured hubs, plans and documents in a paged library. A hub is the
|
|
205
|
+
organizing document older guidance calls a runlist; its ordered child plans
|
|
206
|
+
live on that same document. Coordination, ordered and roadmap hubs are shapes
|
|
207
|
+
of this concept, not separate required container levels. Only
|
|
208
|
+
unambiguous configured narratives or valid native records are editable; untyped
|
|
209
|
+
and unsupported sources remain read only. Private prompts, excluded corpora and
|
|
210
|
+
generated indexes stay outside this adapter. Domain record views retain separate
|
|
211
|
+
action scopes.
|
|
212
|
+
|
|
213
|
+
The browser persists recovery data before Save/Undo requests, retries the same
|
|
214
|
+
payload/operation kind after an uncertain acknowledgement, and exposes retained
|
|
215
|
+
before/after/current evidence for explicit leave-current settlement. It does not
|
|
216
|
+
discard an interrupted operation merely because a request failed. Concurrent
|
|
217
|
+
source conflicts preserve the draft and require explicit merged-draft review
|
|
218
|
+
against the current revision. Saves/undo freeze the workspace while in flight
|
|
219
|
+
so a response cannot silently replace typing entered during the request.
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
The default document view edits supported Markdown blocks inline. It retains
|
|
223
|
+
raw source for untouched blocks, comments, and gaps; line-diff publication
|
|
224
|
+
preserves their original line endings. Enter and slash/plus menus create
|
|
225
|
+
blocks, task controls change existing checkboxes, and selected text supports
|
|
226
|
+
basic formatting. Code edits as plain text. Tables, nested lists, unclosed
|
|
227
|
+
fences, and unsupported block syntax have an explicit Markdown fallback.
|
|
228
|
+
Managed lifecycle history is read only in the block view. New/removal/reordering
|
|
229
|
+
of native anchors still belongs to domain operations; the core remains the
|
|
230
|
+
final validator. Draft undo/redo is distinct from compensating Undo save.
|
|
231
|
+
Inline changes autosave only the private draft; reviewed Save publishes source.
|
|
232
|
+
Pasted/dropped content is inserted as text, never authored HTML.
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
// runlist's OpenCode integration. Installed by `runlist install opencode`, which
|
|
2
|
+
// copies this file (with a version banner prepended) to the OpenCode plugin
|
|
3
|
+
// directory, where OpenCode auto-discovers it — it globs
|
|
4
|
+
// `{plugin,plugins}/*.{ts,js}` under `.opencode/` and the global config dir, so
|
|
5
|
+
// no `opencode.json` edit is needed.
|
|
6
|
+
//
|
|
7
|
+
// TWO CONSTRAINTS, both load-bearing:
|
|
8
|
+
//
|
|
9
|
+
// 1. EXACTLY ONE EXPORT. OpenCode treats every export of a plugin module as a
|
|
10
|
+
// plugin factory and throws `Plugin export is not a function` on anything
|
|
11
|
+
// that isn't one — so an exported helper wouldn't just be untidy, it would
|
|
12
|
+
// be *called* as a second plugin. Helpers stay module-local.
|
|
13
|
+
//
|
|
14
|
+
// 2. Only a deliberate guard denial may throw. OpenCode awaits hooks inside
|
|
15
|
+
// the chat request, so every other failure degrades to a no-op.
|
|
16
|
+
//
|
|
17
|
+
// Runs under Bun inside the OpenCode process. Node builtins only, no deps.
|
|
18
|
+
|
|
19
|
+
import { execFile } from 'node:child_process';
|
|
20
|
+
|
|
21
|
+
// The primer is a nicety; identity is a correctness invariant. They are split
|
|
22
|
+
// across two hooks deliberately: `shell.env` is stable API, while
|
|
23
|
+
// `experimental.chat.system.transform` may be renamed by OpenCode. If it is,
|
|
24
|
+
// priming stops and ownership keeps working — the failure lands on the half
|
|
25
|
+
// that can afford it.
|
|
26
|
+
const PRIMER_TTL_MS = 60_000;
|
|
27
|
+
const PRIMER_TIMEOUT_MS = 5_000;
|
|
28
|
+
|
|
29
|
+
function cliExecutables() {
|
|
30
|
+
return process.platform === 'win32' ? ['runlist.cmd', 'dotmd.cmd'] : ['runlist', 'dotmd'];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Runs the CLI and resolves its trimmed stdout, or '' on any failure. `input`,
|
|
34
|
+
// when given, is written to the child's stdin.
|
|
35
|
+
function runCli(directory, args, input = null) {
|
|
36
|
+
return new Promise(resolve => {
|
|
37
|
+
let settled = false;
|
|
38
|
+
const done = value => { if (!settled) { settled = true; resolve(value); } };
|
|
39
|
+
const candidates = cliExecutables();
|
|
40
|
+
const attempt = index => {
|
|
41
|
+
if (index >= candidates.length) { done(''); return; }
|
|
42
|
+
try {
|
|
43
|
+
const child = execFile(candidates[index], args, {
|
|
44
|
+
cwd: directory,
|
|
45
|
+
timeout: PRIMER_TIMEOUT_MS,
|
|
46
|
+
windowsHide: true,
|
|
47
|
+
// Node refuses to execFile a `.cmd` without a shell (EINVAL since the
|
|
48
|
+
// 2024 batch-file fix), and npm installs the CLI on Windows as one.
|
|
49
|
+
// The arguments are fixed strings, never user input.
|
|
50
|
+
shell: process.platform === 'win32',
|
|
51
|
+
env: { ...process.env, NO_COLOR: '1' },
|
|
52
|
+
}, (error, stdout) => {
|
|
53
|
+
if (error?.code === 'ENOENT') attempt(index + 1);
|
|
54
|
+
else done(error ? '' : (stdout ?? '').trim());
|
|
55
|
+
});
|
|
56
|
+
child.stdin?.on('error', () => {});
|
|
57
|
+
if (input !== null) child.stdin?.end(input);
|
|
58
|
+
else child.stdin?.end();
|
|
59
|
+
} catch { attempt(index + 1); }
|
|
60
|
+
};
|
|
61
|
+
attempt(0);
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// OpenCode runs no Claude Code hooks. Evaluate recognized prompt reads before
|
|
66
|
+
// the tool so opt-in strict mode can block them. Its V1 before hook cannot
|
|
67
|
+
// return model context, so ordinary warnings are appended to the result.
|
|
68
|
+
const PROMPT_FILE = /(^|[\\/])prompts[\\/]\S*\.md\b/;
|
|
69
|
+
|
|
70
|
+
function guardPayload(tool, args) {
|
|
71
|
+
if (tool === 'read' && typeof args?.filePath === 'string' && PROMPT_FILE.test(args.filePath)) {
|
|
72
|
+
return { tool_name: 'Read', tool_input: { file_path: args.filePath } };
|
|
73
|
+
}
|
|
74
|
+
if (tool === 'bash' && typeof args?.command === 'string' && PROMPT_FILE.test(args.command)) {
|
|
75
|
+
return { tool_name: 'Bash', tool_input: { command: args.command } };
|
|
76
|
+
}
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export default async function runlistOpencodePlugin({ directory }) {
|
|
81
|
+
// Keyed by session so a subagent session primes independently, the way
|
|
82
|
+
// SubagentStart does under Claude Code.
|
|
83
|
+
const primers = new Map();
|
|
84
|
+
const guardDecisions = new Map();
|
|
85
|
+
|
|
86
|
+
async function guardFor(tool, args) {
|
|
87
|
+
const payload = guardPayload(tool, args);
|
|
88
|
+
if (!payload) return null;
|
|
89
|
+
const raw = await runCli(directory, ['guard'], JSON.stringify(payload));
|
|
90
|
+
return raw ? JSON.parse(raw)?.hookSpecificOutput ?? null : null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function primerFor(sessionId) {
|
|
94
|
+
const key = sessionId ?? '';
|
|
95
|
+
const cached = primers.get(key);
|
|
96
|
+
// Refreshed on a TTL rather than cached for the session's life: OpenCode
|
|
97
|
+
// rebuilds the system prompt every turn, so a once-only push would reach
|
|
98
|
+
// only the first request — and a never-refreshed one would keep announcing
|
|
99
|
+
// a pending prompt this session already consumed.
|
|
100
|
+
if (cached && Date.now() - cached.at < PRIMER_TTL_MS) return cached.text;
|
|
101
|
+
const text = await runCli(directory, ['hud']);
|
|
102
|
+
primers.set(key, { text, at: Date.now() });
|
|
103
|
+
return text;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
'tool.execute.before': async (input, output) => {
|
|
108
|
+
let refusal = null;
|
|
109
|
+
try {
|
|
110
|
+
const decision = await guardFor(input?.tool, output?.args);
|
|
111
|
+
if (input?.callID && decision) guardDecisions.set(input.callID, decision);
|
|
112
|
+
if (decision?.permissionDecision === 'deny') {
|
|
113
|
+
if (input?.callID) guardDecisions.delete(input.callID);
|
|
114
|
+
refusal = decision.permissionDecisionReason;
|
|
115
|
+
}
|
|
116
|
+
} catch { /* a missing/broken guard must not fail the chat turn */ }
|
|
117
|
+
if (refusal) throw new Error(`[runlist] ${refusal}`);
|
|
118
|
+
},
|
|
119
|
+
// Ownership identity. OpenCode sets no session-id variable of its own, and
|
|
120
|
+
// `OPENCODE_PID` — what runlist falls back to without this plugin — names the
|
|
121
|
+
// OpenCode *process*, so every session in one TUI shares it and can release
|
|
122
|
+
// the others' plans. This is the only place the real session id is
|
|
123
|
+
// available to a tool shell.
|
|
124
|
+
'shell.env': async (input, output) => {
|
|
125
|
+
try {
|
|
126
|
+
if (input?.sessionID) {
|
|
127
|
+
output.env.RUNLIST_SESSION_ID = `opencode:${input.sessionID}`;
|
|
128
|
+
// Legacy name, kept for an older CLI (before 0.77.0) still on PATH.
|
|
129
|
+
output.env.DOTMD_SESSION_ID = `opencode:${input.sessionID}`;
|
|
130
|
+
}
|
|
131
|
+
// The OpenCode server process hosts the session and outlives every tool
|
|
132
|
+
// shell, so it is the process whose liveness answers "is this claim's
|
|
133
|
+
// owner still there?" — `runlist doctor --claims` probes exactly this.
|
|
134
|
+
output.env.RUNLIST_SESSION_PID = String(process.pid);
|
|
135
|
+
output.env.DOTMD_SESSION_PID = String(process.pid);
|
|
136
|
+
} catch { /* never break a shell over this */ }
|
|
137
|
+
},
|
|
138
|
+
|
|
139
|
+
// The guard's warnings, appended after the call ran. A `deny` from the
|
|
140
|
+
// guard is ignored here: the call has already happened, so there is
|
|
141
|
+
// nothing left to refuse.
|
|
142
|
+
'tool.execute.after': async (input, output) => {
|
|
143
|
+
try {
|
|
144
|
+
if (typeof output?.output !== 'string') return;
|
|
145
|
+
const decision = input?.callID ? guardDecisions.get(input.callID) : null;
|
|
146
|
+
if (input?.callID) guardDecisions.delete(input.callID);
|
|
147
|
+
const note = (decision ?? await guardFor(input?.tool, input?.args))?.additionalContext;
|
|
148
|
+
if (typeof note === 'string' && note) output.output += `\n\n${note}`;
|
|
149
|
+
} catch { /* a guard failure never touches the tool result */ }
|
|
150
|
+
},
|
|
151
|
+
|
|
152
|
+
// Session priming — the equivalent of the SessionStart hook that runs
|
|
153
|
+
// `runlist hud` under Claude Code. Silent outside a runlist repo (hud prints
|
|
154
|
+
// nothing and exits 0), so this is inert in unrelated projects.
|
|
155
|
+
'experimental.chat.system.transform': async (input, output) => {
|
|
156
|
+
try {
|
|
157
|
+
const text = await primerFor(input?.sessionID);
|
|
158
|
+
if (text) output.system.push(text);
|
|
159
|
+
} catch { /* priming is best-effort */ }
|
|
160
|
+
},
|
|
161
|
+
};
|
|
162
|
+
}
|