wiki-viewer 2.16.0 → 2.16.1

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.
Files changed (124) hide show
  1. package/.next/standalone/.next/BUILD_ID +1 -1
  2. package/.next/standalone/.next/build-manifest.json +3 -3
  3. package/.next/standalone/.next/prerender-manifest.json +3 -3
  4. package/.next/standalone/.next/required-server-files.json +4 -4
  5. package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  6. package/.next/standalone/.next/server/app/_global-error.html +1 -1
  7. package/.next/standalone/.next/server/app/_global-error.rsc +1 -1
  8. package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  9. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  10. package/.next/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  11. package/.next/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  12. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  13. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  14. package/.next/standalone/.next/server/app/api/agent/activity/route.js.nft.json +1 -1
  15. package/.next/standalone/.next/server/app/api/agent/events/[...path]/route.js.nft.json +1 -1
  16. package/.next/standalone/.next/server/app/api/agent/files/[...path]/route.js.nft.json +1 -1
  17. package/.next/standalone/.next/server/app/api/agent/fs/file/[...path]/route.js.nft.json +1 -1
  18. package/.next/standalone/.next/server/app/api/agent/fs/ls/[[...path]]/route.js.nft.json +1 -1
  19. package/.next/standalone/.next/server/app/api/agent/fs/move/route.js.nft.json +1 -1
  20. package/.next/standalone/.next/server/app/api/agent/fs/search/route.js.nft.json +1 -1
  21. package/.next/standalone/.next/server/app/api/agent/settings/route.js.nft.json +1 -1
  22. package/.next/standalone/.next/server/app/api/agent/sidecar/[...path]/route.js.nft.json +1 -1
  23. package/.next/standalone/.next/server/app/api/app-proxy/[...path]/route.js.nft.json +1 -1
  24. package/.next/standalone/.next/server/app/api/assets/[...path]/route.js.nft.json +1 -1
  25. package/.next/standalone/.next/server/app/api/pdf/save/route.js.nft.json +1 -1
  26. package/.next/standalone/.next/server/app/api/share/[token]/asset/route.js.nft.json +1 -1
  27. package/.next/standalone/.next/server/app/api/share/[token]/route.js.nft.json +1 -1
  28. package/.next/standalone/.next/server/app/api/share/route.js.nft.json +1 -1
  29. package/.next/standalone/.next/server/app/api/system/browse/route.js.nft.json +1 -1
  30. package/.next/standalone/.next/server/app/api/system/reveal/route.js.nft.json +1 -1
  31. package/.next/standalone/.next/server/app/api/system/workspaces/[id]/branch/route.js.nft.json +1 -1
  32. package/.next/standalone/.next/server/app/api/system/workspaces/[id]/open/route.js.nft.json +1 -1
  33. package/.next/standalone/.next/server/app/api/system/workspaces/[id]/refresh/route.js.nft.json +1 -1
  34. package/.next/standalone/.next/server/app/api/system/workspaces/[id]/route.js.nft.json +1 -1
  35. package/.next/standalone/.next/server/app/api/system/workspaces/route.js.nft.json +1 -1
  36. package/.next/standalone/.next/server/app/api/upload/[...path]/route.js.nft.json +1 -1
  37. package/.next/standalone/.next/server/app/api/wiki/app/route.js.nft.json +1 -1
  38. package/.next/standalone/.next/server/app/api/wiki/backlinks/route.js.nft.json +1 -1
  39. package/.next/standalone/.next/server/app/api/wiki/content/route.js.nft.json +1 -1
  40. package/.next/standalone/.next/server/app/api/wiki/download/route.js.nft.json +1 -1
  41. package/.next/standalone/.next/server/app/api/wiki/folder/route.js.nft.json +1 -1
  42. package/.next/standalone/.next/server/app/api/wiki/git-branches/route.js.nft.json +1 -1
  43. package/.next/standalone/.next/server/app/api/wiki/git-checkout/route.js.nft.json +1 -1
  44. package/.next/standalone/.next/server/app/api/wiki/git-diff/route.js.nft.json +1 -1
  45. package/.next/standalone/.next/server/app/api/wiki/git-file-info/route.js.nft.json +1 -1
  46. package/.next/standalone/.next/server/app/api/wiki/git-history/route.js.nft.json +1 -1
  47. package/.next/standalone/.next/server/app/api/wiki/git-pull/route.js.nft.json +1 -1
  48. package/.next/standalone/.next/server/app/api/wiki/move/route.js.nft.json +1 -1
  49. package/.next/standalone/.next/server/app/api/wiki/new-file/route.js.nft.json +1 -1
  50. package/.next/standalone/.next/server/app/api/wiki/outlinks/route.js.nft.json +1 -1
  51. package/.next/standalone/.next/server/app/api/wiki/page/route.js.nft.json +1 -1
  52. package/.next/standalone/.next/server/app/api/wiki/presence/route.js.nft.json +1 -1
  53. package/.next/standalone/.next/server/app/api/wiki/route.js.nft.json +1 -1
  54. package/.next/standalone/.next/server/app/api/wiki/scratch/route.js.nft.json +1 -1
  55. package/.next/standalone/.next/server/app/api/wiki/search/route.js.nft.json +1 -1
  56. package/.next/standalone/.next/server/app/api/wiki/slugs/route.js.nft.json +1 -1
  57. package/.next/standalone/.next/server/app/api/wiki/upload/route.js.nft.json +1 -1
  58. package/.next/standalone/.next/server/app/api/wiki/watch/route.js.nft.json +1 -1
  59. package/.next/standalone/.next/server/app/page/react-loadable-manifest.json +4 -5
  60. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  61. package/.next/standalone/.next/server/app/s/[token]/page_client-reference-manifest.js +1 -1
  62. package/.next/standalone/.next/server/app/signin/page_client-reference-manifest.js +1 -1
  63. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0ltsov-._.js +1 -1
  64. package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_system_workspaces_[id]_open_route_actions_1087xu7.js +1 -1
  65. package/.next/standalone/.next/server/chunks/ssr/0.u4_next_0kf.7hj._.js +1 -1
  66. package/.next/standalone/.next/server/chunks/ssr/0.u4_next_dist_0xugq-k._.js +1 -1
  67. package/.next/standalone/.next/server/chunks/ssr/12y~_mermaid_dist_chunks_mermaid_core_chunk-5ZQYHXKU_mjs_0z1.978._.js +1 -1
  68. package/.next/standalone/.next/server/chunks/ssr/12y~_mermaid_dist_chunks_mermaid_core_chunk-KSCS5N6A_mjs_0v4oeem._.js +1 -1
  69. package/.next/standalone/.next/server/chunks/ssr/_01eqklo._.js +1 -1
  70. package/.next/standalone/.next/server/chunks/ssr/_0858xdh._.js +1 -1
  71. package/.next/standalone/.next/server/chunks/ssr/_0k6g8yo._.js +3 -3
  72. package/.next/standalone/.next/server/chunks/ssr/_0sr4wj.._.js +1 -1
  73. package/.next/standalone/.next/server/chunks/ssr/node_modules__pnpm_06613~i._.js +1 -1
  74. package/.next/standalone/.next/server/chunks/ssr/node_modules__pnpm_0o~d81.._.js +1 -1
  75. package/.next/standalone/.next/server/chunks/ssr/node_modules__pnpm_0szp2v0._.js +1 -1
  76. package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
  77. package/.next/standalone/.next/server/pages/500.html +1 -1
  78. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  79. package/.next/standalone/.next/server/server-reference-manifest.json +1 -1
  80. package/.next/standalone/.next/static/chunks/0.5ywu7_j899v.js +12 -0
  81. package/.next/standalone/.next/static/chunks/{0tjcc1wg-57l1.js → 065s1_n.1k11t.js} +1 -1
  82. package/.next/standalone/.next/static/chunks/{0y~5s_skz1l9a.js → 0ccw-uc_3ao.w.js} +1 -1
  83. package/.next/standalone/.next/static/chunks/{0ii.akeq7k4pf.js → 0f8kfd9d6kq0b.js} +1 -1
  84. package/.next/standalone/.next/static/chunks/0lsw6lsaj7lby.js +1 -0
  85. package/.next/standalone/.next/static/chunks/0rpxacw15~o7k.js +12 -0
  86. package/.next/standalone/.next/static/chunks/{0r7d-1tq61fa1.js → 0s2h7kvkrtq~p.js} +1 -1
  87. package/.next/standalone/.next/static/chunks/{0enm-3xy21jcx.js → 0tzi1~wpigp2y.js} +2 -2
  88. package/.next/standalone/.next/static/chunks/117dkzyg4c~us.js +1 -0
  89. package/.next/standalone/.next/static/chunks/{0j7kp7w2oum68.js → 11pa_9ucyrzh9.js} +1 -1
  90. package/.next/standalone/.scratch/tweak-running-app/craft-checklist.md +52 -0
  91. package/.next/standalone/.scratch/tweak-running-app/issues/01-isolated-origin.md +108 -0
  92. package/.next/standalone/.scratch/tweak-running-app/issues/01-runtime-containment.md +18 -0
  93. package/.next/standalone/.scratch/tweak-running-app/issues/02-injection-transform.md +18 -0
  94. package/.next/standalone/.scratch/tweak-running-app/issues/02-tracer-bullet.md +48 -0
  95. package/.next/standalone/.scratch/tweak-running-app/issues/03-accept-carbonize-mutator.md +19 -0
  96. package/.next/standalone/.scratch/tweak-running-app/issues/03-run-robustness.md +37 -0
  97. package/.next/standalone/.scratch/tweak-running-app/issues/04-multifile-safety.md +37 -0
  98. package/.next/standalone/.scratch/tweak-running-app/issues/04-static-tracer-bullet.md +27 -0
  99. package/.next/standalone/.scratch/tweak-running-app/issues/05-batch-parity.md +43 -0
  100. package/.next/standalone/.scratch/tweak-running-app/issues/05-dynamic-node-app.md +22 -0
  101. package/.next/standalone/.scratch/tweak-running-app/issues/06-remote-transport-bridge.md +20 -0
  102. package/.next/standalone/.scratch/tweak-running-app/issues/07-retire-bespoke-web-tweak.md +17 -0
  103. package/.next/standalone/.scratch/tweak-running-app/keep-ticket.md +74 -0
  104. package/.next/standalone/.scratch/tweak-running-app/mockups.html +305 -0
  105. package/.next/standalone/.scratch/tweak-running-app/ops1-updated.md +215 -0
  106. package/.next/standalone/.scratch/tweak-running-app/ops12-continuation.md +44 -0
  107. package/.next/standalone/.scratch/tweak-running-app/ops22-context.md +84 -0
  108. package/.next/standalone/.scratch/tweak-running-app/pivot-note.md +25 -0
  109. package/.next/standalone/.scratch/tweak-running-app/qa-live-engine.ts +104 -0
  110. package/.next/standalone/.scratch/tweak-running-app/spike.md +256 -0
  111. package/.next/standalone/.scratch/tweak-running-app/suggest-current-assessment.md +39 -0
  112. package/.next/standalone/.scratch/tweak-running-app/suggest-inline-interaction-spec.md +320 -0
  113. package/.next/standalone/.scratch/tweak-running-app/tweak-interaction-spec.md +161 -0
  114. package/.next/standalone/.scratch/tweak-running-app/two-actions-interaction-spec.md +234 -0
  115. package/.next/standalone/.scratch/tweak-running-app/unified-live-spec.md +261 -0
  116. package/.next/standalone/package.json +1 -1
  117. package/.next/standalone/server.js +1 -1
  118. package/package.json +1 -1
  119. package/.next/standalone/.next/static/chunks/04jvwlttwa_sw.js +0 -12
  120. package/.next/standalone/.next/static/chunks/161kgz.8h.3iq.js +0 -1
  121. package/.next/standalone/.next/static/chunks/16kzqur8~yg6b.js +0 -12
  122. /package/.next/standalone/.next/static/{7EbGXTo8ObPhr7UobKARQ → zwUX5EOT_44n1RoB_W9Bo}/_buildManifest.js +0 -0
  123. /package/.next/standalone/.next/static/{7EbGXTo8ObPhr7UobKARQ → zwUX5EOT_44n1RoB_W9Bo}/_clientMiddlewareManifest.js +0 -0
  124. /package/.next/standalone/.next/static/{7EbGXTo8ObPhr7UobKARQ → zwUX5EOT_44n1RoB_W9Bo}/_ssgManifest.js +0 -0
@@ -0,0 +1,84 @@
1
+ # OPS-22 re-home context (ground truth, main @ 23a5129)
2
+
3
+ Goal: rebuild the (removed) "Tweak" interaction on top of the EXISTING
4
+ comments/suggestions sidecar ("proof") engine, so there is one collaboration
5
+ engine underneath. Tweak = "point at a target, say what you want, the agent
6
+ rewrites it." No live engine, no polling, no variants-in-iframe.
7
+
8
+ ## What the proof engine already gives us (do not rebuild)
9
+
10
+ Store: `.proof/<path>.json` sidecar per file, opaque JSON, event log, revisioned.
11
+ Write API: `POST /api/agent/files/[...path]` `{baseRevision, by, ops[]}`, bearer
12
+ + Idempotency-Key, `STALE_REVISION` 409 + fresh snapshot on drift.
13
+
14
+ Comment kinds (src/lib/proof/types.ts):
15
+ - `comment` (legacy): discussion/annotation. Anchor = md `ref` (block) OR
16
+ `lineAnchor{lineStart,lineEnd,textHash}` for text/code files.
17
+ - `instruction` (`kind:"instruction"`): ALREADY EXISTS. Fields
18
+ `instructionState: draft|queued|sent|answered`, `runId?`, `fromCommentId?`.
19
+ Created today only via "Turn into an instruction" (comment-thread.tsx). It has
20
+ NO client dispatcher and the queued/sent/answered lifecycle is dormant
21
+ (driven only in tests). This is the durable home for a tweak.
22
+
23
+ Suggestions (`suggestion.add`, kinds replace/insertAfter/insertBefore/delete,
24
+ status pending|accepted|rejected): reviewed in `suggestion-card.tsx`; Accept
25
+ pushes the equivalent block op into the same batch and writes the doc, gated on
26
+ base fingerprint. THIS IS MARKDOWN-ONLY (route + applier both gate non-md to
27
+ comment ops only).
28
+
29
+ Surface capability asymmetry (hard constraint):
30
+ - Markdown (.md): full — block-ref comments, instruction comments, suggestions,
31
+ accept-writes-doc. Reviewed inline via suggestion-card + comment-pip.
32
+ - Text/code/HTML/any other file (source viewer): line-anchored COMMENTS ONLY.
33
+ No suggestions, no edits, no instruction dispatcher UI, no in-place accept.
34
+ (source-viewer.tsx already builds the selection→lineAnchor detection UI.)
35
+
36
+ ## Current entry points (post-removal)
37
+
38
+ - Markdown editor bubble menu: **Comment** (discuss/annotate) + **Suggest**
39
+ (propose human edit for review). Tweak was removed.
40
+ - Source viewer (text/code): select text → **Comment** pip only.
41
+ - HTML files render as a sandboxed preview iframe (website-viewer.tsx); no
42
+ collaboration affordance on it at all right now.
43
+
44
+ ## Pre-removal Tweak UX we are preserving (recovered from tag pre-tweak-removal)
45
+
46
+ Shared chrome:
47
+ - Bottom-center fixed **queue bar**: "{n} tweak(s) ready" · Cancel (clears all) ·
48
+ [Copy as prompt | Show] extras · dispatch button (label surface-specific).
49
+ - **Targeting panel** anchored to the target: header "Target", textarea
50
+ placeholder "What should change?", Cmd/Enter = add, Cancel, Add tweak.
51
+ Outside-click on empty input = cancel.
52
+ - **Dedup in place**: re-picking the same target updates its instruction/snippet,
53
+ count does not grow.
54
+ - **Copy as prompt** (works with no agent): emits
55
+ `Edit the file \`X\` (a Markdown document). Apply these changes:\n\n1. \`<snippet>\`: <instruction>` (html variant lists `Element \`selector\` (<tag>): ...`).
56
+ Clipboard gate + "Show" textarea fallback for non-secure contexts.
57
+ - **Connect an agent**: when no agent attached, dispatch label becomes "Connect
58
+ an agent" and opens the AI panel; queue preserved.
59
+
60
+ Markdown surface: click rendered block → targeting panel → queue → "Rewrite".
61
+ HTML surface: element picker in iframe → note editor anchored to element → queue
62
+ → "Apply" (confirm modal) → variants. (The whole speculative-preview/variant/
63
+ Accept-writes machinery is the part being deleted.)
64
+
65
+ ## HARD FRAMING (user correction, authoritative)
66
+
67
+ There is NO standalone "Tweak" action and none will be created. There stay
68
+ exactly TWO editor actions: **Comment** and **Suggest**. The pleasant tweak UX
69
+ is PORTED INTO those two, not added beside them. Concretely, the intended split
70
+ (confirm/refine): Comment absorbs "tell the agent what to change" via the
71
+ existing `instruction` comment kind + the targeting affordance ("What should
72
+ change?") + copy-as-prompt + connect-an-agent; the agent's rewrite answer lands
73
+ as a **Suggestion** reviewed in the existing suggestion-card. Reject any design
74
+ that adds a third button.
75
+
76
+ ## The open decision to resolve
77
+
78
+ Default dispatch action for a tweak:
79
+ - (a) instruction-comment default: dispatch persists each queued item as
80
+ `comment.add {kind:"instruction"}` on the target block/line; the user's agent
81
+ picks it up through its normal snapshot/activity flow and answers with a
82
+ suggestion (md) reviewed in suggestion-card. Copy-as-prompt = escape hatch.
83
+ - (b) copy-as-prompt only: no durable artifact; user pastes into their agent.
84
+ PM leaned (b) for simplicity; implementer leaned (a) durable with (b) as hatch.
@@ -0,0 +1,25 @@
1
+ ## Direction change: unify on the Impeccable live model (supersedes the node-app-scoped plan)
2
+
3
+ After reviewing what the Impeccable `live` skill actually does (inject an overlay
4
+ into the app's own running page; hot-swap agent-written source variants via the
5
+ dev server's HMR; carbonize the chosen variant into permanent source on accept;
6
+ 2–5 variants + tunable per-variant knobs), we are replacing the embed-in-iframe
7
+ node-app approach with a single unified mechanism.
8
+
9
+ Decisions:
10
+ - Scope = web/visual surfaces only: static HTML, dynamic HTML, and node apps.
11
+ Markdown prose stays on its current variants-in-place flow (no HMR / rendered
12
+ UI to hot-swap).
13
+ - Engine = reuse the existing Impeccable live engine (helper server + injection
14
+ adapters + HMR hot-swap + carbonize) rather than reimplement it or drop the
15
+ in-product experience. Requires a feasibility spike: confirm the engine can be
16
+ driven by the wiki-viewer product, not only by a chat agent.
17
+
18
+ Consequence:
19
+ - This spec (OPS-1) and its tickets (OPS-2 isolated origin, OPS-3 tracer bullet,
20
+ OPS-4 robustness, OPS-5 multi-file safety, OPS-6 batch parity) are SUPERSEDED.
21
+ The isolated-origin / grant / same-site-hardening architecture is no longer
22
+ needed because the Impeccable model injects into the app's own page (no parent
23
+ to protect) instead of embedding it in the wiki editor's iframe.
24
+ - Next steps: feasibility spike on driving the Impeccable engine from the product,
25
+ then a fresh unified spec (to-spec) and tickets (to-tickets).
@@ -0,0 +1,104 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { LiveEngineClient } from "/home/sil/wiki-viewer/src/lib/live-engine/client";
5
+ import { startEngine, stopEngine } from "/home/sil/wiki-viewer/src/lib/live-engine/supervisor";
6
+
7
+ async function main() {
8
+
9
+ const results: string[] = [];
10
+ function check(name: string, cond: boolean, extra = ""): void {
11
+ results.push(`${cond ? "PASS" : "FAIL"} ${name}${extra ? " :: " + extra : ""}`);
12
+ }
13
+
14
+ const appRoot = fs.mkdtempSync(path.join(os.tmpdir(), "qa-live-"));
15
+ fs.writeFileSync(
16
+ path.join(appRoot, "index.html"),
17
+ '<!doctype html><html><head><title>qa</title></head><body><button id="b">Hi</button></body></html>',
18
+ );
19
+
20
+ let failed = false;
21
+ try {
22
+ const info = await startEngine({ workspaceId: "qa", relPath: "index.html", appRoot });
23
+ check("engine boots via product supervisor + running", info.state === "running", `port=${info.port} tokenLen=${info.token.length}`);
24
+
25
+ const client = new LiveEngineClient(info);
26
+ const h = await client.health();
27
+ check("health -> ok", h.status === "ok");
28
+
29
+ const base = `http://127.0.0.1:${info.port}`;
30
+
31
+ const noTok = await fetch(`${base}/live.js`);
32
+ await noTok.text();
33
+ check("live.js without token -> 401", noTok.status === 401, `status=${noTok.status}`);
34
+
35
+ const js = await fetch(`${base}/live.js?token=${info.token}`);
36
+ const jsBody = await js.text();
37
+ check("live.js with token -> 200 real bundle", js.status === 200 && jsBody.length > 1000, `status=${js.status} len=${jsBody.length}`);
38
+ check("live.js self-embeds the token", jsBody.includes(info.token));
39
+ check("bundle references __IMPECCABLE_ globals we inject", /__IMPECCABLE_(PORT|TOKEN|VOCAB)__/.test(jsBody), (jsBody.match(/__IMPECCABLE_[A-Z_]+__/g) || []).slice(0, 6).join(","));
40
+
41
+ // Opaque-origin (sandboxed srcDoc iframe = Origin: null) CORS preflight
42
+ const opt = await fetch(`${base}/events?token=${info.token}`, { method: "OPTIONS", headers: { Origin: "null" } });
43
+ await opt.text();
44
+ check("OPTIONS opaque-origin preflight -> 204", opt.status === 204, `status=${opt.status}`);
45
+ check("CORS reflects opaque origin (null)", opt.headers.get("access-control-allow-origin") === "null", `acao=${opt.headers.get("access-control-allow-origin")}`);
46
+
47
+ // Opaque-origin with WRONG token gets no CORS grant
48
+ const optBad = await fetch(`${base}/events?token=nope`, { method: "OPTIONS", headers: { Origin: "null" } });
49
+ await optBad.text();
50
+ check("opaque origin + bad token -> no CORS grant", optBad.headers.get("access-control-allow-origin") == null, `acao=${optBad.headers.get("access-control-allow-origin")}`);
51
+
52
+ // Browser uplink: POST /events generate (session-creating), Origin: null
53
+ const genId = "a1b2c3d4";
54
+ const gen = await fetch(`${base}/events`, {
55
+ method: "POST",
56
+ headers: { "Content-Type": "application/json", Origin: "null" },
57
+ body: JSON.stringify({
58
+ token: info.token,
59
+ type: "generate",
60
+ id: genId,
61
+ action: "bolder",
62
+ count: 3,
63
+ pageUrl: "/index.html",
64
+ element: { outerHTML: '<button id="b">Hi</button>', tagName: "BUTTON" },
65
+ }),
66
+ });
67
+ const genBody = await gen.text();
68
+ check("POST /events generate (opaque origin, token) accepted", gen.ok, `status=${gen.status} body=${genBody.slice(0, 140)}`);
69
+
70
+ // POST /events with bad token rejected
71
+ const genBad = await fetch(`${base}/events`, {
72
+ method: "POST",
73
+ headers: { "Content-Type": "application/json" },
74
+ body: JSON.stringify({ token: "nope", type: "generate", id: "b2c3d4e5", action: "bolder", count: 3, pageUrl: "/index.html", element: { outerHTML: "<x/>" } }),
75
+ });
76
+ await genBad.text();
77
+ check("POST /events bad token -> 401", genBad.status === 401, `status=${genBad.status}`);
78
+
79
+ // Product-side poll receives the browser's generate
80
+ const poll = await client.poll({ timeoutMs: 10000, leaseMs: 30000 });
81
+ check("product /poll receives the generate event", poll?.type === "generate" && poll?.id === genId, `type=${String(poll?.type)} id=${String((poll as { id?: string })?.id)}`);
82
+
83
+ await stopEngine({ workspaceId: "qa", relPath: "index.html" });
84
+ let unreachable = false;
85
+ try {
86
+ await new LiveEngineClient(info).health();
87
+ } catch {
88
+ unreachable = true;
89
+ }
90
+ check("stopEngine tears down helper (health unreachable)", unreachable);
91
+ } catch (err) {
92
+ failed = true;
93
+ results.push(`FAIL harness threw :: ${(err as Error).message}`);
94
+ } finally {
95
+ try {
96
+ await stopEngine({ workspaceId: "qa", relPath: "index.html" });
97
+ } catch {}
98
+ fs.rmSync(appRoot, { recursive: true, force: true });
99
+ }
100
+
101
+ console.log(results.join("\n"));
102
+ if (failed || results.some((r) => r.startsWith("FAIL"))) process.exitCode = 1;
103
+ }
104
+ main();
@@ -0,0 +1,256 @@
1
+ # B1 feasibility spike: driving the Impeccable live engine from wiki-viewer
2
+
3
+ **Date:** 2026-08-18
4
+ **Question:** Can the wiki-viewer *product* (a server, not a chat agent) drive the
5
+ Impeccable `live` engine end to end (helper server, `live.js` injection, HMR
6
+ hot-swap, carbonize-on-accept), and how does the engine's poll loop reconcile with
7
+ wiki-viewer's existing attached-agent channel?
8
+
9
+ **Verdict: feasible, with a defined bridge.** The engine has no hard dependency on
10
+ a chat-agent CLI. Its contract is plain HTTP plus file-based state, so a product
11
+ server can drive it. Two integration points need real work and carry the risk:
12
+ (1) the browser-to-helper transport when wiki-viewer is served remotely, and
13
+ (2) reconciling two source-write/lock systems on the same files. One item needs a
14
+ quick confirmation before spec: WebSocket-upgrade passthrough for HMR in the
15
+ app-proxy. Everything else maps cleanly.
16
+
17
+ This memo is concrete enough to write a spec from.
18
+
19
+ ---
20
+
21
+ ## What the engine actually is (verified)
22
+
23
+ Not a monolith and not chat-agent-bound. Three cooperating pieces:
24
+
25
+ 1. **A long-lived helper HTTP server** (`live-server.mjs`), bound to `127.0.0.1`,
26
+ auto-port from `8400`, token-gated. It writes `.impeccable/live/server.json`
27
+ (`{pid, port, token}`). Endpoints that matter:
28
+ - `GET /live.js?token=` — the assembled browser overlay bundle.
29
+ - `GET /events?token=` — **SSE downlink**, server to browser (variant mounts,
30
+ `connected`, `exit`).
31
+ - `POST /events?token=` — browser to server uplink (`generate`, `steer`,
32
+ `accept`, `discard`, annotations).
33
+ - `GET /poll?token=&timeout=&leaseMs=` — **agent long-poll**, default 600 s.
34
+ - `POST /poll` — **agent reply** (`done`/`steer_done`/`error`/`complete`).
35
+ - `GET /source?token=&path=`, `GET /design-system.json`, `POST /annotation`,
36
+ `GET /health` (unauthenticated liveness), `GET /stop`.
37
+ - CORS allows loopback origin **or** a valid token.
38
+
39
+ 2. **A set of stateless per-call CLI mutators** that read/write project source and
40
+ `.impeccable/live/` state: `live-wrap.mjs` (write the variant wrapper into
41
+ source), `live-accept.mjs` (stitch the chosen variant into source / carbonize),
42
+ `live-complete.mjs` (final gate), `live-status.mjs` / `live-resume.mjs`
43
+ (recovery), `live-inject.mjs` (inject the `<script>` tag).
44
+
45
+ 3. **File-based canonical state** under `<appRoot>/.impeccable/live/`: an
46
+ append-only per-session JSONL journal (`sessions/<id>.jsonl`) is the source of
47
+ truth; plus `server.json`, `config.json`, `roots.json`, accept receipts, and
48
+ per-file locks. Sessions survive helper restart via journal replay.
49
+
50
+ The pi-impeccable harness extension is just sugar over this: it spawns the poll,
51
+ maps events to prompts, and wraps `--reply`. **The engine underneath is HTTP plus
52
+ files.** That is the whole reason a product can drive it.
53
+
54
+ ---
55
+
56
+ ## Answering the five spike questions
57
+
58
+ ### (a) Can wiki-viewer run/embed the helper server? — YES
59
+
60
+ `live-server.mjs --background` spawns a detached child, prints the connection JSON,
61
+ and exits. wiki-viewer's server can spawn it exactly as `app-runner.ts` already
62
+ spawns child processes (module-level singleton Map, keyed per workspace). Boot via
63
+ `live.mjs --target <appRoot>` resolves and persists `roots.json`, so every helper
64
+ call is cwd-independent afterward.
65
+
66
+ - Auth is a single token from `server.json`. The product reads it server-side and
67
+ never exposes it in logs (matches wiki-viewer's existing token discipline).
68
+ - `GET /health` gives a clean readiness probe, same pattern as `app-runner`'s port
69
+ probe.
70
+ - Lifecycle keys naturally onto wiki-viewer's `{workspaceId, relPath}` app model:
71
+ one helper per served surface, torn down with the app.
72
+
73
+ **No blocker.** This is the same shape of work wiki-viewer already does for node
74
+ apps.
75
+
76
+ ### (b) Can wiki-viewer inject `live.js` for both served apps and static HTML? — YES, and the seam already exists
77
+
78
+ - **Served (node) apps:** `src/app/api/app-proxy/[...path]/route.ts` already
79
+ rewrites upstream HTML at serve time (injects `<base href>`, `fetch`/`XHR`
80
+ shims, a `<script>` block before `</head>`). Injecting the Impeccable prelude
81
+ globals plus a `<script src=".../live.js">` at that exact point is a natural
82
+ extension. This is **better** than `live-inject.mjs`'s source-file editing: it is
83
+ serve-time only, touches no source, and needs no `--remove` cleanup.
84
+ - **Static HTML files:** `website-viewer.tsx` renders the file (today via an
85
+ opaque-origin `srcDoc` sandbox). The same injection happens at render.
86
+
87
+ **Caveat that shapes the spec:** the stock `live.js` expects to reach the helper
88
+ directly at `http://localhost:8400` and reads prelude globals
89
+ `__IMPECCABLE_PORT__` / `__IMPECCABLE_TOKEN__`. That is fine only when the browser
90
+ is on the same machine as the helper. See (c)/transport below — for a remote
91
+ wiki-viewer the product must point those globals at proxied paths on its own
92
+ origin. Injection itself is not the hard part; the transport target is.
93
+
94
+ ### (c) Can HMR hot-swap through the proxy, with fetch-source fallback for static? — NO for HMR (confirmed), but not a blocker
95
+
96
+ The engine's hot-swap model: `live-wrap`/variant writes edit **real source files**;
97
+ the app's own dev server HMR recompiles and pushes the swap; the browser overlay
98
+ re-mounts. For no-HMR surfaces (static HTML), the overlay fetches source directly
99
+ via the `--file` reply and swaps client-side.
100
+
101
+ - **Static HTML fetch-source fallback:** clean fit. wiki-viewer serves the file and
102
+ can serve `/source`-equivalent reads; no dev server needed.
103
+ - **Dynamic apps / HMR: CONFIRMED does not pass through the proxy today.** Verified
104
+ by inspection (convergent, not by a browser run):
105
+ 1. No `upgrade`/WebSocket handler exists anywhere in `bin/` or `src/` (grep clean).
106
+ 2. The proxy **strips** the `upgrade` header as hop-by-hop (`route.ts:20`) and the
107
+ route is an HTTP GET/POST handler; a Next App Router route handler cannot
108
+ service a WebSocket upgrade at all.
109
+ 3. The optional HTTPS front-proxy (`bin/cli/serve.js:402`) only pipes `req -> res`;
110
+ no `server.on('upgrade')`.
111
+ 4. Apps mostly run without HMR regardless: `defaultScript` prefers
112
+ `start > preview(built) > dev` (`app-runner.ts:139-144`), so the HMR `dev` mode
113
+ is the last-resort default.
114
+
115
+ **Why this is not a blocker.** Impeccable's variant swap is fundamentally a
116
+ browser-side operation the overlay performs; the app framework's HMR is only the
117
+ "smooth" way to get the freshly written source rendered. Two live options for
118
+ dynamic apps without WS work:
119
+ - **Full-reload-through-proxy:** after a variant write, force a page reload. The
120
+ page reloads over plain HTTP (already handled by the proxy's SW/bootstrap
121
+ machinery), the new markup renders, the overlay re-attaches via its durable
122
+ per-origin session. Functionally correct; loses only the flicker-free feel.
123
+ - **fetch-source fallback:** works directly for raw/static-HTML-shaped surfaces
124
+ (no dev server needed). It does **not** help compiled-framework apps, whose
125
+ rendered DOM is not their source, which is exactly why those need HMR or reload.
126
+
127
+ If the smooth HMR experience is wanted for compiled apps, the real work is adding
128
+ a WebSocket-upgrade passthrough: attach `server.on('upgrade')` on wiki-viewer's
129
+ standalone server and forward the socket to the child dev-server port (bounded,
130
+ but genuinely new proxy code). That is a deliberate scope decision, not a
131
+ prerequisite for shipping.
132
+
133
+ **Net:** static-HTML surface is unaffected and is the correct tracer bullet.
134
+ Dynamic-app hot-swap works via full-reload today, or gets the smooth path later via
135
+ WS-upgrade passthrough.
136
+
137
+ ### (d) Can carbonize-on-accept map onto wiki-viewer write safety? — YES, but this is the real integration work
138
+
139
+ `live-accept.mjs` writes directly to source with its own safety layer: per-file
140
+ locks (`.impeccable/live/locks/<sha>.lock`, pid-liveness sweep), accept receipts
141
+ for idempotency (`accept-receipts/<id>.json`, conflicting op returns
142
+ `accept_receipt_conflict`), a generated-file guard, and a carbonize stitch-in that
143
+ a later step rewrites to permanent form. `live-complete.mjs` gates on a
144
+ `source_dirty` scan for leftover markers.
145
+
146
+ wiki-viewer has its **own** parallel safety spine: `resolveWorkspacePath()`
147
+ containment, `proper-lockfile` write locks, collab-state `409 COLLAB_ACTIVE`
148
+ enforcement, and the web-tweak accept path's base-hash drift guard ("Accept is the
149
+ only thing that writes the file, iff hashes still match").
150
+
151
+ Two write/lock systems over the same files is the core hazard. Resolution options
152
+ (a spec decision, not a blocker):
153
+
154
+ - **Preferred:** route Impeccable's source writes through wiki-viewer's own write
155
+ path so containment, collab-state, and locking stay authoritative. Concretely,
156
+ the product performs the accept mutation using wiki-viewer's writer, reusing
157
+ Impeccable's *logic* (marker find, variant extract, carbonize) but not its raw
158
+ `fs.writeFile` + its own lockfile. Impeccable's accept logic is mechanical and
159
+ self-contained enough to host.
160
+ - **Alternative:** let `live-accept.mjs` write, but fence it: ensure the target is
161
+ never a collab-`active` markdown file, confirm `.impeccable/` is path-denied and
162
+ gitignored (it is designed to be), and accept two lock systems that only ever
163
+ contend under concurrent edits (rare, and each is pid/owner-scoped).
164
+
165
+ Either way, `.impeccable/live/` is runtime data: must be added to `resolveWorkspacePath`'s
166
+ denied segments and gitignore, same treatment as `.proof`/`.git`.
167
+
168
+ ### (e) Can the two poll loops reconcile? — YES, clean 1:1 mapping
169
+
170
+ wiki-viewer's attached-agent channel (`src/lib/proof/live/store.ts`) is already a
171
+ poll/reply state machine strikingly close to Impeccable's:
172
+
173
+ | Impeccable engine | wiki-viewer live/store |
174
+ |---|---|
175
+ | `GET /poll` long-poll (600 s) | agent held long-poll (25 s / 400 ms tick) |
176
+ | event `type`: generate/steer/accept/discard/exit | request `kind`: generate/steer/accept/discard/exit (+ web.* kinds) |
177
+ | session journal `sessions/<id>.jsonl` | `live.db` sessions + requests, monotonic `seq` |
178
+ | pending → leased → replied | pending → delivered → working → resolved |
179
+ | one pending event per lease | one-outstanding-per-session (`409 OUTSTANDING_REQUEST`) |
180
+ | idempotency via accept receipts | idempotency key `live:<requestId>` |
181
+ | accept = only write, receipt-guarded | Accept = only write, base-hash-guarded |
182
+
183
+ The natural topology (**"product hosts engine, existing agent generates"**):
184
+
185
+ 1. wiki-viewer spawns the helper and injects `live.js` (served surface or static).
186
+ 2. The browser overlay talks to the helper (directly on localhost, or via proxied
187
+ endpoints when remote — see transport risk).
188
+ 3. wiki-viewer's **server** runs the Impeccable `/poll` loop (not a chat agent).
189
+ 4. On a `generate` event, wiki-viewer translates it into a request on its **own**
190
+ attached-agent channel (`live/store`), so the user's existing chat agent (AI
191
+ panel / MCP) produces the variants it already knows how to produce.
192
+ 5. wiki-viewer applies the variant writes (via `live-wrap` logic) and replies
193
+ `done --file` to the Impeccable `/poll`; HMR/fetch-source mounts them.
194
+ 6. On `accept`, wiki-viewer runs the accept/carbonize mutation through its write
195
+ path and completes.
196
+
197
+ This preserves wiki-viewer's "live is speculative until Accept" invariant and reuses
198
+ the agent channel that already exists, rather than duplicating it.
199
+
200
+ ---
201
+
202
+ ## Risks, ranked
203
+
204
+ 1. **Browser-to-helper transport when wiki-viewer is remote (highest).** `live.js`
205
+ assumes it can reach `localhost:8400` from the browser and read
206
+ `__IMPECCABLE_PORT__/__IMPECCABLE_TOKEN__`. wiki-viewer serves the app page from
207
+ *its* origin, and the browser may be on a different machine than the helper. The
208
+ product must proxy the helper's `/live.js`, `/events` (SSE), `/poll`-uplink,
209
+ `/source`, `/design-system.json`, `/annotation` through wiki-viewer's own origin
210
+ and set the injected globals to those proxied paths. This is a defined, bounded
211
+ bridge, but it is real code and it is the make-or-break for non-localhost use.
212
+ 2. **HMR WebSocket passthrough in app-proxy (CONFIRMED absent).** See (c). Not a
213
+ shipping blocker: static HTML is unaffected; dynamic apps work via
214
+ full-reload-through-proxy. The smooth HMR path for compiled apps is optional
215
+ later work (add `server.on('upgrade')` forwarding to the child dev port).
216
+ 3. **Dual source-write/lock systems (defined, needs a decision).** See (d). Prefer
217
+ routing writes through wiki-viewer's writer.
218
+ 4. **Runtime-dir hygiene.** `.impeccable/live/` must be path-denied
219
+ (`resolveWorkspacePath`) and gitignored so it is never served, traversed, or
220
+ committed. Low effort, must not be forgotten.
221
+ 5. **Auth/CSRF surface.** The helper's token is loopback-scoped; once proxied
222
+ through wiki-viewer, the proxied endpoints inherit wiki-viewer's session/agent
223
+ auth and CSRF Origin checks. Straightforward but must be explicit in the spec.
224
+
225
+ None of these is a "does not work." They are the shape of the bridge.
226
+
227
+ ---
228
+
229
+ ## Recommendation
230
+
231
+ **Proceed to `to-spec` for the unified Impeccable-live tweak, topology "product
232
+ hosts engine, existing agent generates."** Risk #2 is now settled: HMR does not
233
+ pass through the proxy today, but it is not required to ship. Scope the spec so the
234
+ first slice is the static-HTML surface (no HMR, no proxy transport), dynamic apps
235
+ land next via full-reload-through-proxy, and smooth compiled-app HMR is an explicit
236
+ later ticket (WS-upgrade passthrough) rather than a gate.
237
+
238
+ Do **not** fall back to the canceled isolated-origin/iframe architecture. The
239
+ pivot rationale holds: Impeccable injects into the app's own page, so there is no
240
+ parent to protect, which is exactly what deletes the old security problem.
241
+
242
+ ### Suggested first vertical slice (tracer bullet)
243
+ Static HTML surface only (no HMR, no proxy transport complexity): spawn helper,
244
+ inject `live.js` into the static preview, run the product-side `/poll` loop, wire
245
+ one `generate` through the existing agent channel, land one `accept` through
246
+ wiki-viewer's writer. That proves the whole spine end to end on the simplest
247
+ surface, then dynamic apps (proxy transport + HMR) layer on top.
248
+
249
+ ---
250
+
251
+ ## Evidence base
252
+ - Impeccable engine: `/home/sil/.pi/agent/skills/impeccable/scripts/{live,live-server,live-poll,live-inject,live-wrap,live-accept,live-complete,live-status,live-resume}.mjs`; `reference/live.md`, `reference/live-setup.md`.
253
+ - wiki-viewer proxy injection point: `src/app/api/app-proxy/[...path]/route.ts` (serve-time HTML rewrite, `<script>` before `</head>`).
254
+ - Dev/HMR launchable: `src/lib/app-runner.ts` (`defaultScript` start>preview>dev, `detectCmd`, port probe).
255
+ - Existing agent channel: `src/lib/proof/live/store.ts` (session/request state machine); companion write-up `docs/web-tweak-live-channel.md`.
256
+ - Static-HTML tweak today: `src/components/editor/website-viewer.tsx`; `src/lib/web-tweak/*`.
@@ -0,0 +1,39 @@
1
+ # Current Suggest feature — assessment (main @ 23a5129)
2
+
3
+ Verdict: REAL propose→pending→accept-writes/reject-discards pipeline with a
4
+ proper "Suggesting mode", but COARSE and side-mounted, which is why it reads like
5
+ comments.
6
+
7
+ ## Current flow (file:line)
8
+ - Entry: bubble menu "Suggest edit" (bubble-menu.tsx:236-247) -> openSuggestForSelection (editor.tsx:973-975).
9
+ - Block resolution: resolveSelectionBlock picks the WHOLE top-level block; selection offsets computed but UNUSED by Suggest (editor.tsx:267-361, 320, 352-358).
10
+ - Popover: SuggestEditPopover, kind chips Replace/Insert after/Insert before/Delete, whole-block markdown textarea + optional reason (suggest-edit-popover.tsx:47-111). Posts {type:"suggestion.add", ref, kind, markdown?, basisDetail?} with 409 retry once.
11
+ - Suggesting mode (typing capture): status-bar radio Editing/Suggesting (editor.tsx:1047-1068); banner (852-857). flush() diffs EACH top-level block vs snapshot: changed->replace, gone->delete, appended->insertAfter (use-suggestion-capture.ts:70-124), then REVERTS editor to snapshot. No autosave in suggesting (editor-store.ts:324-328,353-356).
12
+ - Render: pending suggestions absolutely positioned OUTSIDE ProseMirror DOM (suggestion-card.tsx:44-48; editor.tsx:901-933). Two static panes "current / proposed" (suggestion-card.tsx:116-137).
13
+ - Accept: suggestion.accept -> archive + supersede other pending same-ref (ops-applier.ts:872-891) + apply block.replace/insertAfter/insertBefore/delete (893-900) -> writes .md (972). Guard: 409 retry once.
14
+ - Reject: archive, file unchanged (902-909).
15
+
16
+ ## Capability vs Google Docs suggesting
17
+ - Suggesting mode toggle: PRESENT.
18
+ - Edits captured, never written: PRESENT.
19
+ - Inline text-range diffs (word-level): ABSENT (whole-block string equality).
20
+ - In-context redline in prose: ABSENT (overlay card beside block, not in editor DOM).
21
+ - Review diff clarity: PARTIAL (two static panes, no word-level diff).
22
+ - Per-change accept/reject in place: PARTIAL (buttons on overlay card).
23
+ - Accept writes doc: PRESENT. Reject discards: PRESENT.
24
+ - Overlapping suggestions same range: DEGENERATE (N cards pile on same pixel, no switcher/offset).
25
+ - Multi-author: PARTIAL (by-label; coarse whole-sidecar reload on fs events, no targeted push).
26
+ - View-mode Suggest button: ABSENT despite ux-contracts §6.1 promising it (contract drift; only Comment has a view-mode button).
27
+
28
+ ## Gaps to make it real (T1 = perception, T2 = granularity)
29
+ T1:
30
+ 1. In-doc redline rendering: TipTap mark extension (inserted=green underline, deleted=strikethrough) mounted INSIDE ProseMirror, replacing the absolute overlay card (editor.tsx:901-933, suggestion-card.tsx:44-48).
31
+ 2. Word-level diff in review affordance (suggestion-card.tsx:116-137) even before/if kept as a card.
32
+ 3. In-place accept/reject at the caret + overlapping-suggestion switcher ("1/3").
33
+ 4. View-mode Suggest button (parity with ViewModeCommentButton; view-mode-comment-button.tsx:26, editor.tsx:987).
34
+ T2:
35
+ 5. Text-range suggestion model: from/to offsets within block markdown instead of whole-block; touches capture (use-suggestion-capture.ts:70-124), proposal (suggest-edit-popover.tsx:85-95), model (types.ts:167-200), applier (ops-applier.ts:830-900).
36
+ 6. Accept-merge: real text-range merge on accept instead of whole-block block.replace clobber (ops-applier.ts:893-900), for edits made while the block changed concurrently.
37
+
38
+ ## Cut impact (why we are NOT cutting)
39
+ suggestion.accept is the ONLY human path that applies an edit (ux-contracts §6.2 :562). It is the review surface the tweak re-home routes the agent's rewrite into. Cutting kills the tweak review loop, orphans agent suggestion.add ops, breaks suggesting mode into silent data-loss, and breaks ux-contracts §6.1-6.3 + activity feed + tests.