oh-my-llmwikimode 1.4.0 → 1.6.0
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/docs/design/EOS_CHAT_HOME_IA.md +62 -8
- package/docs/design/EOS_CHAT_OPERATING_LOOP.md +20 -0
- package/docs/design/EOS_DESIGN.md +74 -0
- package/docs/design/EOS_SIDE_REVIEW_IA.md +20 -0
- package/package.json +3 -1
- package/src/eos/approval/artifacts.js +164 -0
- package/src/eos/approval/contract.js +165 -0
- package/src/eos/approval/read-model.js +82 -0
- package/src/eos/artifact-inspector/view-model.js +240 -0
- package/src/eos/chat/static/styles.css +1 -1
- package/src/eos/home-review-queue.js +147 -0
- package/src/eos/home-workspace.js +79 -0
- package/src/eos/home.js +19 -124
- package/src/eos/http/routes/home.js +42 -0
- package/src/eos/http/routes/side-review.js +51 -0
- package/src/eos/side-review/static/styles.css +4 -4
- package/src/eos/timeline-status/read-model.js +281 -0
- package/src/eos/workspace/static/app.js +122 -1
- package/src/eos/workspace/static/index.html +99 -46
- package/src/eos/workspace/static/styles.css +177 -7
|
@@ -10,6 +10,16 @@
|
|
|
10
10
|
|
|
11
11
|
The user starts with Chat-first capture: saying what they want so Eos captures that intent as a local review-required artifact, then makes it visible in `Side Review`. Chat does not execute shell commands, providers, agents, wiki promotion, git actions, or background work by itself.
|
|
12
12
|
|
|
13
|
+
EOS Workspace uses the **Local AI Operating Workbench** shell:
|
|
14
|
+
|
|
15
|
+
- left app rail for Home, Chat, Side Review, and future evidence/status surfaces.
|
|
16
|
+
- top workspace tabs for keeping multiple work surfaces open while the user compares context.
|
|
17
|
+
- main work area for the active capture, review, or evidence surface.
|
|
18
|
+
- right inspector for artifact metadata, source paths, evidence, and prepared decision details.
|
|
19
|
+
- bottom status strip for Local Only, Review Required, Not Applied, daemon/token state, and harness readiness.
|
|
20
|
+
|
|
21
|
+
The shell is web-service first. It is not a VS Code extension app, and it does not require a VSIX runtime.
|
|
22
|
+
|
|
13
23
|
## 2. Primary promise
|
|
14
24
|
|
|
15
25
|
Use this product promise when shaping UI copy:
|
|
@@ -20,12 +30,16 @@ Tell Eos what you want to prepare. Eos captures it for local review before anyth
|
|
|
20
30
|
|
|
21
31
|
## 3. Required regions
|
|
22
32
|
|
|
23
|
-
1. **
|
|
24
|
-
2. **
|
|
25
|
-
3. **
|
|
26
|
-
4. **
|
|
27
|
-
5. **
|
|
28
|
-
6. **
|
|
33
|
+
1. **Left app rail** - persistent navigation for `EOS Workspace`, `Eos Chat`, and `Side Review`.
|
|
34
|
+
2. **Top workspace tabs** - browser-like open surfaces for Home, Chat, Side Review, and future evidence panels.
|
|
35
|
+
3. **Header rail** - `EOS Workspace`, short local workspace status, and links to `Eos Chat` and `Side Review`.
|
|
36
|
+
4. **Safety strip** - always-visible copy: `Local review workspace. Nothing is applied automatically.`
|
|
37
|
+
5. **Welcome state** - explains chat-first workflow without starter chips.
|
|
38
|
+
6. **Status cards** - wiki status, review queue count, and harness readiness in compact cards.
|
|
39
|
+
7. **Main work area** - local acknowledgements and the active task surface only; no execution transcript unless a future approved execution flow exists.
|
|
40
|
+
8. **Right inspector** - optional artifact, evidence, and prepared decision detail surface.
|
|
41
|
+
9. **Composer** - bottom input with a short placeholder and explicit send button.
|
|
42
|
+
10. **Bottom status strip** - local-only, review-required, Not Applied, and route/auth status.
|
|
29
43
|
|
|
30
44
|
## 4. Empty state copy
|
|
31
45
|
|
|
@@ -83,9 +97,13 @@ Do not use these labels in the Eos Chat MVP:
|
|
|
83
97
|
|
|
84
98
|
- `GET /chat/` serves the Eos Chat local UI.
|
|
85
99
|
- `GET /chat` redirects to `/chat/`.
|
|
86
|
-
- `GET /workspace/` serves the canonical EOS Workspace Home.
|
|
100
|
+
- `GET /workspace/` serves the canonical operating workbench and EOS Workspace Home.
|
|
87
101
|
- `GET /workspace` redirects to `/workspace/`.
|
|
102
|
+
- `GET /side-review/` serves the review desk for local artifacts, evidence, and prepare-only decision notes.
|
|
88
103
|
- `GET /api/home` remains a read-only home data snapshot.
|
|
104
|
+
- `POST /api/agent/side-review/approval-requests` prepares a review-required approval request from an existing `prepare_approval` Side Review decision artifact only.
|
|
105
|
+
- `GET /api/workspace/artifacts/inspect` returns whitelisted read-only artifact inspector data by `{type,id}` only.
|
|
106
|
+
- `GET /api/workspace/timeline-status` returns a local lifecycle timeline/status panel derived from EOS local artifacts only.
|
|
89
107
|
- `POST /api/chat/requests` creates a review-required local artifact only.
|
|
90
108
|
|
|
91
109
|
## 12. Browser-first Workspace Slice
|
|
@@ -98,7 +116,9 @@ Do not use these labels in the Eos Chat MVP:
|
|
|
98
116
|
- Review-required.
|
|
99
117
|
- No hidden execution.
|
|
100
118
|
- No source wiki mutation.
|
|
101
|
-
-
|
|
119
|
+
- Hosted sync is deferred.
|
|
120
|
+
- Production Discord control is deferred.
|
|
121
|
+
- Marketplace, account, payment, publish, push, and package split work are deferred.
|
|
102
122
|
- No auto-promotion, auto-merge, auto-delete, or auto-push.
|
|
103
123
|
- Browser UI uses self-only CSP, local assets, and text-node rendering.
|
|
104
124
|
|
|
@@ -134,3 +154,37 @@ Required dashboard groups:
|
|
|
134
154
|
- `System Health` - explains local load errors or confirms local review data is readable.
|
|
135
155
|
|
|
136
156
|
The Chat home panel may show compact `Next Safe Actions` and `Recent Requests`, but these are navigation/review aids only. They must not become starter chips, execution shortcuts, provider dispatch, or hidden agent controls.
|
|
157
|
+
|
|
158
|
+
## 13. Workspace completion snapshot - 2026-06-01
|
|
159
|
+
|
|
160
|
+
`GET /api/home` now includes an `operating_snapshot` read model for the browser workbench.
|
|
161
|
+
|
|
162
|
+
Stable fields:
|
|
163
|
+
|
|
164
|
+
- `operating_snapshot.version: 2`
|
|
165
|
+
- `operating_snapshot.shell.motif: Local AI Operating Workbench`
|
|
166
|
+
- `operating_snapshot.shell.regions`: `left_app_rail`, `top_workspace_tabs`, `main_work_area`, `right_inspector`, `bottom_status_strip`
|
|
167
|
+
- `operating_snapshot.route_roles`: `/workspace/` as canonical operating workbench, `/chat/` as compatibility inbox, `/side-review/` as review desk
|
|
168
|
+
- `operating_snapshot.artifact_lifecycle`: capture request -> local artifact -> Side Review card -> prepare-only decision artifact -> future explicit approval flow
|
|
169
|
+
- `review_queue.side_review_decision_recent`: latest prepared decision artifacts for the right inspector/Home summary
|
|
170
|
+
|
|
171
|
+
The snapshot is a read model only. It must not become an execution queue, approval queue, hosted sync contract, or source wiki mutation path.
|
|
172
|
+
|
|
173
|
+
## 14. Approval Inspector workspace slice - 2026-06-01
|
|
174
|
+
|
|
175
|
+
`EOS Workspace` now includes an Approval Queue and Artifact Inspector for review-required approval request artifacts.
|
|
176
|
+
|
|
177
|
+
Stable surfaces:
|
|
178
|
+
|
|
179
|
+
- `review_queue.approval_request_recent`: latest local approval request artifacts visible in Home/Workspace.
|
|
180
|
+
- `approval_surface`: local approval queue metadata and route hints for the browser workbench.
|
|
181
|
+
- `/api/agent/side-review/approval-requests`: prepare-only route from `prepare_approval` Side Review decisions to approval request artifacts.
|
|
182
|
+
- `/api/workspace/artifacts/inspect`: safe inspector route for whitelisted artifact types and ids.
|
|
183
|
+
- `/api/workspace/timeline-status`: local lifecycle timeline and Timeline Gate status.
|
|
184
|
+
- Workspace regions: `Approval Queue`, `Artifact Inspector`, `Lifecycle Timeline`, and `Timeline Gate`.
|
|
185
|
+
|
|
186
|
+
Safety contract:
|
|
187
|
+
|
|
188
|
+
- Approval Queue is a review queue, not an execution queue.
|
|
189
|
+
- Artifact Inspector must reject raw local paths, traversal, unsupported types, symlinks, oversized artifacts, and malformed JSON without leaking secrets or user-home paths.
|
|
190
|
+
- Timeline Gate may show `approved_not_executed`, but this is a review state only. It must not imply shell, git, provider, Discord, sync, publish, merge, push, or source wiki mutation work has happened.
|
|
@@ -10,6 +10,12 @@ EOS Workspace is the browser-first Home. Chat is the inbox. Side Review is the r
|
|
|
10
10
|
|
|
11
11
|
The user writes a request through Chat-first capture on `/workspace/` or the compatibility `/chat/` surface. Eos files it as a local review item, adds a deterministic intent hint, and places it on the `Side Review` desk. Nothing is executed or applied just because the request was captured.
|
|
12
12
|
|
|
13
|
+
The UI motif is a Local AI Operating Workbench:
|
|
14
|
+
|
|
15
|
+
- `/workspace/` is the canonical operating workbench with the left app rail, top workspace tabs, main work area, right inspector, and bottom status strip.
|
|
16
|
+
- `/chat/` is the compatibility inbox for focused capture.
|
|
17
|
+
- `/side-review/` is the review desk for review-required artifacts, evidence, prepared decisions, and completion reports.
|
|
18
|
+
|
|
13
19
|
## 2. Operating loop
|
|
14
20
|
|
|
15
21
|
```text
|
|
@@ -106,3 +112,17 @@ This slice must not add:
|
|
|
106
112
|
## 9. Workspace Home coordination
|
|
107
113
|
|
|
108
114
|
`/workspace/` is the canonical browser Home for this slice. It exposes `EOS Workspace`, `Chat-first capture`, `Capture for Review`, `Open Chat`, and `Open Side Review` while keeping every output local-only, review-required, and Not Applied.
|
|
115
|
+
|
|
116
|
+
The review-required artifact lifecycle remains capture request -> local artifact -> Side Review card -> prepare-only decision artifact -> future explicit approval flow. Hosted sync and production Discord control are deferred; marketplace, package split, publish, push, hidden execution, and source wiki mutation stay out of scope.
|
|
117
|
+
|
|
118
|
+
## 10. Completion proof - 2026-06-01
|
|
119
|
+
|
|
120
|
+
The operating loop is now verified as a browser-first product slice:
|
|
121
|
+
|
|
122
|
+
- A request can be captured through the local HTTP/browser flow and surfaced as Home queue pressure.
|
|
123
|
+
- The captured request appears in Side Review with evidence and safe prepare-only actions.
|
|
124
|
+
- Preparing a decision writes `.system/eos/side-review/decisions/{id}.json` and Home shows it as a recent prepared decision.
|
|
125
|
+
- Source markdown checksums remain unchanged; only `.system/eos` review artifacts are written.
|
|
126
|
+
- Chrome desktop/mobile evidence confirms the workspace shell and route-specific surfaces render as nonblank local pages.
|
|
127
|
+
|
|
128
|
+
Completion evidence lives under `.omo/ulw-loop/evidence/eos-workspace-completion-20260601/`.
|
|
@@ -22,6 +22,22 @@ Eos is **not** a cloud assistant surface, marketing landing page, autonomous exe
|
|
|
22
22
|
|
|
23
23
|
The interface should feel like a trustworthy local cockpit where conversation opens separate work panels, not a generic chatbot SaaS clone or button-heavy dashboard.
|
|
24
24
|
|
|
25
|
+
**Local AI Operating Workbench** is the browser-service shell motif for EOS Workspace. It borrows the familiar shape of operating tools without turning EOS into a VS Code extension:
|
|
26
|
+
|
|
27
|
+
- left app rail: persistent navigation for Workspace Home, Chat, Side Review, future evidence views, and status surfaces.
|
|
28
|
+
- top workspace tabs: multiple open work surfaces can be kept visible like browser or IDE tabs.
|
|
29
|
+
- main work area: the active surface owns the center of the screen and carries the primary task.
|
|
30
|
+
- right inspector: optional evidence, artifact metadata, source path, decision detail, and review context.
|
|
31
|
+
- bottom status strip: local-only state, review-required mode, Not Applied state, harness readiness, and route/auth status.
|
|
32
|
+
|
|
33
|
+
Route roles stay explicit:
|
|
34
|
+
|
|
35
|
+
- `/workspace/` is the canonical operating workbench and default browser Home.
|
|
36
|
+
- `/chat/` is the compatibility inbox and focused capture surface.
|
|
37
|
+
- `/side-review/` is the review desk for artifacts, evidence, prepared decisions, and completion reports.
|
|
38
|
+
|
|
39
|
+
The review-required artifact lifecycle is: capture request -> local artifact -> Side Review card -> prepare-only decision artifact -> future explicit approval flow. Hosted sync, production Discord control, marketplace work, package split, publish, push, real execution, and automatic mutation are deferred until separate specs authorize them.
|
|
40
|
+
|
|
25
41
|
Compatibility note: the VS Code extension design gate still tracks the earlier `Dawn Control Room` metaphor and the Korean chat prompt `무엇을 맡길까요?`. Treat both as compatible markers for the same calm, chat-first local command center direction, not as permission to add a dashboard-first or execution-first surface.
|
|
26
42
|
|
|
27
43
|
## Primary Surfaces for This Slice
|
|
@@ -298,3 +314,61 @@ Verification anchors:
|
|
|
298
314
|
- `test/eos/home-data.test.js`
|
|
299
315
|
- `test/eos/side-review-routes.test.js`
|
|
300
316
|
- `test/eos/workspace-boundary-side-review-view-model.test.js`
|
|
317
|
+
|
|
318
|
+
## Implemented EOS Workspace Completion Slice - 2026-06-01
|
|
319
|
+
|
|
320
|
+
The EOS Workspace completion slice turns `/workspace/` into the canonical browser-first operating workbench.
|
|
321
|
+
|
|
322
|
+
What changed:
|
|
323
|
+
|
|
324
|
+
- `GET /api/home` now exposes operating snapshot v2 with the `Local AI Operating Workbench` motif, route roles, artifact lifecycle, recent chat requests, and recent prepared Side Review decisions.
|
|
325
|
+
- `/workspace/` now renders the shell regions directly: left app rail, top workspace tabs, main work area, right inspector, and bottom status strip.
|
|
326
|
+
- Chat capture remains available from `/workspace/` and `/chat/`; both routes create review-required local artifacts only.
|
|
327
|
+
- Prepared Side Review decisions are visible from Home/Workspace as local review-required artifacts.
|
|
328
|
+
- Desktop and mobile Chrome evidence is captured under `.omo/ulw-loop/evidence/eos-workspace-completion-20260601/`.
|
|
329
|
+
|
|
330
|
+
Boundary:
|
|
331
|
+
|
|
332
|
+
- The slice remains web-service first, not a VS Code extension.
|
|
333
|
+
- It does not add hosted sync, production Discord control, marketplace/account/payment features, package splitting, npm publish, auto-apply, auto-promotion, auto-merge/delete, auto-push, hidden execution, or source wiki mutation.
|
|
334
|
+
|
|
335
|
+
Verification anchors:
|
|
336
|
+
|
|
337
|
+
- `test/eos/home-data.test.js`
|
|
338
|
+
- `test/eos/workspace-home-routes.test.js`
|
|
339
|
+
- `test/eos/uiux-product-shape.test.js`
|
|
340
|
+
- `npm run test:eos`
|
|
341
|
+
- `npm run verify:eos-workspace-completion`
|
|
342
|
+
|
|
343
|
+
## Implemented EOS Workspace Approval Inspector Slice - 2026-06-01
|
|
344
|
+
|
|
345
|
+
The EOS Workspace Approval Inspector slice makes approval requests first-class local review artifacts inside the browser-first workbench.
|
|
346
|
+
|
|
347
|
+
What changed:
|
|
348
|
+
|
|
349
|
+
- Approval request artifacts now live under `.system/eos/approval/requests/` with review-required lifecycle statuses, deterministic duplicate handling, and append-only audit metadata.
|
|
350
|
+
- `GET /api/home` includes a local approval queue summary with recent approval request artifacts.
|
|
351
|
+
- `POST /api/agent/side-review/approval-requests` prepares an approval request only from an existing `prepare_approval` Side Review decision artifact.
|
|
352
|
+
- `GET /api/workspace/artifacts/inspect` exposes a whitelisted, read-only artifact inspector for approval requests and adjacent EOS artifacts.
|
|
353
|
+
- `GET /api/workspace/timeline-status` exposes a local-only lifecycle timeline/status panel derived from chat, Side Review, approval, visual QA, and local evidence artifacts.
|
|
354
|
+
- `/workspace/` renders Approval Queue, Artifact Inspector, Lifecycle Timeline, and Timeline Gate surfaces with Local Only, Review Required, and Not Applied safety labels.
|
|
355
|
+
- Browser product evidence is captured under `.omo/ulw-loop/evidence/eos-approval-inspector-20260601/`.
|
|
356
|
+
|
|
357
|
+
Boundary:
|
|
358
|
+
|
|
359
|
+
- The slice remains web-service first, not a VS Code extension or VSIX.
|
|
360
|
+
- Approval statuses such as `approved_not_executed` mean explicit review state only; they do not execute shell, git, provider, Discord, or wiki mutation actions.
|
|
361
|
+
- Side Review approval request creation is prepare-only; it creates a local review artifact and never applies the underlying decision.
|
|
362
|
+
- The inspector reads only whitelisted local artifacts by type/id, rejects raw paths and traversal, redacts secrets and user-home paths, and degrades safely on malformed artifacts.
|
|
363
|
+
- This slice does not add hosted sync, production Discord control, marketplace/account/payment features, package splitting, npm publish, auto-apply, auto-promotion, auto-merge/delete, auto-push, hidden execution, or source wiki mutation.
|
|
364
|
+
|
|
365
|
+
Verification anchors:
|
|
366
|
+
|
|
367
|
+
- `test/eos/approval-request-artifacts.test.js`
|
|
368
|
+
- `test/eos/approval-queue-read-model.test.js`
|
|
369
|
+
- `test/eos/side-review-approval-requests.test.js`
|
|
370
|
+
- `test/eos/artifact-inspector-view-model.test.js`
|
|
371
|
+
- `test/eos/workspace-approval-inspector-routes.test.js`
|
|
372
|
+
- `test/eos/timeline-status-panel.test.js`
|
|
373
|
+
- `test/eos/approval-inspector-gate.test.js`
|
|
374
|
+
- `npm run verify:eos-approval-inspector`
|
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
|
|
9
9
|
`Side Review` is the companion window for looking at structured material while `EOS Workspace` at `/workspace/` remains the browser-first Home and `Eos Chat` remains the compatibility capture surface.
|
|
10
10
|
|
|
11
|
+
In the Local AI Operating Workbench shell, `/side-review/` is the review desk. It can be opened from the left app rail or held as a top workspace tab while the main work area stays on Chat or Home. Its cards may feed the right inspector with artifact paths, evidence, and prepared decision detail, while the bottom status strip continues to show Local Only, Review Required, and Not Applied state.
|
|
12
|
+
|
|
11
13
|
It answers five questions:
|
|
12
14
|
|
|
13
15
|
1. What is waiting?
|
|
@@ -356,3 +358,21 @@ The UI and API may summarize these proof fields, but must not convert them into
|
|
|
356
358
|
### Future boundary
|
|
357
359
|
|
|
358
360
|
Real approval execution, source wiki mutation, promotion, merge, delete, push, release, npm publish, hosted sync, and Discord runtime control require a separate explicit product spec and are not part of this MVP.
|
|
361
|
+
|
|
362
|
+
## 13. Workspace completion integration - 2026-06-01
|
|
363
|
+
|
|
364
|
+
`Side Review` is now part of the EOS operating workbench loop rather than a standalone browser surface.
|
|
365
|
+
|
|
366
|
+
Integration contract:
|
|
367
|
+
|
|
368
|
+
- `/workspace/` links to Side Review as the review desk in the left app rail and top workspace tabs.
|
|
369
|
+
- Prepared decision artifacts remain in `.system/eos/side-review/decisions/` and are summarized by Home through `review_queue.side_review_decision_recent`.
|
|
370
|
+
- The Workspace right inspector may summarize recent prepared decisions, but it must not expose execution, approval, promotion, merge, delete, push, or source mutation controls.
|
|
371
|
+
- The decision artifact lifecycle remains prepare-only until a future explicit approval flow is specified and separately verified.
|
|
372
|
+
|
|
373
|
+
Verification anchors:
|
|
374
|
+
|
|
375
|
+
- `test/eos/home-data.test.js`
|
|
376
|
+
- `test/eos/side-review-routes.test.js`
|
|
377
|
+
- `test/eos/workspace-home-routes.test.js`
|
|
378
|
+
- `test/eos/uiux-product-shape.test.js`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "oh-my-llmwikimode",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.0",
|
|
4
4
|
"description": "LLM Wiki plugin for OpenCode ??auto-memory, retrieval, and knowledge promotion",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/server.js",
|
|
@@ -29,6 +29,8 @@
|
|
|
29
29
|
"test:security": "node --test test/security.test.js",
|
|
30
30
|
"test:eos": "node --test test/eos/**/*.test.js",
|
|
31
31
|
"verify:eos-workspace-product": "npm run test:eos && npm run verify:eos-boundary-policy && npm run verify:eos-boundary && npm run eos:a11y-snapshot -- --json",
|
|
32
|
+
"verify:eos-workspace-completion": "node scripts/verify-eos-workspace-completion.js",
|
|
33
|
+
"verify:eos-approval-inspector": "node scripts/verify-eos-approval-inspector.js",
|
|
32
34
|
"verify:package": "node test/verify-package.test.js",
|
|
33
35
|
"verify:docs": "node test/verify-docs.test.js",
|
|
34
36
|
"verify:side-effects": "node scripts/side-effect-audit.js",
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { appendFile, mkdir, readdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
APPROVAL_REQUEST_STATUSES,
|
|
6
|
+
createApprovalAuditEntry,
|
|
7
|
+
createApprovalRequestArtifact,
|
|
8
|
+
validateApprovalRequest,
|
|
9
|
+
} from "./contract.js";
|
|
10
|
+
|
|
11
|
+
export { APPROVAL_REQUEST_STATUSES } from "./contract.js";
|
|
12
|
+
|
|
13
|
+
const AUDIT_ACTIONS = Object.freeze([
|
|
14
|
+
"approval_requested",
|
|
15
|
+
"reject_approval_request",
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
function resolveUnderWikiRoot(wikiRoot, relativePath) {
|
|
19
|
+
const root = path.resolve(wikiRoot);
|
|
20
|
+
const target = path.resolve(root, ...relativePath.split("/"));
|
|
21
|
+
const relative = path.relative(root, target);
|
|
22
|
+
if (!relative || relative.startsWith("..") || path.isAbsolute(relative)) {
|
|
23
|
+
throw new Error("Eos approval artifact path escapes wiki root");
|
|
24
|
+
}
|
|
25
|
+
return target;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function resolveRequestArtifactPath(wikiRoot, relativePath) {
|
|
29
|
+
const target = resolveUnderWikiRoot(wikiRoot, relativePath);
|
|
30
|
+
const requestsRoot = path.resolve(getApprovalArtifactPaths(wikiRoot).requests);
|
|
31
|
+
const relative = path.relative(requestsRoot, target);
|
|
32
|
+
if (!relative || relative.startsWith("..") || path.isAbsolute(relative)) {
|
|
33
|
+
throw new Error("Eos approval artifact path escapes requests directory");
|
|
34
|
+
}
|
|
35
|
+
return target;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function getApprovalArtifactPaths(wikiRoot) {
|
|
39
|
+
const root = path.join(wikiRoot, ".system", "eos", "approval");
|
|
40
|
+
return Object.freeze({
|
|
41
|
+
root,
|
|
42
|
+
requests: path.join(root, "requests"),
|
|
43
|
+
auditFile: path.join(root, "audit.jsonl"),
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function ensureApprovalArtifactStructure(wikiRoot) {
|
|
48
|
+
const paths = getApprovalArtifactPaths(wikiRoot);
|
|
49
|
+
await mkdir(paths.requests, { recursive: true });
|
|
50
|
+
return paths;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function prepareApprovalRequestArtifact(wikiRoot, input = {}, options = {}) {
|
|
54
|
+
if (!wikiRoot || typeof wikiRoot !== "string") {
|
|
55
|
+
throw new Error("wikiRoot is required for Eos approval artifacts");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const now = options.now instanceof Date ? options.now : new Date(options.now || Date.now());
|
|
59
|
+
const validation = validateApprovalRequest(input);
|
|
60
|
+
if (!validation.valid) {
|
|
61
|
+
await appendApprovalAudit(wikiRoot, "reject_approval_request", {
|
|
62
|
+
source_decision_id: input.source_decision_id || "",
|
|
63
|
+
requested_action: input.requested_action || "",
|
|
64
|
+
reason: validation.errors.join("; "),
|
|
65
|
+
}, { ...options, now });
|
|
66
|
+
return rejected(validation.errors.join("; "));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const prepared = createApprovalRequestArtifact(validation.value, { ...options, now });
|
|
70
|
+
const resolvedArtifactPath = resolveRequestArtifactPath(wikiRoot, prepared.artifactPath);
|
|
71
|
+
const existing = await readExistingJson(resolvedArtifactPath);
|
|
72
|
+
if (existing.exists) {
|
|
73
|
+
return Object.freeze({
|
|
74
|
+
success: true,
|
|
75
|
+
duplicate: true,
|
|
76
|
+
request: existing.value,
|
|
77
|
+
artifactPath: prepared.artifactPath,
|
|
78
|
+
executed: false,
|
|
79
|
+
sourceEntryMutated: false,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
await ensureApprovalArtifactStructure(wikiRoot);
|
|
84
|
+
await writeFile(resolvedArtifactPath, `${JSON.stringify(prepared.artifact, null, 2)}\n`, {
|
|
85
|
+
encoding: "utf-8",
|
|
86
|
+
flag: "wx",
|
|
87
|
+
});
|
|
88
|
+
await appendApprovalAuditEntry(wikiRoot, prepared.auditEntry);
|
|
89
|
+
|
|
90
|
+
return Object.freeze({
|
|
91
|
+
success: true,
|
|
92
|
+
duplicate: false,
|
|
93
|
+
request: prepared.artifact,
|
|
94
|
+
artifactPath: prepared.artifactPath,
|
|
95
|
+
audit: prepared.auditEntry,
|
|
96
|
+
auditPath: ".system/eos/approval/audit.jsonl",
|
|
97
|
+
executed: false,
|
|
98
|
+
sourceEntryMutated: false,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export async function readApprovalRequestArtifacts(wikiRoot) {
|
|
103
|
+
const paths = getApprovalArtifactPaths(wikiRoot);
|
|
104
|
+
const requests = [];
|
|
105
|
+
const load_errors = [];
|
|
106
|
+
try {
|
|
107
|
+
const entries = await readdir(paths.requests, { withFileTypes: true });
|
|
108
|
+
for (const entry of entries) {
|
|
109
|
+
if (!entry.isFile() || !entry.name.endsWith(".json")) continue;
|
|
110
|
+
const filePath = path.join(paths.requests, entry.name);
|
|
111
|
+
try {
|
|
112
|
+
const artifact = JSON.parse(await readFile(filePath, "utf-8"));
|
|
113
|
+
if (artifact?.type === "eos_approval_request" && APPROVAL_REQUEST_STATUSES.includes(artifact.status)) {
|
|
114
|
+
requests.push(artifact);
|
|
115
|
+
}
|
|
116
|
+
} catch (error) {
|
|
117
|
+
load_errors.push({ path: filePath, error: error instanceof Error ? error.message : String(error) });
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
} catch (error) {
|
|
121
|
+
if (error?.code !== "ENOENT") throw error;
|
|
122
|
+
}
|
|
123
|
+
return Object.freeze({
|
|
124
|
+
paths,
|
|
125
|
+
requests: Object.freeze(requests.sort((left, right) => String(left.id).localeCompare(String(right.id)))),
|
|
126
|
+
load_errors: Object.freeze(load_errors),
|
|
127
|
+
summary: Object.freeze({ requests: requests.length, load_errors: load_errors.length }),
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function readExistingJson(filePath) {
|
|
132
|
+
try {
|
|
133
|
+
return Object.freeze({ exists: true, value: JSON.parse(await readFile(filePath, "utf-8")) });
|
|
134
|
+
} catch (error) {
|
|
135
|
+
if (error?.code === "ENOENT") return Object.freeze({ exists: false, value: null });
|
|
136
|
+
throw error;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async function appendApprovalAudit(wikiRoot, action, entry = {}, options = {}) {
|
|
141
|
+
const payload = {
|
|
142
|
+
...createApprovalAuditEntry(action, entry, options),
|
|
143
|
+
reason: entry.reason || "",
|
|
144
|
+
};
|
|
145
|
+
await appendApprovalAuditEntry(wikiRoot, payload);
|
|
146
|
+
return payload;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async function appendApprovalAuditEntry(wikiRoot, entry) {
|
|
150
|
+
if (!AUDIT_ACTIONS.includes(entry.action)) {
|
|
151
|
+
throw new Error(`Unsupported Eos approval audit action: ${entry.action}`);
|
|
152
|
+
}
|
|
153
|
+
const paths = await ensureApprovalArtifactStructure(wikiRoot);
|
|
154
|
+
await appendFile(paths.auditFile, `${JSON.stringify(entry)}\n`, "utf-8");
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function rejected(error) {
|
|
158
|
+
return Object.freeze({
|
|
159
|
+
success: false,
|
|
160
|
+
error,
|
|
161
|
+
executed: false,
|
|
162
|
+
sourceEntryMutated: false,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
export const APPROVAL_REQUEST_STATUSES = Object.freeze([
|
|
5
|
+
"review_required",
|
|
6
|
+
"blocked",
|
|
7
|
+
"approved_not_executed",
|
|
8
|
+
"handoff_prepared",
|
|
9
|
+
"rejected",
|
|
10
|
+
]);
|
|
11
|
+
|
|
12
|
+
export const APPROVAL_ACTION = "prepare_handoff";
|
|
13
|
+
|
|
14
|
+
const SUPPORTED_SOURCE_ROOTS = Object.freeze([
|
|
15
|
+
".system/eos/chat/requests/",
|
|
16
|
+
".system/eos/side-review/decisions/",
|
|
17
|
+
".system/eos/approval/requests/",
|
|
18
|
+
".system/eos/visual-qa/",
|
|
19
|
+
".omo/ulw-loop/evidence/",
|
|
20
|
+
]);
|
|
21
|
+
|
|
22
|
+
function isRecord(value) {
|
|
23
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function iso(value) {
|
|
27
|
+
return (value instanceof Date ? value : new Date(value || Date.now())).toISOString();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function stableJson(value) {
|
|
31
|
+
if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
|
|
32
|
+
if (isRecord(value)) {
|
|
33
|
+
return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableJson(value[key])}`).join(",")}}`;
|
|
34
|
+
}
|
|
35
|
+
return JSON.stringify(value);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function shortHash(value) {
|
|
39
|
+
return createHash("sha256").update(stableJson(value)).digest("hex").slice(0, 12);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function safeId(value, fallback = "approval") {
|
|
43
|
+
return String(value || fallback)
|
|
44
|
+
.toLowerCase()
|
|
45
|
+
.replace(/[^a-z0-9_-]+/g, "-")
|
|
46
|
+
.replace(/^-+|-+$/g, "")
|
|
47
|
+
.slice(0, 80) || fallback;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function relativeRequestPath(id) {
|
|
51
|
+
return path.posix.join(".system", "eos", "approval", "requests", `${id}.json`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function normalizeRelativePath(value, field, options = {}) {
|
|
55
|
+
if (typeof value !== "string" || !value.trim()) return { error: `${field} is required` };
|
|
56
|
+
const normalized = value.trim().replaceAll("\\", "/");
|
|
57
|
+
if (path.isAbsolute(normalized) || normalized.includes("://")) return { error: `${field} must be a safe relative path` };
|
|
58
|
+
const collapsed = path.posix.normalize(normalized);
|
|
59
|
+
if (collapsed.startsWith("../") || collapsed === ".." || collapsed.startsWith("/")) {
|
|
60
|
+
return { error: `${field} must stay inside the wiki root` };
|
|
61
|
+
}
|
|
62
|
+
if (options.supportedRoot && !SUPPORTED_SOURCE_ROOTS.some((root) => collapsed.startsWith(root))) {
|
|
63
|
+
return { error: `${field} must use a supported approval source root` };
|
|
64
|
+
}
|
|
65
|
+
return { value: collapsed };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function validateApprovalRequest(input = {}) {
|
|
69
|
+
const errors = [];
|
|
70
|
+
const sourceDecisionId = typeof input.source_decision_id === "string" ? input.source_decision_id.trim() : "";
|
|
71
|
+
const requestedAction = typeof input.requested_action === "string" ? input.requested_action.trim() : "";
|
|
72
|
+
const sourceArtifactPath = normalizeRelativePath(input.source_artifact_path, "source_artifact_path", { supportedRoot: true });
|
|
73
|
+
const sourcePath = input.source_path ? normalizeRelativePath(input.source_path, "source_path") : { value: null };
|
|
74
|
+
const evidence = Array.isArray(input.evidence) ? input.evidence : [];
|
|
75
|
+
|
|
76
|
+
if (!sourceDecisionId) errors.push("source_decision_id is required");
|
|
77
|
+
if (requestedAction !== APPROVAL_ACTION) errors.push(`requested_action must be ${APPROVAL_ACTION}`);
|
|
78
|
+
if (input.status && !APPROVAL_REQUEST_STATUSES.includes(input.status)) errors.push("status is not supported");
|
|
79
|
+
if (sourceArtifactPath.error) errors.push(sourceArtifactPath.error);
|
|
80
|
+
if (sourcePath.error) errors.push(sourcePath.error);
|
|
81
|
+
if (evidence.length === 0) errors.push("evidence is required");
|
|
82
|
+
|
|
83
|
+
if (errors.length > 0) return Object.freeze({ valid: false, errors: Object.freeze(errors) });
|
|
84
|
+
|
|
85
|
+
return Object.freeze({
|
|
86
|
+
valid: true,
|
|
87
|
+
value: Object.freeze({
|
|
88
|
+
source_decision_id: sourceDecisionId,
|
|
89
|
+
source_artifact_path: sourceArtifactPath.value,
|
|
90
|
+
requested_action: requestedAction,
|
|
91
|
+
reason: typeof input.reason === "string" ? input.reason.trim() : "",
|
|
92
|
+
source_path: sourcePath.value,
|
|
93
|
+
evidence: Object.freeze(evidence.map((item) => String(item).trim()).filter(Boolean)),
|
|
94
|
+
requested_by: typeof input.requested_by === "string" && input.requested_by.trim()
|
|
95
|
+
? input.requested_by.trim()
|
|
96
|
+
: "eos-approval",
|
|
97
|
+
}),
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function createApprovalRequestArtifact(request, options = {}) {
|
|
102
|
+
const seed = {
|
|
103
|
+
source_decision_id: request.source_decision_id,
|
|
104
|
+
source_artifact_path: request.source_artifact_path,
|
|
105
|
+
requested_action: request.requested_action,
|
|
106
|
+
evidence: request.evidence,
|
|
107
|
+
};
|
|
108
|
+
const id = `approval-${safeId(request.source_decision_id)}-${safeId(request.requested_action)}-${shortHash(seed)}`;
|
|
109
|
+
const artifactPath = relativeRequestPath(id);
|
|
110
|
+
const auditEntry = createApprovalAuditEntry("approval_requested", request, options, { requestId: id, artifactPath });
|
|
111
|
+
return Object.freeze({
|
|
112
|
+
artifactPath,
|
|
113
|
+
artifact: Object.freeze({
|
|
114
|
+
schema_version: 1,
|
|
115
|
+
type: "eos_approval_request",
|
|
116
|
+
id,
|
|
117
|
+
source_decision_id: request.source_decision_id,
|
|
118
|
+
source_artifact_path: request.source_artifact_path,
|
|
119
|
+
requested_action: request.requested_action,
|
|
120
|
+
reason: request.reason,
|
|
121
|
+
source_path: request.source_path,
|
|
122
|
+
evidence: request.evidence,
|
|
123
|
+
status: "review_required",
|
|
124
|
+
requested_by: request.requested_by,
|
|
125
|
+
safety_flags: Object.freeze({
|
|
126
|
+
review_required: true,
|
|
127
|
+
approved_not_executed: false,
|
|
128
|
+
executed: false,
|
|
129
|
+
source_entry_mutated: false,
|
|
130
|
+
}),
|
|
131
|
+
boundaries: Object.freeze({
|
|
132
|
+
no_hidden_execution: true,
|
|
133
|
+
no_source_mutation: true,
|
|
134
|
+
auto_apply: false,
|
|
135
|
+
auto_promote: false,
|
|
136
|
+
auto_merge: false,
|
|
137
|
+
auto_delete: false,
|
|
138
|
+
auto_push: false,
|
|
139
|
+
auto_publish: false,
|
|
140
|
+
hosted_sync: false,
|
|
141
|
+
}),
|
|
142
|
+
audit: Object.freeze([auditEntry]),
|
|
143
|
+
created_at: iso(options.now),
|
|
144
|
+
updated_at: iso(options.now),
|
|
145
|
+
artifact_path: artifactPath,
|
|
146
|
+
}),
|
|
147
|
+
auditEntry,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function createApprovalAuditEntry(action, request, options = {}, refs = {}) {
|
|
152
|
+
return Object.freeze({
|
|
153
|
+
schema_version: 1,
|
|
154
|
+
action,
|
|
155
|
+
recorded_at: iso(options.now),
|
|
156
|
+
actor: options.actor || "eos-approval",
|
|
157
|
+
request_id: refs.requestId || null,
|
|
158
|
+
artifact_path: refs.artifactPath || null,
|
|
159
|
+
source_decision_id: request.source_decision_id || "",
|
|
160
|
+
requested_action: request.requested_action || "",
|
|
161
|
+
executed: false,
|
|
162
|
+
live_execution: false,
|
|
163
|
+
source_entry_mutated: false,
|
|
164
|
+
});
|
|
165
|
+
}
|