diff --git a/.env.example b/.env.example index f8a1908a..1343cc90 100644 --- a/.env.example +++ b/.env.example @@ -1,7 +1,7 @@ # Local configuration only. Never place provider credentials in client variables. # Desktop provider: meta by default, baseten, or opencode-go. -MOKE_PROVIDER=meta +SECOND_SET_PROVIDER=meta # Meta Model API hackathon key. Meta's docs call this MODEL_API_KEY. MODEL_API_KEY= @@ -24,10 +24,10 @@ OPENCODE_API_KEY= MODAL_TOKEN_ID= MODAL_TOKEN_SECRET= MODAL_ENVIRONMENT=main -MOKE_MODAL_APP=moke-reviews -MOKE_MODAL_IMAGE=moke-review-engine:latest -MOKE_MODAL_TIMEOUT_MINUTES=15 +SECOND_SET_MODAL_APP=second-set-reviews +SECOND_SET_MODAL_IMAGE=second-set-review-engine:latest +SECOND_SET_MODAL_TIMEOUT_MINUTES=15 # Optional short-lived warm sandbox; omit to avoid idle compute charges. -# MOKE_MODAL_POOL_QUEUE=moke-review-pool +# SECOND_SET_MODAL_POOL_QUEUE=second-set-review-pool # Packaged builds: private JSON configuration, permissions 600 on Unix. -# MOKE_MODAL_CONFIG=/absolute/path/to/modal-session.json +# SECOND_SET_MODAL_CONFIG=/absolute/path/to/modal-session.json diff --git a/.project-kit/manifest.json b/.project-kit/manifest.json index 1d40e3b0..7fab74c6 100644 --- a/.project-kit/manifest.json +++ b/.project-kit/manifest.json @@ -2,7 +2,7 @@ "schemaVersion": 1, "generatorVersion": "0.1.0", "settings": { - "name": "moke-meta-hackathon", + "name": "second-set", "profile": "desktop", "authentication": "undecided" }, diff --git a/AGENTS.md b/AGENTS.md index 4ba3bebb..6963c1fa 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,4 +1,4 @@ -# moke-meta-hackathon +# second-set Use the Bun version in .bun-version. Run bun run verify before handing back implementation changes. Use Bun for installs, scripts, checking, formatting, tests, and desktop/backend builds. diff --git a/PRODUCT.md b/PRODUCT.md index d4ef7427..c26d2e1f 100644 --- a/PRODUCT.md +++ b/PRODUCT.md @@ -1,4 +1,4 @@ -# MOKE +# Second Set @@ -16,7 +16,7 @@ conversation. A review does not require an existing finding or count check. Bun and Effect own APIs, SQLite, migrations, PDF processing and Pi Durable execution. Electron owns the trusted bridge, file dialogs and credential storage. -The full MOKE AGENTS.md, shared review method and applicable discipline files +The full vendored AGENTS.md, shared review method and applicable discipline files supply Pi's system instructions. Whole-set review includes the general and all discipline instructions; a discipline review includes general context and its selected instructions. The review inventories the supplied documents, reads @@ -85,7 +85,7 @@ chooser, with a revision label requested for subsequent imports. The composer names its model, reviewer, revision and whole-set, selected-sheet or discipline scope. Optional sheet discipline assignments persist independently of count verification. Explicit scopes take precedence over request wording. User requests appear as chat -bubbles, followed by elapsed progress and MOKE's saved response. A note is saved +bubbles, followed by elapsed progress and Second Set's saved response. A note is saved without starting inference. Active reviews accept durable follow-up instructions from the composer and show their delivery state. Findings open their source evidence in a central canvas with zoom (including Ctrl+scroll and trackpad pinch), diff --git a/README.md b/README.md index 3186c683..e390860a 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,15 @@ -# Moke +# Second Set -Moke is a desktop workbench for reviewing engineering drawing packets, built for +Second Set is a desktop workbench for reviewing engineering drawing packets, built for the Meta AI Developer Hackathon. Import the drawings and ask for a whole-set QA -pass or a discipline review. Moke reads source images, follows its review +pass or a discipline review. Second Set reads source images, follows its review instructions, records findings and coverage, and produces an offline HTML report. Follow-up requests can deepen the review while retaining earlier issue IDs. -[Watch the narrated demo on YouTube](https://youtu.be/wl8wwq_Tlrw). +[Watch the narrated demo on YouTube](https://youtu.be/wl8wwq_Tlrw). Second Set was called +MOKE until November 9, 2026, so the demo and earlier records use that name. -![Moke's five-sheet review with 19 findings, coverage gaps, and findings navigation](docs/screenshots/workbench.png) +![Second Set's five-sheet review with 19 findings, coverage gaps, and findings navigation](docs/screenshots/workbench.png) ## The review workflow @@ -18,7 +19,7 @@ Follow-up requests can deepen the review while retaining earlier issue IDs. drawing set" or "Review the electrical design and supporting calculations." Choose a model and reviewer, or let Auto route the request. A saved note does not start inference. -3. **Follow the inspection.** Moke inventories the supplied sheets, opens source +3. **Follow the inspection.** Second Set inventories the supplied sheets, opens source images and detail crops, and records the four review passes: document completeness, code and agency requirements, coordination, and prior comments. A Florida code index points each check to its code section, statute or rule. @@ -40,9 +41,8 @@ Follow-up requests can deepen the review while retaining earlier issue IDs. ![A saved false-alarm decision beside the source drawing and SAM highlight](docs/screenshots/correction.png) Revision reviews carry earlier findings into the revised sheets. The demo also -shows a finding that remained open on a 13-sheet set reviewed on October 31. - -![A revision recheck retains an unresolved area discrepancy and the reviewer's decision](docs/screenshots/revision.png) +shows [a finding that remained open](https://youtu.be/wl8wwq_Tlrw?t=122) on a +13-sheet set reviewed on October 31. Drawn-to-schedule count comparisons remain available as specialized checks. Reviewers accept their count inputs and coverage before deterministic comparison; @@ -59,12 +59,26 @@ dimensions. ## Install Download the installer for Windows, macOS (Apple silicon) or Linux from -[GitHub Releases](https://github.com/sc-frederick/moke-meta-hackathon/releases/latest). +[GitHub Releases](https://github.com/Surfside-Software/second-set/releases/latest). The installers are unsigned. macOS can block the first launch; allow it under System Settings › Privacy & Security › Open Anyway. On Linux, make the AppImage executable or install the `.deb`. Model reviews need a provider key; see [inference setup](docs/inference.md). +### Upgrading from MOKE 0.2.2 + +- Second Set installs as a new app beside MOKE and opens MOKE's projects from the same + data folder. Don't run both at once; uninstall MOKE when you're done with it. +- On macOS and Linux, keys saved in MOKE may not unlock. Enter them again in Review + settings or import them from the File menu; the keys that still unlock keep working. +- A review paused in MOKE can't resume. Start a new review; finished reviews and their + reports are unchanged. +- `MOKE_*` environment variables are now `SECOND_SET_*`, for example + `SECOND_SET_MODAL_CONFIG` and `SECOND_SET_SESSION_CREDENTIAL_FILE`. A saved Modal + configuration needs `appName` `second-set-reviews` and `imageName` + `second-set-review-engine:latest`, and each provider Secret needs the key + `SECOND_SET_API_KEY`; see [cloud reviews](docs/cloud-reviews.md). + ## How it works | Process | Owns | @@ -78,7 +92,7 @@ over authorized Electron MessagePorts; Electron forwards application requests to Bun over authenticated loopback HTTP. Database access and provider credentials stay outside the renderer. -Pi Durable journals the review and runs the full Moke system instructions. A +Pi Durable journals the review and runs the full vendored review instructions. A separate ledger records model and SAM intent before dispatch, preserving unknown outcomes without automatic resends. Reviews have pause and cancel controls and an optional call budget. The default packet review has no application call ceiling; @@ -158,13 +172,14 @@ repository began on October 27, 2026. The [rules and evidence review](docs/valid maps the public repository, Meta model integration, demo, and source attribution to the participant guide checked on November 4. -The Moke name, review prompts, and the owner's 2023 Florida Building Code -plan-review reference predate this entry. The code index tooling and +The review prompts and the owner's 2023 Florida Building Code plan-review +reference predate this entry. The code index tooling and `florida-code-additions.md` were added during the hackathon; see the [code index provenance](backend/review/code-index/README.md). This repository implements a fresh application from the project-kit desktop starter, with drawing evidence, corrections, and revision tracking built for the hackathon. It does not reuse the -earlier Moke application. Pi Durable provides agent execution as an MIT-licensed +earlier MOKE application. The project was renamed from MOKE to Second Set on +November 9, 2026, and no longer uses the MOKE name. Pi Durable provides agent execution as an MIT-licensed dependency; the new work is the drawing-review application, evidence handling, corrections, revision behavior and portable review execution. Prompt and dependency provenance live in @@ -173,7 +188,7 @@ dependency provenance live in ## License -Moke is released under the [MIT License](LICENSE). Vendored code and third-party +Second Set is released under the [MIT License](LICENSE). Vendored code and third-party assets keep their own licenses. The styles in `src/lib/forge.css` are adapted from Uiverse Design's Forge and remain under its license; see [the design notes](docs/design.md). diff --git a/backend/cloud/cache.ts b/backend/cloud/cache.ts index 642df68c..52befc1f 100644 --- a/backend/cloud/cache.ts +++ b/backend/cloud/cache.ts @@ -75,7 +75,7 @@ export const writeCloudCacheState = (path: string, state: CloudCacheState) => * letters, digits, dashes, periods and underscores, and must be shorter than 64 characters. */ export const cloudCacheVolumeName = (appName: string, id: string) => - `${appName}-cache-${id}`.length < 64 ? `${appName}-cache-${id}` : `moke-cache-${id}`; + `${appName}-cache-${id}`.length < 64 ? `${appName}-cache-${id}` : `second-set-cache-${id}`; /** Content-addressed originals and renders in the local artifact store. */ export const listLocalArtifacts = (dataDirectory: string) => diff --git a/backend/cloud/contracts.ts b/backend/cloud/contracts.ts index d40c7d05..42fba902 100644 --- a/backend/cloud/contracts.ts +++ b/backend/cloud/contracts.ts @@ -22,7 +22,8 @@ import { codeIndexVersion } from "../review/code-index/code-index.ts"; export const cloudEngineIdentity = { // Protocol 4 workers read unchanged source files from the workspace cache Volume. // Protocol 5 workers add the Florida code index, code search and PDF source text. - protocol: 5, + // Protocol 6 workers keep their files under /second-set and read SECOND_SET_ variables. + protocol: 6, engine: packageEngineFormat, prompt: reviewPromptVersion, instructions: reviewInstructionRevision, @@ -69,7 +70,7 @@ export const CloudReviewRecord = Schema.Struct({ id: CloudReviewId, // Unmarked receipts predate protocol-aware admission and may name a running v2 worker. // Protocol 3 receipts remain readable so already dispatched work can still return. - workerProtocol: Schema.optionalKey(Schema.Literals([3, 4, cloudEngineIdentity.protocol])), + workerProtocol: Schema.optionalKey(Schema.Literals([3, 4, 5, cloudEngineIdentity.protocol])), revision: Schema.Int.pipe(Schema.withDecodingDefaultKey(Effect.succeed(0))), syncFailure: Schema.NullOr(CloudReviewError).pipe( Schema.withDecodingDefaultKey(Effect.succeed(null)), @@ -100,6 +101,10 @@ export const CloudReviewRecord = Schema.Struct({ /** Persisted record type stays private to cloud application and adapters. */ export type CloudReviewRecord = typeof CloudReviewRecord.Type; +/** Workers before protocol 6 shipped under the MOKE name and keep their files under /moke. */ +export const cloudWorkerRoot = (record: Pick) => + record.workerProtocol === cloudEngineIdentity.protocol ? "/second-set" : "/moke"; + /** * Provider injection uses the canonical application route and a separately supplied secret. A * custom endpoint is not a cloud route: its key stays on the computer that saved it. diff --git a/backend/cloud/modal.ts b/backend/cloud/modal.ts index 6afaacee..f0139b30 100644 --- a/backend/cloud/modal.ts +++ b/backend/cloud/modal.ts @@ -23,6 +23,7 @@ import { cloudEngineIdentity, cloudProviderEndpoints, cloudWorkerMinimumTimeoutMs, + cloudWorkerRoot, type CloudReviewRecord, } from "./contracts.ts"; import { CloudSandbox } from "./service.ts"; @@ -79,13 +80,13 @@ class ModalRequestFailure extends Schema.TaggedError()("Mod /** Readiness checks use the worker itself, without exposing a public HTTP tunnel. */ export const modalReadinessProbe = () => - Probe.withExec(["bun", "/opt/moke/worker.mjs", "--identity"]); + Probe.withExec(["bun", "/opt/second-set/worker.mjs", "--identity"]); /** Pooled sandboxes start without credentials and can later receive an outbound policy. */ export const emptyReviewOutboundPolicy = () => new ExperimentalOutboundPolicy().withHeaderReplacement({ domain: "api.meta.ai", - headers: { "X-Moke-Sandbox": "review" }, + headers: { "X-Second-Set-Sandbox": "review" }, }); /** Bounded review capacity shared by cold allocation and optional warm capacity. */ @@ -146,12 +147,12 @@ const retainScript = ( ), ].join("\n"); -/** Delete cached files outside the keep list, which the caller writes to /tmp/moke-keep. */ +/** Delete cached files outside the keep list, which the caller writes to /tmp/second-set-keep. */ const pruneCacheScript = `set -eu mkdir -p ${cacheFolder} -ls -1 ${cacheFolder} | sort > /tmp/moke-have -sort -u /tmp/moke-keep > /tmp/moke-keep-sorted -comm -23 /tmp/moke-have /tmp/moke-keep-sorted | while read -r hash; do rm -f -- "${cacheFolder}/$hash"; done +ls -1 ${cacheFolder} | sort > /tmp/second-set-have +sort -u /tmp/second-set-keep > /tmp/second-set-keep-sorted +comm -23 /tmp/second-set-have /tmp/second-set-keep-sorted | while read -r hash; do rm -f -- "${cacheFolder}/$hash"; done sync /cache`; /** Official Modal SDK adapter; API keys are injected into outbound HTTPS outside the workload. */ @@ -218,7 +219,7 @@ export const modalSandboxLayer = ( const identity = Effect.fn("Modal.identity")(function* (sandbox: Sandbox) { const text = yield* modalCall(async () => { - const process = await sandbox.exec(["bun", "/opt/moke/worker.mjs", "--identity"], { + const process = await sandbox.exec(["bun", "/opt/second-set/worker.mjs", "--identity"], { timeoutMs: 15_000, }); @@ -347,7 +348,7 @@ export const modalSandboxLayer = ( experimentalOptions: { enable_exit_snapshot: true }, experimentalOutboundPolicy: emptyReviewOutboundPolicy(), readinessProbe: modalReadinessProbe(), - tags: { "moke-kind": "review", "moke-job": record.id }, + tags: { "second-set-kind": "review", "second-set-job": record.id }, }; if (cache !== null) params.volumes = cache; @@ -388,9 +389,9 @@ export const modalSandboxLayer = ( }), ); - yield* modalCall(() => recovery.mountImage("/moke/output", snapshot)).pipe( - Effect.tapError(() => modalCall(() => recovery.terminate()).pipe(Effect.ignore)), - ); + yield* modalCall(() => + recovery.mountImage(`${cloudWorkerRoot(record)}/output`, snapshot), + ).pipe(Effect.tapError(() => modalCall(() => recovery.terminate()).pipe(Effect.ignore))); return { sandbox: recovery, recovery: true, sourceStopped: false }; } @@ -459,14 +460,14 @@ export const modalSandboxLayer = ( yield* modalCall(() => client.secrets.fromName(`${config.appName}-${provider.provider}-api`, { environment: config.environment, - requiredKeys: ["MOKE_API_KEY"], + requiredKeys: ["SECOND_SET_API_KEY"], }), ).pipe( Effect.mapError( () => new CloudReviewError({ reason: "configuration", - message: `The cloud ${provider.provider === "baseten" ? "Muse Spark Open" : reviewProviderRoutes[provider.provider].label} connection is missing or inaccessible. Configure the matching provider secret with MOKE_API_KEY in Modal before sending. Your workspace has not been uploaded.`, + message: `The cloud ${provider.provider === "baseten" ? "Muse Spark Open" : reviewProviderRoutes[provider.provider].label} connection is missing or inaccessible. Configure the matching provider secret with SECOND_SET_API_KEY in Modal before sending. Your workspace has not been uploaded.`, }), ), ); @@ -489,7 +490,7 @@ export const modalSandboxLayer = ( cpuLimit: 1, memoryMiB: 2048, memoryLimitMiB: 2048, - tags: { "moke-kind": "preflight" }, + tags: { "second-set-kind": "preflight" }, }), ), (probe) => modalCall(() => probe.terminate({ wait: true })).pipe(Effect.ignore), @@ -510,7 +511,7 @@ export const modalSandboxLayer = ( const iterator = yield* Effect.acquireRelease( Effect.sync(() => sandbox.filesystem - .watch("/moke", { + .watch(cloudWorkerRoot(record), { recursive: false, filter: ["Create", "Modify"], timeoutMs: 30_000, @@ -537,7 +538,9 @@ export const modalSandboxLayer = ( Effect.mapError(unavailable), ), ), - Stream.filter((event) => event.paths.includes("/moke/progress.json")), + Stream.filter((event) => + event.paths.includes(`${cloudWorkerRoot(record)}/progress.json`), + ), Stream.map(() => undefined), ); }), @@ -562,10 +565,15 @@ export const modalSandboxLayer = ( modalCall(() => sandbox.filesystem.copyFromLocal( join(source, file.path), - `/moke/input/${file.path}`, + `${cloudWorkerRoot(record)}/input/${file.path}`, ), ), - modalCall(() => sandbox.filesystem.writeText(text, "/moke/input/manifest.json")), + modalCall(() => + sandbox.filesystem.writeText( + text, + `${cloudWorkerRoot(record)}/input/manifest.json`, + ), + ), report, (file) => cached.get(file.sha256) === file.bytes, ); @@ -577,12 +585,12 @@ export const modalSandboxLayer = ( request: record.request, timeoutMs: config.timeoutMinutes * 60_000, }), - "/moke/request.json", + `${cloudWorkerRoot(record)}/request.json`, ), ); const checkpoint = yield* modalCall(() => - sandbox.snapshotDirectory("/moke", { ttlMs: 7 * 24 * 60 * 60_000 }), + sandbox.snapshotDirectory(cloudWorkerRoot(record), { ttlMs: 7 * 24 * 60 * 60_000 }), ); return checkpoint.imageId; @@ -609,7 +617,7 @@ export const modalSandboxLayer = ( request: record.request, timeoutMs: remainingMs, }), - "/moke/request.json", + `${cloudWorkerRoot(record)}/request.json`, ), ); const providers = [provider, ...alternatives]; @@ -625,14 +633,14 @@ export const modalSandboxLayer = ( const secret = yield* modalCall(() => client.secrets.fromName(`${record.appName}-${connection.provider}-api`, { environment: record.environment, - requiredKeys: ["MOKE_API_KEY"], + requiredKeys: ["SECOND_SET_API_KEY"], }), ); policy = policy.withHeaderReplacement({ domain: new URL(connection.baseUrl).hostname, secret, - headers: { Authorization: "Bearer $MOKE_API_KEY" }, + headers: { Authorization: "Bearer $SECOND_SET_API_KEY" }, }); } @@ -646,8 +654,8 @@ export const modalSandboxLayer = ( ); yield* modalCall(async () => { - const process = await sandbox.exec(["bun", "/opt/moke/worker.mjs"], { - env: { MOKE_CLOUD_PROVIDERS: JSON.stringify(routes) }, + const process = await sandbox.exec(["bun", "/opt/second-set/worker.mjs"], { + env: { SECOND_SET_CLOUD_PROVIDERS: JSON.stringify(routes) }, stdout: "ignore", stderr: "ignore", timeoutMs: remainingMs + 60_000, @@ -663,7 +671,9 @@ export const modalSandboxLayer = ( const text = yield* Effect.tryPromise({ try: async () => { try { - return await sandbox.filesystem.readText("/moke/progress.json"); + return await sandbox.filesystem.readText( + `${cloudWorkerRoot(record)}/progress.json`, + ); } catch (error) { if (sourceStopped && error instanceof SandboxFilesystemNotFoundError) return JSON.stringify({ @@ -691,7 +701,9 @@ export const modalSandboxLayer = ( const checkpoint = progress.phase === "ready" ? yield* modalCall(() => - sandbox.snapshotDirectory("/moke/output", { ttlMs: 7 * 24 * 60 * 60_000 }), + sandbox.snapshotDirectory(`${cloudWorkerRoot(record)}/output`, { + ttlMs: 7 * 24 * 60 * 60_000, + }), ) : null; @@ -702,7 +714,7 @@ export const modalSandboxLayer = ( reading(record, (sandbox) => Effect.gen(function* () { const text = yield* modalCall(() => - sandbox.filesystem.readText("/moke/output/manifest.json"), + sandbox.filesystem.readText(`${cloudWorkerRoot(record)}/output/manifest.json`), ); yield* downloadCloudPackage( @@ -711,7 +723,7 @@ export const modalSandboxLayer = ( (file, local) => Effect.gen(function* () { const info = yield* modalCall(() => - sandbox.filesystem.stat(`/moke/output/${file.path}`), + sandbox.filesystem.stat(`${cloudWorkerRoot(record)}/output/${file.path}`), ); if (info.type !== "file" || info.size !== file.bytes) @@ -720,7 +732,10 @@ export const modalSandboxLayer = ( message: "A cloud package file changed before download.", }); yield* modalCall(() => - sandbox.filesystem.copyToLocal(`/moke/output/${file.path}`, local), + sandbox.filesystem.copyToLocal( + `${cloudWorkerRoot(record)}/output/${file.path}`, + local, + ), ); }), report, @@ -774,7 +789,7 @@ export const modalSandboxLayer = ( cpu: 0.25, memoryMiB: 512, volumes: { "/cache": volume }, - tags: { "moke-kind": "cache" }, + tags: { "second-set-kind": "cache" }, }), ), (sandbox) => modalCall(() => sandbox.terminate({ wait: true })).pipe(Effect.ignore), @@ -836,7 +851,7 @@ export const modalSandboxLayer = ( if (keep !== null && !(yield* request.paused)) { yield* modalCall(() => - uploader.filesystem.writeText(`${keep.join("\n")}\n`, "/tmp/moke-keep"), + uploader.filesystem.writeText(`${keep.join("\n")}\n`, "/tmp/second-set-keep"), ); yield* script(pruneCacheScript); } diff --git a/backend/cloud/progress.ts b/backend/cloud/progress.ts index 7687c64c..58053c50 100644 --- a/backend/cloud/progress.ts +++ b/backend/cloud/progress.ts @@ -1,6 +1,17 @@ import { Effect, Schema, Struct } from "effect"; import { CloudReviewError } from "#lib/review/cloud.ts"; -import { CloudEngineIdentity, CloudWorkerProgress, type CloudReviewRecord } from "./contracts.ts"; +import { + CloudEngineIdentity, + CloudWorkerProgress, + cloudEngineIdentity, + type CloudReviewRecord, +} from "./contracts.ts"; + +/** + * Workers before protocol 6 shipped as MOKE. Their progress files still announce the instruction + * names they were built with, so recovery matches those exact strings. + */ +const legacyPrompt = Schema.Literal("moke-selected-coordination-v2"); /** Workers from earlier protocols, as built with any packet prompt they shipped with. */ const priorWorkerProgress = ( @@ -13,6 +24,7 @@ const priorWorkerProgress = ( // The code index entered the identity with protocol 5. ...CloudEngineIdentity.mapFields(Struct.omit(["codeIndex"])).fields, protocol: Schema.Literal(protocol), + prompt: legacyPrompt, packetPrompt: Schema.Literals(packetPrompts), }), }); @@ -30,33 +42,34 @@ const Protocol4CloudWorkerProgress = priorWorkerProgress(4, [ "moke-packet-review-v5", ]); -// Earlier protocol-5 images used packet prompts v6 or v7 with the v1 index, v8 with v2, then v9 with v3. -const Protocol5CloudWorkerProgress = Schema.Union([ - CloudWorkerProgress, - Schema.Struct({ - ...CloudWorkerProgress.fields, - identity: Schema.Struct({ - ...CloudEngineIdentity.fields, - packetPrompt: Schema.Literals(["moke-packet-review-v6", "moke-packet-review-v7"]), - codeIndex: Schema.Literal("florida-code-index-v1"), - }), - }), - Schema.Struct({ - ...CloudWorkerProgress.fields, - identity: Schema.Struct({ - ...CloudEngineIdentity.fields, - packetPrompt: Schema.Literal("moke-packet-review-v8"), - codeIndex: Schema.Literal("florida-code-index-v2"), - }), - }), +const protocol5WorkerProgress = ( + packetPrompts: ReadonlyArray<`moke-packet-review-v${6 | 7 | 8 | 9 | 10}`>, + codeIndex: `florida-code-index-v${1 | 2 | 3}`, +) => Schema.Struct({ ...CloudWorkerProgress.fields, identity: Schema.Struct({ ...CloudEngineIdentity.fields, - packetPrompt: Schema.Literal("moke-packet-review-v9"), - codeIndex: Schema.Literal("florida-code-index-v3"), + protocol: Schema.Literal(5), + prompt: legacyPrompt, + packetPrompt: Schema.Literals(packetPrompts), + codeIndex: Schema.Literal(codeIndex), }), - }), + }); + +// Protocol-5 images used packet prompts v6 or v7 with the v1 index, v8 with v2, then v9 and v10 +// with v3. A stopped source's synthesized failure carries the current identity. +const Protocol5CloudWorkerProgress = Schema.Union([ + CloudWorkerProgress, + protocol5WorkerProgress( + ["moke-packet-review-v6", "moke-packet-review-v7"], + "florida-code-index-v1", + ), + protocol5WorkerProgress(["moke-packet-review-v8"], "florida-code-index-v2"), + protocol5WorkerProgress( + ["moke-packet-review-v9", "moke-packet-review-v10"], + "florida-code-index-v3", + ), ]); /** Unmarked receipts were saved by protocol 2 or 3 desktops, never by protocol 4. */ @@ -93,18 +106,19 @@ export const parseCloudWorkerProgress = Effect.fn("parseCloudWorkerProgress")(fu }); // Old receipts also include v3 jobs saved before this marker was introduced. Only - // observation of already dispatched work can accept protocols 2 to 4; this never admits a request. + // observation of already dispatched work can accept protocols 2 to 5; this never admits a request. const dispatched = record.phase === "dispatching" || record.phase === "running"; - const schema = !dispatched - ? CloudWorkerProgress - : record.workerProtocol === undefined - ? UnmarkedCloudWorkerProgress - : record.workerProtocol === 3 - ? Protocol3CloudWorkerProgress - : record.workerProtocol === 4 - ? Protocol4CloudWorkerProgress - : Protocol5CloudWorkerProgress; + const schema = + !dispatched || record.workerProtocol === cloudEngineIdentity.protocol + ? CloudWorkerProgress + : record.workerProtocol === undefined + ? UnmarkedCloudWorkerProgress + : record.workerProtocol === 3 + ? Protocol3CloudWorkerProgress + : record.workerProtocol === 4 + ? Protocol4CloudWorkerProgress + : Protocol5CloudWorkerProgress; const progress = yield* Schema.decodeUnknownEffect(Schema.fromJsonString(schema))(text).pipe( Effect.mapError( diff --git a/backend/cloud/service.ts b/backend/cloud/service.ts index bc302b61..47c24d7d 100644 --- a/backend/cloud/service.ts +++ b/backend/cloud/service.ts @@ -623,7 +623,7 @@ export class CloudReviews extends Context.Service< sandboxId: null, inputSnapshotId: null, resultSnapshotId: null, - sandboxName: `moke-review-${id}`, + sandboxName: `second-set-review-${id}`, appName: config.modal.appName, environment: config.modal.environment, ...versions, diff --git a/backend/cloud/worker-main.ts b/backend/cloud/worker-main.ts index 8ce6e0c3..2c98b5a1 100644 --- a/backend/cloud/worker-main.ts +++ b/backend/cloud/worker-main.ts @@ -23,7 +23,7 @@ const failed = () => const main = Effect.scoped( Effect.gen(function* () { const text = yield* Effect.tryPromise({ - try: () => readFile("/moke/request.json", "utf8"), + try: () => readFile("/second-set/request.json", "utf8"), catch: failed, }); @@ -31,13 +31,13 @@ const main = Effect.scoped( const [provider, ...alternatives] = yield* Schema.decodeUnknownEffect( Schema.fromJsonString(CloudProviders), - )(process.env.MOKE_CLOUD_PROVIDERS); + )(process.env.SECOND_SET_CLOUD_PROVIDERS); // This exclusive marker fences even two exec requests with an ambiguous delivery outcome. yield* Effect.tryPromise({ try: async () => { - await mkdir("/moke", { recursive: true, mode: 0o700 }); - const file = await open("/moke/dispatch.lock", "wx", 0o600); + await mkdir("/second-set", { recursive: true, mode: 0o700 }); + const file = await open("/second-set/dispatch.lock", "wx", 0o600); try { await file.writeFile(job.id); @@ -51,7 +51,7 @@ const main = Effect.scoped( const progress = (update: Omit) => writeCloudFile( - "/moke/progress.json", + "/second-set/progress.json", JSON.stringify({ id: job.id, identity: cloudEngineIdentity, ...update }), ); @@ -65,7 +65,7 @@ const main = Effect.scoped( }); // Files the desktop skipped come from the read-only workspace cache, checked by hash. - yield* materializeCloudInput("/moke/input", "/cache").pipe( + yield* materializeCloudInput("/second-set/input", "/cache").pipe( Effect.tapError((error) => Effect.sync(() => { failure = `${error.message} No model calls were made.`; @@ -74,16 +74,16 @@ const main = Effect.scoped( ); const config = new BackendConfig({ - dataDirectory: "/moke/workspace", - migrationDirectory: "/opt/moke/migrations", + dataDirectory: "/second-set/workspace", + migrationDirectory: "/opt/second-set/migrations", token: Redacted.make(crypto.randomUUID()), inference: provider, inferenceAlternatives: alternatives, processing: { - workerCommand: ["bun", "/opt/moke/pdf-worker.mjs"], - assetsDirectory: "/opt/moke/node_modules/pdfjs-dist", + workerCommand: ["bun", "/opt/second-set/pdf-worker.mjs"], + assetsDirectory: "/opt/second-set/node_modules/pdfjs-dist", nativeLibraryPath: - "/opt/moke/node_modules/@napi-rs/canvas-linux-x64-gnu/skia.linux-x64-gnu.node", + "/opt/second-set/node_modules/@napi-rs/canvas-linux-x64-gnu/skia.linux-x64-gnu.node", }, }); @@ -107,11 +107,13 @@ const main = Effect.scoped( callbacks = runtime; const application = yield* Effect.promise(() => runtime.runPromise(Application)); - yield* Effect.promise(() => runtime.runPromise(application.restorePackage("/moke/input"))); + yield* Effect.promise(() => + runtime.runPromise(application.restorePackage("/second-set/input")), + ); yield* Effect.promise(() => runtime.runPromise( - runCloudReview(application, job.request, job.timeoutMs, "/moke/output", progress), + runCloudReview(application, job.request, job.timeoutMs, "/second-set/output", progress), ), ); }); diff --git a/backend/documents/artifact-store.ts b/backend/documents/artifact-store.ts index bab2e803..db710d77 100644 --- a/backend/documents/artifact-store.ts +++ b/backend/documents/artifact-store.ts @@ -92,7 +92,7 @@ export const storeBytes = Effect.fnUntraced(function* ( }); /** Stored artifact bytes read and hashed to verify them against their content hash. */ -export const artifactBytesVerified = Metric.counter("moke_artifact_bytes_verified", { +export const artifactBytesVerified = Metric.counter("second_set_artifact_bytes_verified", { description: "Stored artifact bytes read and hashed to verify their content hash.", incremental: true, }); diff --git a/backend/documents/pdf-worker-main.ts b/backend/documents/pdf-worker-main.ts index 207bcaab..ade8c671 100644 --- a/backend/documents/pdf-worker-main.ts +++ b/backend/documents/pdf-worker-main.ts @@ -1,6 +1,6 @@ /** * Standalone PDF worker: schema-validated NDJSON on stdin/stdout. - * Run under Bun with `MOKE_PDFJS_ASSETS` and `NAPI_RS_NATIVE_LIBRARY_PATH` set. + * Run under Bun with `SECOND_SET_PDFJS_ASSETS` and `NAPI_RS_NATIVE_LIBRARY_PATH` set. */ import { createCanvas } from "@napi-rs/canvas"; import { getDocument, GlobalWorkerOptions } from "pdfjs-dist/legacy/build/pdf.mjs"; @@ -22,10 +22,10 @@ import { WorkerTextResult, } from "./worker-messages.ts"; -const assetsRoot = process.env.MOKE_PDFJS_ASSETS; +const assetsRoot = process.env.SECOND_SET_PDFJS_ASSETS; if (!assetsRoot) { - console.error(JSON.stringify({ fatal: "MOKE_PDFJS_ASSETS is required." })); + console.error(JSON.stringify({ fatal: "SECOND_SET_PDFJS_ASSETS is required." })); process.exit(2); } @@ -413,13 +413,13 @@ async function handleRequest(line: string) { } // Test hooks: record each accepted job with this process ID, then hold it before PDF work. - const started = process.env.MOKE_PDF_WORKER_STARTED_DIRECTORY; + const started = process.env.SECOND_SET_PDF_WORKER_STARTED_DIRECTORY; if (started) writeFileSync(join(started, `${decoded._tag}.${randomUUID()}`), String(process.pid)); - if (process.env.MOKE_PDF_WORKER_DELAY_MS) { + if (process.env.SECOND_SET_PDF_WORKER_DELAY_MS) { await new Promise((resolve) => - setTimeout(resolve, Number(process.env.MOKE_PDF_WORKER_DELAY_MS)), + setTimeout(resolve, Number(process.env.SECOND_SET_PDF_WORKER_DELAY_MS)), ); } diff --git a/backend/documents/worker-client.ts b/backend/documents/worker-client.ts index 4c8fa52b..1f903036 100644 --- a/backend/documents/worker-client.ts +++ b/backend/documents/worker-client.ts @@ -35,7 +35,7 @@ export const openPdfWorker = Effect.fn("openPdfWorker")(function* (config: Docum stderr: "inherit", env: { ...process.env, - MOKE_PDFJS_ASSETS: config.assetsDirectory, + SECOND_SET_PDFJS_ASSETS: config.assetsDirectory, NAPI_RS_NATIVE_LIBRARY_PATH: config.nativeLibraryPath, NODE_PATH: join(config.assetsDirectory, "..", "modules"), BUN_OPTIONS: "", diff --git a/backend/inference/pacing.ts b/backend/inference/pacing.ts index 877c991d..7fba7f4d 100644 --- a/backend/inference/pacing.ts +++ b/backend/inference/pacing.ts @@ -16,7 +16,7 @@ export const reportedRateLimits = { meta: { requestsPerMinute: 3000, tokensPerMinute: 4_000_000 }, baseten: { requestsPerMinute: 100, tokensPerMinute: 2_000_000 }, // OpenCode Go publishes no request rate; it asks for typical coding agent traffic and monitors for - // abuse. This ceiling is MOKE's own choice, well above three reviewers' measured 8 requests a + // abuse. This ceiling is Second Set's own choice, well above three reviewers' measured 8 requests a // minute, so a burst of quick answers still looks like one person's agents. "opencode-go": { requestsPerMinute: 60, tokensPerMinute: 5_000_000 }, // A custom endpoint's limits are unknown until it sends these headers. A 429 starts the shared diff --git a/backend/inference/provider.ts b/backend/inference/provider.ts index 07f0199b..c784b146 100644 --- a/backend/inference/provider.ts +++ b/backend/inference/provider.ts @@ -67,8 +67,8 @@ export interface ProviderConfiguration { * attribution name as `x-opencode-client` (OpenCode Console docs, 2026-11-01). */ const openCodeIdentity: ClientIdentity = { - userAgent: { custom: `moke/${packageInfo.version}` }, - attribution: { name: "moke", url: "https://github.com/sc-frederick/moke-meta-hackathon" }, + userAgent: { custom: `second-set/${packageInfo.version}` }, + attribution: { name: "second-set", url: "https://github.com/Surfside-Software/second-set" }, }; /** Meta serves Muse Spark 1.3 with a larger output ceiling than Pi's catalog records (checked 2026-10-29). */ @@ -127,7 +127,7 @@ const ModelNotFound = Schema.fromJsonString( const refusalBudgetMs = 10 * 60_000; const maxRefusals = 40; -/** OpenCode's own client resends a rate-limited request at most ten times; MOKE matches it. */ +/** OpenCode's own client resends a rate-limited request at most ten times; Second Set matches it. */ const openCodeMaxRefusals = 11; const OpenCodeRefusal = Schema.fromJsonString( @@ -253,7 +253,7 @@ function refusalDelay(retryAfterMs: number | undefined, retry: number) { export function promptCacheKey(role: string, ...prefix: ReadonlyArray) { const digest = createHash("sha256").update(JSON.stringify(prefix)).digest("hex"); - return `moke-${role}-${digest.slice(0, 32)}`; + return `second-set-${role}-${digest.slice(0, 32)}`; } /** @@ -646,7 +646,7 @@ export function createAdmittedModels( const serverOutputLimit = selected.provider === "baseten" || custom !== null; // Pi supplies each conversation's durable session; a request outside one gets this adapter's. - const fallbackSession = `moke-${randomUUID()}`; + const fallbackSession = `second-set-${randomUUID()}`; /** Send one image to the Files API once a slot is free; any failure or stop leaves it inline. */ const uploadImage = (image: string, sha256: string, signal: AbortSignal) => @@ -1173,7 +1173,7 @@ export function createAdmittedModels( return { ...payload, ...changes }; }; // OpenCode Go requires a stable session per conversation and routes and caches by it. Pi's - // provider sends it as `x-opencode-session`, with MOKE's identity, so sheet reviewers sharing a + // provider sends it as `x-opencode-session`, with Second Set's identity, so sheet reviewers sharing a // prompt prefix still keep their own sessions, as OpenCode's guidance asks. else if (openCodeGo) { result.sessionId = options?.sessionId ?? fallbackSession; diff --git a/backend/inference/segmentation.ts b/backend/inference/segmentation.ts index b40b699d..49bda5ff 100644 --- a/backend/inference/segmentation.ts +++ b/backend/inference/segmentation.ts @@ -232,7 +232,7 @@ export class ReviewSegmentation extends Context.Service< projectId: typeof ProjectId.Type, ) => Effect.Effect, ExecutionError>; } ->()("moke/ReviewSegmentation") { +>()("second-set/ReviewSegmentation") { /** One parent-owned runtime supplies cancellation and callbacks; this layer starts no runtime. */ static layer(config: ReviewSegmentationConfiguration) { return Layer.sync(ReviewSegmentation, () => { diff --git a/backend/packet-review/jurisdiction.ts b/backend/packet-review/jurisdiction.ts index 2a87a7f2..c4342226 100644 --- a/backend/packet-review/jurisdiction.ts +++ b/backend/packet-review/jurisdiction.ts @@ -246,7 +246,7 @@ export function jurisdictionHistory( return { history: { jurisdiction, - note: "Leads from earlier MOKE reviews in this workspace, matched by state or locality. They are not evidence for this project. Read a source again with read_source and confirm its edition and applicability before citing it. Check an earlier agency comment against the current drawings before recording anything.", + note: "Leads from earlier Second Set reviews in this workspace, matched by state or locality. They are not evidence for this project. Read a source again with read_source and confirm its edition and applicability before citing it. Check an earlier agency comment against the current drawings before recording anything.", earlierReviews: reviews.map(({ review, value, match }) => ({ projectName: review.projectName, jurisdiction: clip(value, 200), diff --git a/backend/packet-review/report.ts b/backend/packet-review/report.ts index c264ef9b..e683fb27 100644 --- a/backend/packet-review/report.ts +++ b/backend/packet-review/report.ts @@ -168,7 +168,10 @@ export function renderPacketReport( ? "Review has unresolved coverage or inputs" : "Review in progress or incomplete"; - const storageKey = `moke:packet-report:${review.projectId}:${review.id}:v${review.version}`; + const storageKey = `second-set:packet-report:${review.projectId}:${review.id}:v${review.version}`; + + // Reports exported before the rename from MOKE saved completion under this key. + const legacyStorageKey = `moke:packet-report:${review.projectId}:${review.id}:v${review.version}`; const evidenceImage = (id: string, caption: string) => { const uri = evidence.get(id); @@ -252,9 +255,9 @@ export function renderPacketReport( ); return ` -${escape(review.projectName)} — MOKE drawing review -
-

MOKE · Drawing review

${escape(review.projectName)}

${escape(scope)}

${escape(reviewState)}
+${escape(review.projectName)} — Second Set drawing review +
+

Second Set · Drawing review

${escape(review.projectName)}

${escape(scope)}

${escape(reviewState)}

Review summary

${escape(coverageSummary)}

${paragraph(review.summary || "No review summary has been recorded.")}${review.error ? `

${escape(review.error)}

` : ""}

This report records the requested scope and evidence below. User completion tracking is separate from finding status, engineering acceptance and reviewer-verified prior-comment closure.

Request

${paragraph(review.request)}

Missing inputs

${list(review.missingInputs, "No missing inputs were recorded.")}${gaps.length ? `

Coverage requiring follow-up

${list(gaps, "")}` : ""}

${review.citations.length === 0 ? "No governing citations were recorded; regulatory conclusions remain unverified." : `${unresolvedCitations} of ${review.citations.length} citations remain unverified or incorrect.`}

Project configuration and governing sources

${ Object.keys(review.projectConfiguration).length @@ -290,7 +293,7 @@ ${findings ) .join("")}

Document inventory and review coverage

Coverage is recorded for every requested sheet, discipline and pass. Whole-set reviews account for all 11 disciplines; excluded work requires an explicit Not applicable reason. Opening a source image does not establish completion of its checks. Missing entries remain unreviewed. Compatible coverage from earlier scopes remains visible but does not settle a different discipline's checks.

${review.sheets.length ? `
${passes.map(([, title]) => ``).join("")}${coverageRows}
Document / sheet${title}
` : paragraph("No source sheets were recorded for this review.")}
-

Governing-source and citation log

${review.sources.length ? review.sources.map((source) => `

${escape(source.title || "Untitled source")}

${badge(source.status)} · ${source.official ? "Official source" : "Other source"}

${sourceLink(source.url)}

Source ID
${escape(source.id)}
Retrieved
${escape(Number.isNaN(Date.parse(source.retrievedAt)) ? source.retrievedAt : recordedAt(Date.parse(source.retrievedAt)))}
Content SHA-256
${escape(source.sha256 ?? "Not available")}
${source.status === "unavailable" ? paragraph(source.text || "Source access was unavailable.") : ""}
`).join("") : paragraph("No governing sources have been retrieved.")}${review.citations.map((citation) => `

${escape(citation.document || "Governing document needs input")} ${escape(citation.section)}

${badge(citationStandingLabels[citationStanding(citation)])} · Citation ${escape(citation.id)}

Jurisdiction
${escape(citation.jurisdiction || "Needs input")}
Edition / date
${escape(citation.edition || "Needs input")}
Retrieved source
${escape(review.sources.find((source) => source.id === citation.sourceId && source.status === "available")?.id ?? "Not available")}
${citation.reference ? `
Code section
${escape(citation.reference.title)} · ${escape(citation.reference.document)}, ${escape(citation.reference.edition)}
${sourceLink(citation.reference.url)}
` : ""}
${citationStanding(citation) === "indexed" ? paragraph("This section comes from Moke's Florida code index and its wording has not been confirmed; open the link to check it.") + (citation.reference?.check ? paragraph(citation.reference.check) : "") : `

Governing text

${citation.quote ? `
${escape(citation.quote)}
` : paragraph("No governing text was recorded.")}`}

Application to the observed condition

${paragraph(citation.application)}${citationStanding(citation) === "indexed" && !citationDisplayNote(citation) ? "" : paragraph(citationDisplayNote(citation))}
`).join("")}
+

Governing-source and citation log

${review.sources.length ? review.sources.map((source) => `

${escape(source.title || "Untitled source")}

${badge(source.status)} · ${source.official ? "Official source" : "Other source"}

${sourceLink(source.url)}

Source ID
${escape(source.id)}
Retrieved
${escape(Number.isNaN(Date.parse(source.retrievedAt)) ? source.retrievedAt : recordedAt(Date.parse(source.retrievedAt)))}
Content SHA-256
${escape(source.sha256 ?? "Not available")}
${source.status === "unavailable" ? paragraph(source.text || "Source access was unavailable.") : ""}
`).join("") : paragraph("No governing sources have been retrieved.")}${review.citations.map((citation) => `

${escape(citation.document || "Governing document needs input")} ${escape(citation.section)}

${badge(citationStandingLabels[citationStanding(citation)])} · Citation ${escape(citation.id)}

Jurisdiction
${escape(citation.jurisdiction || "Needs input")}
Edition / date
${escape(citation.edition || "Needs input")}
Retrieved source
${escape(review.sources.find((source) => source.id === citation.sourceId && source.status === "available")?.id ?? "Not available")}
${citation.reference ? `
Code section
${escape(citation.reference.title)} · ${escape(citation.reference.document)}, ${escape(citation.reference.edition)}
${sourceLink(citation.reference.url)}
` : ""}
${citationStanding(citation) === "indexed" ? paragraph("This section comes from Second Set's Florida code index and its wording has not been confirmed; open the link to check it.") + (citation.reference?.check ? paragraph(citation.reference.check) : "") : `

Governing text

${citation.quote ? `
${escape(citation.quote)}
` : paragraph("No governing text was recorded.")}`}

Application to the observed condition

${paragraph(citation.application)}${citationStanding(citation) === "indexed" && !citationDisplayNote(citation) ? "" : paragraph(citationDisplayNote(citation))}
`).join("")}

Supporting calculations

${ review.calculations.length ? review.calculations @@ -326,6 +329,7 @@ const completionScript = ` (() => { const root = document.querySelector('main'); const key = root && root.dataset.storageKey; + const legacyKey = root && root.dataset.legacyStorageKey; const controls = Array.from(document.querySelectorAll('input[data-completion-id]')); const labels = Array.from(document.querySelectorAll('[data-completion-label]')); const notice = document.getElementById('completion-storage'); @@ -345,7 +349,7 @@ const completionScript = ` } }; try { - const saved = key ? localStorage.getItem(key) : null; + const saved = key ? (localStorage.getItem(key) ?? (legacyKey ? localStorage.getItem(legacyKey) : null)) : null; if (saved) { const value = JSON.parse(saved); if (value && typeof value === 'object' && !Array.isArray(value)) { diff --git a/backend/packet-review/service.ts b/backend/packet-review/service.ts index f086b98c..4ecdc695 100644 --- a/backend/packet-review/service.ts +++ b/backend/packet-review/service.ts @@ -575,7 +575,7 @@ interface Configuration { ) => Promise; } -/** Project-first durable Moke reviews, with frozen source inventory and persistent tool records. */ +/** Project-first durable packet reviews, with frozen source inventory and persistent tool records. */ export class PacketReviews extends Context.Service< PacketReviews, { @@ -915,7 +915,7 @@ export class PacketReviews extends Context.Service< }); yield* sql`INSERT INTO review_commands(command_id,project_id,fingerprint,receipt) VALUES (${commandId},${row.project_id},${fingerprint},${JSON.stringify(receipt)})`; - yield* sql`INSERT INTO review_events(project_id,command_id,kind,message,created_at) VALUES (${row.project_id},${commandId},'packet_review','MOKE review response saved.',${message.createdAt})`; + yield* sql`INSERT INTO review_events(project_id,command_id,kind,message,created_at) VALUES (${row.project_id},${commandId},'packet_review','Second Set review response saved.',${message.createdAt})`; }); const recordTool = async ( @@ -1833,7 +1833,7 @@ export class PacketReviews extends Context.Service< ), tool( "search_code", - "Search Moke's Florida code index for the section, statute or rule that governs a check: 2023 FBC sections with plan review notes, Florida Statutes, Florida Administrative Code rules, and 9th Edition volumes. Results are pointers with links, not code text. Cite one with record_citation and its id as codeReferenceId.", + "Search Second Set's Florida code index for the section, statute or rule that governs a check: 2023 FBC sections with plan review notes, Florida Statutes, Florida Administrative Code rules, and 9th Edition volumes. Results are pointers with links, not code text. Cite one with record_citation and its id as codeReferenceId.", Schema.Struct({ query: Schema.Trim.check(Schema.isMinLength(2), Schema.isMaxLength(500)), limit: Schema.optionalKey( @@ -2857,6 +2857,7 @@ export class PacketReviews extends Context.Service< ); registry.install( defineExtension({ + // Journals may name this extension, so it keeps the name it shipped with. name: "moke-packet-review-v1", tools, hooks: [ @@ -4402,7 +4403,7 @@ export class PacketReviews extends Context.Service< if (pending.length) return yield* error( "conflict", - "MOKE is still finishing the request it sent before the pause. Its result will be kept; try Resume again in a minute.", + "Second Set is still finishing the request it sent before the pause. Its result will be kept; try Resume again in a minute.", ); }), ); @@ -4464,10 +4465,14 @@ export class PacketReviews extends Context.Service< if ( input.action === "resume" && - (row.payload.state !== "paused" || - row.engine !== journalFormat || - row.payload.promptVersion !== promptVersion) + (row.engine !== journalFormat || row.payload.promptVersion !== promptVersion) ) + return yield* error( + "incompatible", + "This review uses an earlier engine or instruction version and can't resume. Start a new review; its saved results stay readable.", + ); + + if (input.action === "resume" && row.payload.state !== "paused") return yield* error( "incompatible", "Only a compatible paused review can resume. Uncertain work must not be resent.", diff --git a/backend/packet-review/sources.ts b/backend/packet-review/sources.ts index fe817863..a6be29ef 100644 --- a/backend/packet-review/sources.ts +++ b/backend/packet-review/sources.ts @@ -170,7 +170,7 @@ function requestPublic( family: address.family, signal, headers: { - "User-Agent": "Moke/0.1 public-source-review", + "User-Agent": "second-set/0.1 public-source-review", Accept: "text/html,text/plain,application/xhtml+xml,application/xml;q=0.9,application/pdf;q=0.8,*/*;q=0.1", "Accept-Encoding": "identity", diff --git a/backend/review/code-index/README.md b/backend/review/code-index/README.md index f41fe805..1f57b0e5 100644 --- a/backend/review/code-index/README.md +++ b/backend/review/code-index/README.md @@ -1,6 +1,6 @@ # Florida code index -Moke's `search_code` tool searches this index for the code section, statute or +Second Set's `search_code` tool searches this index for the code section, statute or rule behind a review check. Each entry gives the section number and title, a one-line plan review note and a link. The index contains no copied code text. @@ -19,7 +19,7 @@ Each entry's `access` says how a reviewer reads the text it points to. - `open_in_browser`: ICC Digital Codes blocks automated clients, and its [website user agreement](https://iccsafe.org/website-user-agreement/) does not license robots or data extraction tools. Florida rules publish their text - as Word documents. Moke records these citations as unverified with the + as Word documents. Second Set records these citations as unverified with the section and link, and the engineer opens the link to confirm them. - `read_source`: Florida Statutes pages on leg.state.fl.us return their official text, so `read_source` can retrieve them and a citation can be @@ -39,7 +39,7 @@ instead. That entry describes the link whichever way a citation reaches it. ### How far the links are checked -Nobody has opened an ICC link from Moke by automation, because the user agreement +Nobody has opened an ICC link from Second Set by automation, because the user agreement excludes it. A test rebuilds each of the owner's hand-written anchors by swapping the section number in another anchor from the same chapter, the same rule the host uses for a section listed in a combined title. Every section the host links this way, and diff --git a/backend/review/execution-port.ts b/backend/review/execution-port.ts index b28c663d..c0d1b9ea 100644 --- a/backend/review/execution-port.ts +++ b/backend/review/execution-port.ts @@ -70,7 +70,7 @@ export const DetailRequest = Schema.Struct({ missingFact: Schema.String.check(Schema.isMinLength(1), Schema.isMaxLength(1000)), }); -/** Moke supplies bounded pixels and transforms for the exact resized image delivered to Muse. */ +/** Second Set supplies bounded pixels and transforms for the exact resized image delivered to Muse. */ export class ExecutionImage extends Schema.Class("ExecutionImage")({ artifactId: ArtifactId, projectId: ProjectId, diff --git a/backend/review/model-image.ts b/backend/review/model-image.ts index c05f3eea..afc8aedb 100644 --- a/backend/review/model-image.ts +++ b/backend/review/model-image.ts @@ -1,6 +1,6 @@ import { deliveredImageSize } from "#lib/review/geometry.ts"; -/** Largest decoded image Moke sends a model or SAM. */ +/** Largest decoded image Second Set sends a model or SAM. */ export const modelImageBytes = 1024 * 1024; /** diff --git a/backend/review/packages.ts b/backend/review/packages.ts index 6020237a..0b82f2d6 100644 --- a/backend/review/packages.ts +++ b/backend/review/packages.ts @@ -27,6 +27,7 @@ import { PackageRunSnapshot, RestoredRun, packageEngineFormat, + packageFormat, packageFormatVersion, } from "#lib/review/packages.ts"; import { exportDatabase } from "../database-files.ts"; @@ -1413,7 +1414,7 @@ const writeApplicationPackage = ( const createdAt = new Date(yield* Clock.currentTimeMillis).toISOString(); const manifest = new PackageManifest({ - format: "moke-review-package", + format: packageFormat, formatVersion: packageFormatVersion, scope: "application", scopeLabel: "whole-application", diff --git a/backend/review/prompts/README.md b/backend/review/prompts/README.md index 33d2d685..9bf4c2c3 100644 --- a/backend/review/prompts/README.md +++ b/backend/review/prompts/README.md @@ -1,11 +1,11 @@ -# Moke review instructions +# Review instructions `assets/` contains unmodified files from the author's private review-instruction repository at revision `8a0e2648738018631eaa6d24ee0b6ebaa239afb1`. `provenance.json` records the upstream paths and SHA-256 hashes. This application does not edit that source. `review-prompt.ts` imports the files with Bun's text loader, embedding them in the -compiled backend. Packet review version `moke-packet-review-v10` supplies the full +compiled backend. Packet review version `second-set-packet-review-v11` supplies the full master AGENTS.md and REVIEW_METHOD.md, including Report and release, as Pi Durable's conversation `agent.instructions`. The reviewer request and document inventory are separate input data. Each run records the prompt version and @@ -94,7 +94,7 @@ a compatible paused review and can extend its optional call budget. Follow-up requests in the same conversation carry prior records and stable issue IDs. Copied package journals remain historical and cannot resume. -The earlier `moke-selected-coordination-v2` adapter remains for existing +The earlier `second-set-selected-coordination-v3` adapter remains for existing selected-count executions. Its narrower tools, limits and scope apply to that specialized protocol. Packet review uses the full system instructions and its own project/conversation root. Both paths preserve reviewer acceptance of @@ -134,3 +134,10 @@ to update by id or mark `Resolved`, and `withdraw_finding` removes a finding rec the same review. A finding that calls an area blank, empty or illegible must cite a focused crop of it, because full-sheet images are too coarse for small text. The prompt adds short severity definitions and forbids test findings. + +Packet review `second-set-packet-review-v11` and count prompt +`second-set-selected-coordination-v3` change only the application's name, from MOKE to +Second Set, in the opening line and the `search_code` description. Versions through v10 +shipped as `moke-packet-review-vN`; migration 0014 renamed saved records to +`second-set-packet-review-vN`, and workers built before the rename still announce the +older names. diff --git a/backend/review/prompts/provenance.json b/backend/review/prompts/provenance.json index b0ec1013..e9cc3fc2 100644 --- a/backend/review/prompts/provenance.json +++ b/backend/review/prompts/provenance.json @@ -2,7 +2,7 @@ "repository": "Author's private review-instruction repository", "revision": "8a0e2648738018631eaa6d24ee0b6ebaa239afb1", "source": "moke-review", - "promptVersion": "moke-selected-coordination-v2", + "promptVersion": "second-set-selected-coordination-v3", "files": [ { "path": "SKILL.md", @@ -75,5 +75,5 @@ "sha256": "b30491a08e7fafbb9c75501c4bc8fc1c01d8f25401c4a6b0bf7baaf95e0d95e4" } ], - "packetPromptVersion": "moke-packet-review-v10" + "packetPromptVersion": "second-set-packet-review-v11" } diff --git a/backend/review/prompts/review-prompt.ts b/backend/review/prompts/review-prompt.ts index aea5c7e1..e4134c67 100644 --- a/backend/review/prompts/review-prompt.ts +++ b/backend/review/prompts/review-prompt.ts @@ -23,7 +23,7 @@ import { findingSummaryInstructions } from "#lib/review/finding-summary.ts"; import { routeReviewDiscipline } from "#lib/review/execution.ts"; /** Version of the bounded runtime adaptation; upstream files are vendored unmodified. */ -export const reviewPromptVersion = "moke-selected-coordination-v2"; +export const reviewPromptVersion = "second-set-selected-coordination-v3"; /** Exact upstream revision retained with runs and proposed observations. */ export const reviewInstructionRevision = provenance.revision; @@ -42,7 +42,7 @@ const disciplines: Record = { fire_protection: fireProtection, }; -/** Adapt Moke to the selected-count engine without promising unavailable checks. */ +/** Adapt the vendored review instructions to the selected-count engine without promising unavailable checks. */ export function selectedReviewPrompt(context: typeof ExecutionReviewContext.Type | null): string { const discipline = context?.request.discipline === "auto" @@ -50,7 +50,7 @@ export function selectedReviewPrompt(context: typeof ExecutionReviewContext.Type : (context?.request.discipline ?? "general"); return [ - `Moke instruction ${reviewPromptVersion}; skills revision ${reviewInstructionRevision}.`, + `Second Set instruction ${reviewPromptVersion}; skills revision ${reviewInstructionRevision}.`, master, // Report rendering and browser-local completion belong to the application. method.split("## Report and release")[0], @@ -74,7 +74,7 @@ export function selectedReviewPrompt(context: typeof ExecutionReviewContext.Type } /** Version of the project-first system prompt used for new packet reviews. */ -export const packetPromptVersion = "moke-packet-review-v10"; +export const packetPromptVersion = "second-set-packet-review-v11"; /** * A review runs as one reviewer, or as a coordinator plus one reviewer per sheet. Large packets @@ -130,7 +130,7 @@ const roleGuidance: Record> = { ], }; -/** Full Moke system instructions for project-first review; original source files stay intact. */ +/** Full vendored system instructions for project-first review; original source files stay intact. */ export function packetReviewPrompt( scope: import("../../../src/lib/review/packet.ts").PacketReview["scope"], options: { @@ -144,7 +144,7 @@ export function packetReviewPrompt( }, ): string { return [ - `Moke instruction ${packetPromptVersion}; skills revision ${reviewInstructionRevision}.`, + `Second Set instruction ${packetPromptVersion}; skills revision ${reviewInstructionRevision}.`, master, method, general, diff --git a/backend/segmentation/inventory-report.ts b/backend/segmentation/inventory-report.ts index f488444b..1dc76a42 100644 --- a/backend/segmentation/inventory-report.ts +++ b/backend/segmentation/inventory-report.ts @@ -66,25 +66,36 @@ export function inventoryReport( ) .join(""); - const key = JSON.stringify(`moke-fixture-reconciliation:${revision}`).replaceAll("<", "\\u003c"); + const key = JSON.stringify(`second-set-fixture-reconciliation:${revision}`).replaceAll( + "<", + "\\u003c", + ); + + // Worksheets generated before the rename from MOKE saved selections under this key. + const legacyKey = JSON.stringify(`moke-fixture-reconciliation:${revision}`).replaceAll( + "<", + "\\u003c", + ); + const reportRevision = JSON.stringify(revision).replaceAll("<", "\\u003c"); const candidates = JSON.stringify(fixtures).replaceAll("<", "\\u003c"); - return `MOKE plumbing fixture reconciliation

MOKE · Plumbing fixture reconciliation

Schedule versus ${evidence.length} selected plan regions.

Experimental coordination worksheet. Muse supplies candidate classes and locations; SAM supplies proposed masks. Counts include existing and unclear-scope fixtures. The whole sheet, risers, required quantities, and code compliance have not been reviewed. Matching totals do not confirm fixture identity or a complete inventory.

Schedule and scoped inventory

${totals}
MarkFixtureSchedule quantityIncluded proposalsDeltaCoordination status

Only WC and LAV were tested. Other schedule rows remain untested. A differing count calls for inspection, not an automatic drawing-conflict finding.

Review and correct

Inspect each mask against the original drawing. Uncheck a mistaken proposal to recalculate its quantity and delta. These controls do not change the source drawings or saved model responses.

${inventory}
Count proposalMark from MuseRoom from MuseScope from MuseEvidenceState
${tasks}

Completed records your task tracking. It does not verify a finding or establish compliance. Selections are stored only in this browser for this report revision; they do not travel with a copied HTML file.

Plan evidence

Muse candidate boxes; SAM masks hidden.

Mask geometry can have holes or partial tanks. Confirm the intended instance and boundary; model confidence is not measured accuracy.

${images}

Schedule evidence

Supplied plumbing fixture schedule

The schedule was read separately, without supplying its counts to the plan-grounding requests.

Coverage and source status

Checked inputs: ${evidence.length} named plan crops and the supplied fixture schedule crop. WC/LAV count coordination is proposed and pending visual review. Other classes, sheet completeness, exact marks, new/removed scope, architecture, risers, prior comments, and governing requirements need further input. Project identity, jurisdiction, and permit stage were anonymized or excluded.

No regulatory finding or verified prior-comment closure is reported. Basis: MOKE plumbing instructions at sc-frederick/skills revision 8a0e2648738018631eaa6d24ee0b6ebaa239afb1. Report revision: ${Bun.escapeHTML(revision)}.

`; } diff --git a/backend/segmentation/meta-review.ts b/backend/segmentation/meta-review.ts index 744d1099..f95c116d 100644 --- a/backend/segmentation/meta-review.ts +++ b/backend/segmentation/meta-review.ts @@ -170,7 +170,7 @@ export class MetaReviewClient extends Context.Service< readonly results: ReadonlyArray; }) => Effect.Effect; } ->()("moke/MetaReviewClient") { +>()("second-set/MetaReviewClient") { /** Build a no-retry adapter with durable local request/response evidence and a call ceiling. */ static layer(config: typeof MetaReviewConfig.Type, directory: string, maximumRequests: number) { return Layer.effect( diff --git a/backend/segmentation/report.ts b/backend/segmentation/report.ts index adde3e6d..1ed69f4e 100644 --- a/backend/segmentation/report.ts +++ b/backend/segmentation/report.ts @@ -79,5 +79,5 @@ export function reportHtml( }) .join(""); - return `MOKE SAM 3.1 experiment

MOKE · SAM 3.1 segmentation experiment

Model: ${Bun.escapeHTML(model)}. Saved local run. Each comparison uses identical input pixels.

${results.length} of ${plannedCount} selected cases have saved outcomes. ${plannedCount - results.length} not yet attempted.

Inspect masks against the drawing. Confidence is the model's score, not measured accuracy. Zero matches is a completed result, not proof of absence. No engineering rule, physical measurement, or revision conclusion is inferred here.

Human labels, correctness metrics, and cost are not yet available. Request bodies contain image crops; provider preprocessing remains outside our control. PDF coordinates use a top-left origin, 72 points per inch, with half-open right/bottom edges.

${cards}`; + return `Second Set SAM 3.1 experiment

Second Set · SAM 3.1 segmentation experiment

Model: ${Bun.escapeHTML(model)}. Saved local run. Each comparison uses identical input pixels.

${results.length} of ${plannedCount} selected cases have saved outcomes. ${plannedCount - results.length} not yet attempted.

Inspect masks against the drawing. Confidence is the model's score, not measured accuracy. Zero matches is a completed result, not proof of absence. No engineering rule, physical measurement, or revision conclusion is inferred here.

Human labels, correctness metrics, and cost are not yet available. Request bodies contain image crops; provider preprocessing remains outside our control. PDF coordinates use a top-left origin, 72 points per inch, with half-open right/bottom edges.

${cards}`; } diff --git a/backend/segmentation/sam.ts b/backend/segmentation/sam.ts index 1d0d167b..ddfd3797 100644 --- a/backend/segmentation/sam.ts +++ b/backend/segmentation/sam.ts @@ -98,7 +98,7 @@ export class SamClient extends Context.Service< image: Uint8Array, ) => Effect.Effect; } ->()("moke/SamClient") { +>()("second-set/SamClient") { /** Build the fetch adapter without automatic retries of paid requests. */ static layer(config: typeof SamConfig.Type) { return Layer.succeed( diff --git a/bun.lock b/bun.lock index 1782f9c3..7801b01d 100644 --- a/bun.lock +++ b/bun.lock @@ -3,7 +3,7 @@ "configVersion": 1, "workspaces": { "": { - "name": "moke-meta-hackathon", + "name": "second-set", "dependencies": { "effect": "4.1.1", "electron-effect-rpc": "1.0.1", diff --git a/desktop/connection-check.ts b/desktop/connection-check.ts index 15cbe6ec..487beffe 100644 --- a/desktop/connection-check.ts +++ b/desktop/connection-check.ts @@ -8,7 +8,7 @@ export type EndpointCheck = | { readonly kind: "unreachable" } | { readonly kind: "unexpected" }; -const checkTool = "moke_connection_check"; +const checkTool = "second_set_connection_check"; /** Only the fields the check reads; everything else in the answer is ignored. */ const Completion = Schema.Struct({ @@ -122,8 +122,8 @@ export const checkConnection = Effect.fn("checkConnection")(function* (endpoint: */ export function openCodeHeaders(version: string, session: string) { return { - "user-agent": `moke/${version}`, - "x-opencode-client": "moke", + "user-agent": `second-set/${version}`, + "x-opencode-client": "second-set", "x-opencode-session": session, }; } diff --git a/desktop/credentials.ts b/desktop/credentials.ts index 8e691fec..e70180ec 100644 --- a/desktop/credentials.ts +++ b/desktop/credentials.ts @@ -2,7 +2,7 @@ import { safeStorage } from "electron"; import { Schema } from "effect"; import { existsSync } from "node:fs"; import { readFile, rm, writeFile } from "node:fs/promises"; -import { join } from "node:path"; +import { basename, join } from "node:path"; import { InferenceConfiguration } from "../src/lib/backend-rpc.ts"; import { reviewProviderRoutes } from "../src/lib/review/models.ts"; import { @@ -68,6 +68,43 @@ async function readStored(path: string, message: string) { } } +/** A saved credential file the app could not read; it starts without that connection. */ +export type UnreadableCredential = { + readonly file: string; + readonly reason: "locked" | "mismatched" | "unavailable"; + readonly message: string; +}; + +/** + * Decrypt a saved credential, or record why it can't be read and treat it as missing. A key saved + * before the rename can stop unlocking, and one stale file must not hide the others. + */ +async function readStoredOrSkip( + path: string, + message: string, + unreadable: Array, +) { + if (!(await secureStorageAvailable())) { + unreadable.push({ + file: basename(path), + reason: "unavailable", + message: + "Secure credential storage is unavailable. Select a session-only credential configuration.", + }); + + return null; + } + + try { + return await readStored(path, message); + } catch (cause) { + if (!(cause instanceof CredentialError)) throw cause; + unreadable.push({ file: basename(path), reason: "locked", message }); + + return null; + } +} + /** Encrypt with the OS store and write an owner-only file; never falls back to plain text. */ async function writeStored(path: string, plain: string) { if (!(await secureStorageAvailable())) @@ -85,17 +122,26 @@ async function writeStored(path: string, plain: string) { } /** Session file, imported credentials, or (in development only) the environment. */ -async function readImportedConfigurations(directory: string, packaged: boolean) { - const sessionFile = process.env.MOKE_SESSION_CREDENTIAL_FILE; +async function readImportedConfigurations( + directory: string, + packaged: boolean, + unreadable: Array, +) { + const sessionFile = process.env.SECOND_SET_SESSION_CREDENTIAL_FILE; if (sessionFile !== undefined) return readProviderConfigurations(sessionFile, true); const storedFile = join(directory, importedFile); - if (existsSync(storedFile)) - return parseProviderConfigurations( - await readStored(storedFile, "The saved provider credential could not be unlocked."), + if (existsSync(storedFile)) { + const text = await readStoredOrSkip( + storedFile, + "The saved provider credential could not be unlocked.", + unreadable, ); + if (text !== null) return parseProviderConfigurations(text); + } + if (packaged) return []; const keys = { @@ -106,7 +152,8 @@ async function readImportedConfigurations(directory: string, packaged: boolean) const order = (["meta", "baseten", "opencode-go"] as const).toSorted( (left, right) => - Number(right === process.env.MOKE_PROVIDER) - Number(left === process.env.MOKE_PROVIDER), + Number(right === process.env.SECOND_SET_PROVIDER) - + Number(left === process.env.SECOND_SET_PROVIDER), ); return order.flatMap((entry) => { @@ -128,32 +175,63 @@ async function readImportedConfigurations(directory: string, packaged: boolean) /** * Packaged launches use OS-encrypted storage or an explicitly selected session-only file. * Connections saved in Review settings replace the same provider from those sources and are listed - * after them, so Meta or Baseten stays the startup default when present. + * after them, so Meta or Baseten stays the startup default when present. A file that can't be read + * is reported and skipped, so the others still load and it can be saved again. */ export async function readInferenceConfigurations(directory: string, packaged: boolean) { - let configs = await readImportedConfigurations(directory, packaged); + const unreadable: Array = []; + let configurations = await readImportedConfigurations(directory, packaged, unreadable); // A session-only launch must still start when the keyring is down. - if (process.env.MOKE_SESSION_CREDENTIAL_FILE !== undefined && !(await secureStorageAvailable())) - return configs; + if ( + process.env.SECOND_SET_SESSION_CREDENTIAL_FILE !== undefined && + !(await secureStorageAvailable()) + ) + return { configurations, unreadable }; for (const [provider, file] of Object.entries(settingsFiles)) { const stored = join(directory, file); if (!existsSync(stored)) continue; - const saved = parseProviderConfiguration( - await readStored(stored, "A connection saved in Review settings could not be unlocked."), + const text = await readStoredOrSkip( + stored, + "A connection saved in Review settings could not be unlocked.", + unreadable, ); - if (saved.provider !== provider) - throw new CredentialError({ + if (text === null) continue; + const saved = parseProviderConfiguration(text); + + if (saved.provider !== provider) { + unreadable.push({ + file, + reason: "mismatched", message: "A connection saved in Review settings is for another provider.", }); - configs = [...configs.filter((entry) => entry.provider !== provider), saved]; + continue; + } + + configurations = [...configurations.filter((entry) => entry.provider !== provider), saved]; } - return configs; + return { configurations, unreadable }; +} + +/** + * Delete saved copies that no longer unlock, so a key replaced only in Review settings stops + * prompting at every launch. They can't be decrypted, so nothing readable is lost; files skipped + * because storage itself is unavailable are kept. + */ +export async function forgetUnreadableCredentials( + directory: string, + unreadable: ReadonlyArray, +) { + const known = new Set([importedFile, ...Object.values(settingsFiles)]); + + for (const entry of unreadable) + if (entry.reason === "locked" && known.has(entry.file)) + await rm(join(directory, entry.file), { force: true }); } /** Save a Review settings connection; the next launch passes it to the backend. */ @@ -186,10 +264,16 @@ export async function removeSettingsConnection(directory: string, provider: Sett if (!existsSync(imported)) return; - const configs = parseProviderConfigurations( - await readStored(imported, "The saved provider credential could not be unlocked."), + // A copy that no longer unlocks has nothing to remove; it is replaced when a key is imported. + const text = await readStoredOrSkip( + imported, + "The saved provider credential could not be unlocked.", + [], ); + if (text === null) return; + const configs = parseProviderConfigurations(text); + const kept = configs.filter((entry) => entry.provider !== provider); if (kept.length === configs.length) return; @@ -211,7 +295,8 @@ export async function storeInferenceConfiguration(directory: string, source: str const imported = incoming.filter((entry) => !isSettingsProvider(entry.provider)); if (imported.length > 0) { - const existing = await readImportedConfigurations(directory, true); + // An earlier copy that no longer unlocks is replaced rather than blocking the import. + const existing = await readImportedConfigurations(directory, true, []); const configs = [ ...imported, diff --git a/desktop/data-directory.ts b/desktop/data-directory.ts new file mode 100644 index 00000000..aab4232b --- /dev/null +++ b/desktop/data-directory.ts @@ -0,0 +1,21 @@ +import { existsSync } from "node:fs"; +import { join } from "node:path"; + +/** Releases through 0.2.2 shipped as moke-meta-hackathon and kept their data under that name. */ +export const legacyDataFolder = "moke-meta-hackathon"; + +/** + * The userData directory to keep using after the Second Set rename, or null for Electron's own. + * A workspace saved under the old name stays in place until the new location holds a database. + */ +export function legacyUserData( + appData: string, + userData: string, + exists: (path: string) => boolean = existsSync, +): string | null { + const legacy = join(appData, legacyDataFolder); + + return !exists(join(userData, "app.sqlite")) && exists(join(legacy, "app.sqlite")) + ? legacy + : null; +} diff --git a/desktop/icons/README.md b/desktop/icons/README.md index 16248fda..665cbff5 100644 --- a/desktop/icons/README.md +++ b/desktop/icons/README.md @@ -1,10 +1,10 @@ # Application icon -The icon draws the brand M as walls in plan: a Bone outline over a hatched cut, -dimensioned above, on a Filament drafting grid. An Ember revision cloud, the -scalloped outline reviewers draw around a questioned detail, circles the right -foot as a finding. It follows Forge: flat Ink surface, hairline structure and -Ember as the single accent, placed where Forge's cover sets its Ember cursor. +The icon draws a drawing sheet over the set behind it: a Bone sheet with a hatched +title-block strip and a floor plan, dimensioned below, in front of a Steel sheet on a +Filament drafting grid. An Ember revision cloud, the scalloped outline reviewers draw +around a questioned detail, circles a room corner as a finding. It follows Forge: flat +Ink surface, hairline structure and Ember as the single accent. | File | Use | | --------------------- | --------------------------------------------------------------- | @@ -15,18 +15,8 @@ Ember as the single accent, placed where Forge's cover sets its Ember cursor. | `icon.ico` | Windows executable and installer icon, 16 through 256 px | The ICNS file is prebuilt because Electron Builder's PNG-to-ICNS converter -hangs under Bun. Edit the SVG sources, then regenerate the raster files with -librsvg, ImageMagick and Bun: +hangs under Bun. Edit the SVG sources, then regenerate every raster file with resvg: ```sh -rsvg-convert -w 1024 -h 1024 desktop/icons/icon.svg -o desktop/icons/icon.png -mkdir -p /tmp/moke-icns -for size in 16 32 64 128 256 512 1024; do - rsvg-convert -w $size -h $size desktop/icons/icon.svg -o /tmp/moke-icns/$size.png -done -bun scripts/build-icns.ts /tmp/moke-icns desktop/icons/icon.icns -for size in 16 24 32 48 64 128 256; do - rsvg-convert -w $size -h $size desktop/icons/icon-full-bleed.svg -o /tmp/moke-icon-$size.png -done -magick /tmp/moke-icon-{16,24,32,48,64,128,256}.png desktop/icons/icon.ico +bun scripts/build-icons.ts ``` diff --git a/desktop/icons/icon-full-bleed.svg b/desktop/icons/icon-full-bleed.svg index 3a34ebfc..6506f104 100644 --- a/desktop/icons/icon-full-bleed.svg +++ b/desktop/icons/icon-full-bleed.svg @@ -1,20 +1,26 @@ - + - + - - - - - + + + + + + + + + + + diff --git a/desktop/icons/icon.icns b/desktop/icons/icon.icns index 79c13f98..35a0b893 100644 Binary files a/desktop/icons/icon.icns and b/desktop/icons/icon.icns differ diff --git a/desktop/icons/icon.ico b/desktop/icons/icon.ico index 9e9f0c93..975c1036 100644 Binary files a/desktop/icons/icon.ico and b/desktop/icons/icon.ico differ diff --git a/desktop/icons/icon.png b/desktop/icons/icon.png index 2623940e..087a0027 100644 Binary files a/desktop/icons/icon.png and b/desktop/icons/icon.png differ diff --git a/desktop/icons/icon.svg b/desktop/icons/icon.svg index 972ec15d..ad245cff 100644 --- a/desktop/icons/icon.svg +++ b/desktop/icons/icon.svg @@ -1,22 +1,27 @@ - + - + - - - - - - + + + + + + + + + + + diff --git a/desktop/main.ts b/desktop/main.ts index 618170c5..66f72545 100644 --- a/desktop/main.ts +++ b/desktop/main.ts @@ -24,6 +24,7 @@ import { reviewProviderRoutes } from "../src/lib/review/models.ts"; import { BackendApi, type BackendLaunch } from "./backend.ts"; import { ReviewProxy, ExecutionProxy, PacketProxy, CloudReviewProxy } from "./review-proxy.ts"; import { + forgetUnreadableCredentials, readInferenceConfigurations, removeSettingsConnection, type SettingsProvider, @@ -34,6 +35,7 @@ import { CredentialError, customEndpointUrl } from "./credential-files.ts"; import { checkConnection, describeEndpointCheck, openCodeHeaders } from "./connection-check.ts"; import { readModalConfiguration } from "./modal-credentials.ts"; import { createDropImports } from "./drop-imports.ts"; +import { legacyUserData } from "./data-directory.ts"; protocol.registerSchemesAsPrivileged([ { @@ -59,6 +61,16 @@ if ( throw new Error("Desktop development requires a loopback Vite URL."); } +// The single-instance lock lives in userData, so choose it first. Explicit locations win. +if ( + !app.commandLine.hasSwitch("user-data-dir") && + process.env.PROJECT_KIT_DATA_DIRECTORY === undefined +) { + const legacy = legacyUserData(app.getPath("appData"), app.getPath("userData")); + + if (legacy !== null) app.setPath("userData", legacy); +} + if (!app.requestSingleInstanceLock()) { app.quit(); } else { @@ -98,11 +110,56 @@ if (!app.requestSingleInstanceLock()) { ), )(await readFile(join(assetDirectory, "layout.json"), "utf8")); - const [inference, ...inferenceAlternatives] = await readInferenceConfigurations( - dataDirectory, - app.isPackaged, + // macOS and Linux store the credential key under the app's name, so 0.2.2 keys can stop + // unlocking after the rename. Start with the keys that still read, and say how to restore + // the others rather than exit. + const credentials = await readInferenceConfigurations(dataDirectory, app.isPackaged).catch( + async (cause: unknown) => { + if (!(cause instanceof CredentialError)) throw cause; + + await dialog.showMessageBox({ + type: "warning", + message: "Saved provider keys could not be read", + detail: `${cause.message} Model reviews stay unavailable until the credential configuration is fixed.`, + }); + + return { configurations: [], unreadable: [] }; + }, ); + if (credentials.unreadable.length > 0) { + const locked = credentials.unreadable.some((entry) => entry.reason === "locked"); + const usable = credentials.unreadable.some((entry) => entry.reason !== "unavailable"); + + const messages = [...new Set(credentials.unreadable.map((entry) => entry.message))].join( + " ", + ); + + const choice = await dialog.showMessageBox({ + type: "warning", + message: locked + ? "A saved provider key could not be unlocked" + : usable + ? "A saved provider connection can't be used" + : "Secure credential storage is unavailable", + detail: usable + ? `${messages} Reviews that need it stay unavailable until you enter the key again in Review settings or import it from the File menu.${locked ? " This can happen once after updating from MOKE 0.2.2 or earlier on macOS or Linux. Forget these keys deletes only the saved copies that can't be unlocked." : ""}` + : "Saved provider keys can't be read on this computer. Select a session-only credential configuration to run model reviews.", + buttons: locked ? ["OK", "Forget these keys"] : ["OK"], + defaultId: 0, + cancelId: 0, + noLink: true, + }); + + // A file that can't be deleted now just prompts again next launch. + if (locked && choice.response === 1) + await forgetUnreadableCredentials(dataDirectory, credentials.unreadable).catch( + (cause: unknown) => console.warn("Saved credentials could not be forgotten", cause), + ); + } + + const [inference, ...inferenceAlternatives] = credentials.configurations; + const modal = await Effect.runPromise(readModalConfiguration(app.isPackaged)); const launchOptions: BackendLaunch = { @@ -149,7 +206,7 @@ if (!app.requestSingleInstanceLock()) { } const drops = createDropImports(); - ipcMain.handle("moke:stage-pdf", async (event, value) => { + ipcMain.handle("second-set:stage-pdf", async (event, value) => { if ( !event.senderFrame || event.senderFrame !== event.sender.mainFrame || @@ -193,7 +250,7 @@ if (!app.requestSingleInstanceLock()) { cancelId: 0, message: `Remove ${name}?`, detail: - "Its saved key is deleted and MOKE restarts. Reviews already run on it keep their results.", + "Its saved key is deleted and Second Set restarts. Reviews already run on it keep their results.", }), catch: () => new ReviewStorageError({ message: "The remove confirmation could not open." }), @@ -271,8 +328,8 @@ if (!app.requestSingleInstanceLock()) { Effect.tryPromise({ try: () => dialog.showSaveDialog({ - title: "Save MOKE review report", - defaultPath: "MOKE-review.html", + title: "Save Second Set review report", + defaultPath: "Second-Set-review.html", filters: [{ name: "HTML report", extensions: ["html"] }], }), catch: () => @@ -372,7 +429,7 @@ if (!app.requestSingleInstanceLock()) { const destination = join( parent, - `MOKE-review-${new Date().toISOString().replaceAll(":", "-")}`, + `Second-Set-review-${new Date().toISOString().replaceAll(":", "-")}`, ); return api.ExportReviewPackage({ destination }).pipe( @@ -392,7 +449,7 @@ if (!app.requestSingleInstanceLock()) { const selection = yield* Effect.tryPromise({ try: () => dialog.showOpenDialog({ - title: "Choose a complete MOKE review package", + title: "Choose a complete Second Set review package", properties: ["openDirectory"], }), catch: () => @@ -468,7 +525,7 @@ if (!app.requestSingleInstanceLock()) { defaultId: check.kind === "ready" ? 1 : 0, cancelId: 0, message: "Save this custom endpoint?", - detail: `${describeEndpointCheck(check)} MOKE restarts to connect it. The key is encrypted on this computer and is used only for reviews you run here.`, + detail: `${describeEndpointCheck(check)} Second Set restarts to connect it. The key is encrypted on this computer and is used only for reviews you run here.`, }), catch: () => new ReviewStorageError({ message: "The save confirmation could not open." }), @@ -508,7 +565,7 @@ if (!app.requestSingleInstanceLock()) { baseUrl: reviewProviderRoutes["opencode-go"].endpoint, model: upstream, apiKey: settings.apiKey, - headers: openCodeHeaders(app.getVersion(), `moke-check-${randomUUID()}`), + headers: openCodeHeaders(app.getVersion(), `second-set-check-${randomUUID()}`), }); const confirmation = yield* Effect.tryPromise({ @@ -519,7 +576,7 @@ if (!app.requestSingleInstanceLock()) { defaultId: check.kind === "ready" ? 1 : 0, cancelId: 0, message: "Save this OpenCode Go key?", - detail: `${describeEndpointCheck(check, "opencode-go")} Tested with ${upstream}. MOKE restarts to connect it. The key is encrypted on this computer and is used only for reviews you run here.`, + detail: `${describeEndpointCheck(check, "opencode-go")} Tested with ${upstream}. Second Set restarts to connect it. The key is encrypted on this computer and is used only for reviews you run here.`, }), catch: () => new ReviewStorageError({ message: "The save confirmation could not open." }), @@ -661,7 +718,7 @@ if (!app.requestSingleInstanceLock()) { .ExportReviewPackage({ destination: join( selection.filePaths[0], - `MOKE-review-${new Date().toISOString().replaceAll(":", "-")}`, + `Second-Set-review-${new Date().toISOString().replaceAll(":", "-")}`, ), }) .pipe(Effect.asVoid), diff --git a/desktop/modal-credentials.ts b/desktop/modal-credentials.ts index e0db13e3..160a3f75 100644 --- a/desktop/modal-credentials.ts +++ b/desktop/modal-credentials.ts @@ -7,7 +7,7 @@ import { CredentialError } from "./credential-files.ts"; export const readModalConfiguration = Effect.fn("readModalConfiguration")(function* ( packaged: boolean, ) { - const sessionFile = process.env.MOKE_MODAL_CONFIG; + const sessionFile = process.env.SECOND_SET_MODAL_CONFIG; if (sessionFile !== undefined) { const text = yield* Effect.tryPromise({ @@ -55,11 +55,11 @@ export const readModalConfiguration = Effect.fn("readModalConfiguration")(functi return yield* Schema.decodeUnknownEffect(ModalConfiguration)({ tokenId, tokenSecret, - appName: process.env.MOKE_MODAL_APP ?? "moke-reviews", + appName: process.env.SECOND_SET_MODAL_APP ?? "second-set-reviews", environment: process.env.MODAL_ENVIRONMENT ?? "main", - imageName: process.env.MOKE_MODAL_IMAGE ?? "moke-review-engine:latest", - timeoutMinutes: Number(process.env.MOKE_MODAL_TIMEOUT_MINUTES ?? "15"), - poolQueue: process.env.MOKE_MODAL_POOL_QUEUE ?? null, + imageName: process.env.SECOND_SET_MODAL_IMAGE ?? "second-set-review-engine:latest", + timeoutMinutes: Number(process.env.SECOND_SET_MODAL_TIMEOUT_MINUTES ?? "15"), + poolQueue: process.env.SECOND_SET_MODAL_POOL_QUEUE ?? null, }).pipe( Effect.mapError(() => new CredentialError({ message: "The Modal configuration is invalid." })), ); diff --git a/desktop/preload.ts b/desktop/preload.ts index 07c638f5..1667f4ae 100644 --- a/desktop/preload.ts +++ b/desktop/preload.ts @@ -3,6 +3,7 @@ import { exposeRpcBridge } from "electron-effect-rpc/preload"; exposeRpcBridge({ endpoints: ["default"] }); -contextBridge.exposeInMainWorld("mokeFiles", { - stageDrop: (file: File) => ipcRenderer.invoke("moke:stage-pdf", webUtils.getPathForFile(file)), +contextBridge.exposeInMainWorld("secondSetFiles", { + stageDrop: (file: File) => + ipcRenderer.invoke("second-set:stage-pdf", webUtils.getPathForFile(file)), }); diff --git a/docs/README.md b/docs/README.md index f3755bda..2c3cba41 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,6 +1,6 @@ # Project documentation -Start with [Moke's README](../README.md) for the product, workflow, and screenshots. +Start with [the Second Set README](../README.md) for the product, workflow, and screenshots. ## Setup and development diff --git a/docs/architecture/pi-runtime.md b/docs/architecture/pi-runtime.md index ca573d37..dfee953a 100644 --- a/docs/architecture/pi-runtime.md +++ b/docs/architecture/pi-runtime.md @@ -74,7 +74,7 @@ permit. The inspect tool delivers up to two full selected-sheet overview images, rendering missing overviews through that port. Targeted render delivers the newly rendered image. Both return actual SDK image blocks and text metadata with exact -delivered dimensions/transforms. Moke owns resizing and provenance. +delivered dimensions/transforms. Second Set owns resizing and provenance. ## Persistence and admission diff --git a/docs/architecture/review-packages.md b/docs/architecture/review-packages.md index e2aaf178..a26a213b 100644 --- a/docs/architecture/review-packages.md +++ b/docs/architecture/review-packages.md @@ -6,7 +6,7 @@ Primary demo target: Linux x64. The writer uses same-filesystem rename, director ## Package directory -Format name `moke-review-package`, format version `1`, hash `sha256`, engine `review-pi-1.4.0-v1`. The only scope this writer produces is `application` with scope label `whole-application`. That means the whole local application database plus the originals, renders, and closed Pi journals that database references. +Format name `second-set-review-package`, format version `1`, hash `sha256`, engine `review-pi-1.4.0-v1`. Packages exported before the rename from MOKE name `moke-review-package` and restore unchanged. The only scope this writer produces is `application` with scope label `whole-application`. That means the whole local application database plus the originals, renders, and closed Pi journals that database references. ```text manifest.json diff --git a/docs/cloud-reviews.md b/docs/cloud-reviews.md index eeac982f..18860dcb 100644 --- a/docs/cloud-reviews.md +++ b/docs/cloud-reviews.md @@ -3,7 +3,7 @@ The workbench calls this **Push to cloud**. A reviewer chooses it beside the message composer, chooses a whole-set or discipline review, and sends their instructions. The same cloud protocol also supports selected count-check requests. -Moke transfers the workspace, runs a new bounded review away from the computer, +Second Set transfers the workspace, runs a new bounded review away from the computer, and automatically syncs verified results back when the review finishes. The infrastructure provider is Modal; that name belongs in operator setup rather than the review controls. @@ -17,9 +17,9 @@ or incoming HTTP tunnel. Configure the existing model connection first; see [inference setup](inference.md). Create a named Secret in the [Modal Secrets dashboard](https://modal.com/secrets) -with the key `MOKE_API_KEY` holding that provider's API key. Its name is -`--api`: the defaults are `moke-reviews-meta-api`, -`moke-reviews-baseten-api` and `moke-reviews-opencode-go-api`. The current JS SDK can reference named Secrets but +with the key `SECOND_SET_API_KEY` holding that provider's API key. Its name is +`--api`: the defaults are `second-set-reviews-meta-api`, +`second-set-reviews-baseten-api` and `second-set-reviews-opencode-go-api`. The current JS SDK can reference named Secrets but cannot create them; use the dashboard or Modal's admin CLI for that one-time step. The application and sandbox workloads remain TypeScript on Bun. @@ -29,11 +29,11 @@ For development, put these additional values in the local, ignored `.env`: MODAL_TOKEN_ID=... MODAL_TOKEN_SECRET=... MODAL_ENVIRONMENT=main -MOKE_MODAL_APP=moke-reviews -MOKE_MODAL_IMAGE=moke-review-engine:latest -MOKE_MODAL_TIMEOUT_MINUTES=15 +SECOND_SET_MODAL_APP=second-set-reviews +SECOND_SET_MODAL_IMAGE=second-set-review-engine:latest +SECOND_SET_MODAL_TIMEOUT_MINUTES=15 # Optional; omit to avoid paying for unused warm capacity: -# MOKE_MODAL_POOL_QUEUE=moke-review-pool +# SECOND_SET_MODAL_POOL_QUEUE=second-set-review-pool ``` Publish the worker image from the current checkout: @@ -50,16 +50,16 @@ local data, `.env`, logs, and credentials are not copied into the image. Publish matching image again when changing review engine code or migrations. Keep an immutable image tag per deployed desktop version when distributing builds. -Packaged launches use `MOKE_MODAL_CONFIG` pointing to a private JSON file. They +Packaged launches use `SECOND_SET_MODAL_CONFIG` pointing to a private JSON file. They do not read these credentials automatically from `.env`. The file contains: ```json { "tokenId": "...", "tokenSecret": "...", - "appName": "moke-reviews", + "appName": "second-set-reviews", "environment": "main", - "imageName": "moke-review-engine:latest", + "imageName": "second-set-review-engine:latest", "timeoutMinutes": 15, "poolQueue": null } @@ -172,8 +172,8 @@ authenticated NDJSON on the existing loopback endpoint, then Electron's trusted MessagePort to the renderer. The renderer does not hold Modal credentials or open a connection to a public sandbox endpoint. -For an active review the backend watches `/moke` with Modal's filesystem watch -API, filtering atomic updates of `progress.json`. These are wake-up hints, not +For an active review the backend watches the worker folder, `/second-set`, or `/moke` +for a job dispatched to a protocol-5 worker, with Modal's filesystem watch API, filtering atomic updates of `progress.json`. These are wake-up hints, not the source of truth. It reconciles on initial attachment and every ten seconds as well, covering missed events, unsupported watchers, and connection loss. The watcher is bounded to thirty seconds and reconnects; local iterator cleanup @@ -261,13 +261,13 @@ limit, and no warm pool. The operator can choose 1–60 minutes. The sandbox get an additional two minutes for transfer/checkpoint cleanup. Selected count reviews retain their call/token admission budget. Drawing-set reviews honor the optional call budget and the shared time limit. Inference remains on the selected Meta/Baseten service; Modal credits pay for the sandbox, storage, and -related Modal usage. Moke does not read a Modal wallet or promise a dollar cap. +related Modal usage. Second Set does not read a Modal wallet or promise a dollar cap. Configure account budgets in Modal separately from these runtime limits. Transfer and setup consume the same deadline used to claim warm capacity. Exec timeouts are rounded down to whole seconds, as required by the SDK; a transfer leaving less than one minute fails before inference dispatch. A 1-minute limit can therefore never dispatch, so Send refuses it before preflight, -allocation or upload and asks for 2 minutes or more. Moke does not add review +allocation or upload and asks for 2 minutes or more. Second Set does not add review time beyond the configured limit; whether a minimum above 1 minute or a full minute after setup is wanted is an open product decision. @@ -294,7 +294,7 @@ sandboxes, and local work stays fenced until the reviewer confirms stop. Optional prewarming: ```sh -# Requires MOKE_MODAL_POOL_QUEUE in the operator configuration. +# Requires SECOND_SET_MODAL_POOL_QUEUE in the operator configuration. bun run modal:warm ``` @@ -312,11 +312,18 @@ it. Queue messages use JSON to preserve millisecond expiry times through the SDK ## Validation -The current worker is `moke-review-engine:9f4e2fb67`, image -`im-01M6KD1KYF0PJZMCQJXBRPKRHX`, with protocol 5, packet prompt v10 and code index -v3. `moke-review-engine:latest` resolves to the same image. On 2026-11-04 it reported -the expected identity in a fresh sandbox with no network; no cloud review has run on -it yet. The `moke-reviews-opencode-go-api` Modal Secret was created the same day. +The current worker is `second-set-review-engine:latest` in the `second-set-reviews` +App, image `im-01M7137QNPVEHAETCAYGKX9EV8`, with protocol 6, packet prompt +`second-set-packet-review-v11` and code index v3. It was published from `bc722cb` on +2026-11-09 and reported the expected identity in a fresh sandbox with no network; no +cloud review has run on it yet. The `second-set-reviews-meta-api`, +`second-set-reviews-baseten-api` and `second-set-reviews-opencode-go-api` Secrets were +created the same day and are reachable with `SECOND_SET_API_KEY`. + +The last protocol-5 worker is `moke-review-engine:9f4e2fb67`, image +`im-01M6KD1KYF0PJZMCQJXBRPKRHX`, with packet prompt v10 and code index v3, in the +`moke-reviews` App. It reported the expected identity on 2026-11-04, and the +`moke-reviews-opencode-go-api` Secret was created the same day. The previous worker is `moke-review-engine:4cc8c03`, image `im-01M6DBQX7M1HEP05AA68MHBW7R`, with packet prompt v8 and code index v2, kept for @@ -382,7 +389,7 @@ WARP for the review, or remove the entry with `warp-cli dns fallback remove host (`warp-cli dns fallback reset` restores the defaults). **A finished review stops at "Sync paused" with "A package file could not be written."** Windows -antivirus and indexing briefly hold files Moke has just written, and replacing such a file fails +antivirus and indexing briefly hold files Second Set has just written, and replacing such a file fails until they let go. Restores, transfers and database replacement now retry that rename for about ten seconds (`backend/rename.ts`). A paused sync keeps the downloaded result; choose Retry sync. @@ -403,7 +410,7 @@ it does not deploy Harbor or a parallel evaluation fleet. ## Compatibility with drawing-set reviews The cloud composer follows the whole-set and discipline workflow introduced in -[PR #9](https://github.com/sc-frederick/moke-meta-hackathon/pull/9); it does not require +[PR #9](https://github.com/Surfside-Software/second-set/pull/9); it does not require an existing count check. The chosen model selects its own provider connection. Configured Meta tools remain available with Baseten chat through separate outbound header policies. Packet findings, coverage, evidence and reports return @@ -412,11 +419,11 @@ are fenced while the cloud owns the workspace. ### Current admission and recovery -New reviews are admitted only to a protocol-5 worker whose identity matches this +New reviews are admitted only to a protocol-6 worker whose identity matches this build exactly: package engine `review-pi-1.4.0-v1`, count prompt -`moke-selected-coordination-v2`, packet prompt `moke-packet-review-v10`, and the +`second-set-selected-coordination-v3`, packet prompt `second-set-packet-review-v11`, and the current instruction revision and code index version. Every new ownership receipt -records `workerProtocol: 5`. An image with a different identity fails the +records `workerProtocol: 6`. An image with a different identity fails the identity probe before transfer. Migrations are not part of the identity: a worker restores only packages whose migration history is a prefix of its own, so an image built before a new migration passes the probe and refuses the package after @@ -425,7 +432,9 @@ prompts, migrations or the code index change. Recovery only observes work already `dispatching` or `running`; it never redispatches or restarts a model. Earlier identities must still match the current -engine, count prompt and instruction revision. Protocols 2 to 4 omit the code index; earlier protocol-5 workers use index v1: +engine and instruction revision. Workers built before protocol 6 announce the MOKE-era +names they shipped with: count prompt `moke-selected-coordination-v2` and packet prompts +`moke-packet-review-v1` to `v10`. Protocols 2 to 4 omit the code index; earlier protocol-5 workers use index v1: | Receipt `workerProtocol` | Progress accepted while dispatched | | ----------------------------------------- | ------------------------------------------------------------------ | @@ -433,12 +442,26 @@ engine, count prompt and instruction revision. Protocols 2 to 4 omit the code in | `3` | Protocol 3 with packet prompts v1 to v3 | | `4` | Protocol 4 with packet prompts v3 to v5 | | `5` | Protocol 5 with v9 or v10/index v3, v8/index v2, or v6/v7/index v1 | +| `6` | Current progress | Receipts in every other phase accept only current progress. ### Protocol history -Each entry records what an upgrade required when it shipped. +Each entry records what an upgrade required when it shipped, with the names in use then. + +**Protocol 6** ships the rename from MOKE to Second Set. The worker keeps its files +under `/second-set` and `/opt/second-set`, reads `SECOND_SET_CLOUD_PROVIDERS`, and +expects each provider Secret's key as `SECOND_SET_API_KEY`. The defaults become the +`second-set-reviews` App, `second-set-review-engine` image and +`second-set-reviews--api` Secrets. It ships with count prompt +`second-set-selected-coordination-v3` and packet prompt `second-set-packet-review-v11`, +which change only the application's name in model-facing text. Publish the image under +the new App with `bun run modal:image` and create the renamed Secrets before sending; a +protocol-5 image fails the identity probe. Jobs already sent to a protocol-5 worker are +still read from `/moke` and stay recoverable. Migration 0014 renames saved +`moke-packet-review-vN` and `moke-selected-coordination-v2` records to their +`second-set-` names. **Protocol 4** adds the workspace cache: new receipts record the cache Volume and `workerProtocol: 4`, and the worker fills skipped files from `/cache` before @@ -492,7 +515,7 @@ before local ownership returns. An incompatible sync pause saved by an earlier build still requires **Retry sync** after upgrading; it is not cleared implicitly. Before transferring, the desktop resolves the selected published image and required provider -secrets, including their MOKE_API_KEY keys. A credential-free, network-blocked probe +secrets, including their SECOND_SET_API_KEY keys. A credential-free, network-blocked probe validates the worker identity before ownership is recorded; successful compatibility is cached by immutable image ID for the runtime. The probe has a one-minute lifetime and is released after validation. Cloud setup reports readiness failures explicitly. diff --git a/docs/data.md b/docs/data.md index 14c571a5..dea3eb5f 100644 --- a/docs/data.md +++ b/docs/data.md @@ -12,6 +12,16 @@ queries use Effect SQL with the Bun SQLite driver. The migration and backup code uses Bun's node:sqlite compatibility API so the existing backup format, migration hashes, and recovery behavior remain intact. No separate SQLite addon is required. +Releases through 0.2.2 shipped as `moke-meta-hackathon` and stored this directory +under that name. When the old folder holds app.sqlite and the Second Set folder +does not, Electron keeps using the old folder in place and moves or copies nothing. +See [desktop/data-directory.ts](../desktop/data-directory.ts). An explicit +`--user-data-dir` or `PROJECT_KIT_DATA_DIRECTORY` takes precedence. On Windows, +saved keys still unlock from that folder. On macOS and Linux the encryption key is +stored under the app's name, so a key saved by 0.2.2 may not unlock after the +rename. Startup then continues with the keys that still unlock and asks for the +others again; importing or saving a key replaces a copy that no longer unlocks. + A process holds the canonical data-directory ownership lock before recovery or migrations. Queries open scoped Effect SQL connections with foreign keys and FULL synchronization. A shared short semaphore serializes SQL and replacement; PDF @@ -107,6 +117,14 @@ The renderer version changes with the pinned PDF.js or canvas version. Rendering checks the original's hash on the exact bytes given to PDF.js; a replaced original fails and stores no image. +Migration 0014 follows the rename from MOKE to Second Set. Saved run prompt versions, +packet review and record payloads, command receipts and execution responses take the +`second-set-` instruction names, so `moke-packet-review-v10` reads +`second-set-packet-review-v10`. Provider attempts, tool results and SAM records are left +byte for byte as the record of what was sent and returned. Renderer identities stay +unchanged: `moke-render-2` is hashed into every stored render's identity, and renaming it +would rasterize every sheet again for the same pixels. + PDF.js removes an embedded image larger than its image limit from the page without an error. Renderer `moke-render-1` used the 16-megapixel output limit there, so scanned schedules and details above it rendered as blank paper; a 17.6-megapixel panel schedule on a sample sheet @@ -199,11 +217,13 @@ guard runs under that exclusion before a review connection opens. Download retries reuse only files whose size and SHA-256 match the selected result manifest; interrupted Modal copy scratch files are cleared before package validation. -New cloud receipts record `workerProtocol: 5`, the only protocol admitted for new -work, with packet prompt v10. Older receipts recover progress only for jobs already -`dispatching` or `running`: unmarked receipts accept protocol 2 or 3, protocol-3 -receipts accept protocol 3, and protocol-4 receipts accept protocol 4 with packet -prompts v3 to v5. [Current admission and recovery](cloud-reviews.md#current-admission-and-recovery) +New cloud receipts record `workerProtocol: 6`, the only protocol admitted for new +work, with packet prompt `second-set-packet-review-v11`. Older receipts recover progress +only for jobs already `dispatching` or `running`: unmarked receipts accept protocol 2 or +3, protocol-3 receipts accept protocol 3, protocol-4 receipts accept protocol 4 with +packet prompts v3 to v5, and protocol-5 receipts accept the MOKE-era protocol-5 workers, +read from `/moke`. A count fact restored from such a result keeps +`moke-selected-coordination-v2` until migration 0014 runs at the next launch. [Current admission and recovery](cloud-reviews.md#current-admission-and-recovery) lists the exact identities. Recovery changes no saved review content and never replays model work. Existing explicit sync pauses remain paused until **Retry sync**; ownership returns only through verified result diff --git a/docs/deployment.md b/docs/deployment.md index 80710a91..7725faeb 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -108,7 +108,7 @@ embedded Modal or model credential in a distributed binary. Use File > Import provider credential to encrypt a configuration with Electron safeStorage. Linux storage without a running secret service is refused, because Chromium would otherwise encrypt with a publicly known key. Without secure OS storage, -set `MOKE_SESSION_CREDENTIAL_FILE` to an explicitly selected private JSON file +set `SECOND_SET_SESSION_CREDENTIAL_FILE` to an explicitly selected private JSON file for that launch. On Unix it must have owner-only permissions (for example `chmod 600`). It contains `provider`, `apiKey`, canonical `baseUrl`, and `model`. Meta uses `https://api.meta.ai/v1` and `muse-spark-1.3`; Baseten uses @@ -125,5 +125,5 @@ redacted value, and is never returned. See [OpenCode Go](inference.md#opencode-go) and [custom endpoints](inference.md#custom-openai-compatible-endpoint). -Set `MOKE_E2E_LIVE=1` only for the explicitly budgeted packaged acceptance test. +Set `SECOND_SET_E2E_LIVE=1` only for the explicitly budgeted packaged acceptance test. It adds a live inspection to the ordinary four-outcome desktop flow. diff --git a/docs/design.md b/docs/design.md index 3b007eac..988c0ddb 100644 --- a/docs/design.md +++ b/docs/design.md @@ -1,6 +1,6 @@ # Workbench design -MOKE adapts [Forge](https://design.uiverse.io), an industrial dark design system +Second Set adapts [Forge](https://design.uiverse.io), an industrial dark design system from Uiverse Design. Forge's license allows adapted application use and forbids republishing or repackaging the pack, so the pack itself stays out of the repository and application distributions. Only this adaptation ships. diff --git a/docs/development.md b/docs/development.md index 108e2880..06916e3a 100644 --- a/docs/development.md +++ b/docs/development.md @@ -25,7 +25,7 @@ It never uses an existing desktop. It needs unprivileged user namespaces for `Xvfb` and `xdpyinfo`. Inside private user, mount, network, PID and IPC namespaces it starts its own Xvfb on `:99`, strips inherited display, D-Bus and audio variables, and refuses to run while the desktop display `:0` is reachable. -Set `MOKE_XVFB_BINARY` when `Xvfb` is not on `PATH`. Setting `DISPLAY` or wrapping +Set `SECOND_SET_XVFB_BINARY` when `Xvfb` is not on `PATH`. Setting `DISPLAY` or wrapping the command in `xvfb-run` has no effect: the runner discards inherited display variables, and the Playwright fixtures refuse runs that bypass it. The fixed display's lock is `/tmp/.X99-lock`, so run one desktop suite per host at a time. @@ -95,7 +95,7 @@ The Electron flow creates its own two-sheet PDFs and browser profile in a temporary data directory. It captures the real workbench at 1440 × 1000, a corrected finding, and a revision recheck. Screenshots go to each test's output directory under -`test-results/`, leaving the demo frames in `docs/screenshots/` intact. +`test-results/`, leaving the README screenshots in `docs/screenshots/` intact. No model inspection starts. The test refuses screenshot mode with live inference enabled, checks renderer errors, and removes its temporary project after closing. diff --git a/docs/experiments/plumbing-segmentation.md b/docs/experiments/plumbing-segmentation.md index f58253c7..a6c0c25a 100644 --- a/docs/experiments/plumbing-segmentation.md +++ b/docs/experiments/plumbing-segmentation.md @@ -1,6 +1,6 @@ -# SAM for Moke plumbing coordination +# SAM for plumbing coordination -This study applies Moke's [plumbing review instructions](../../backend/review/prompts/assets/disciplines/plumbing.md): reconcile fixture marks and quantities, room uses, and existing/new scope across drawings. +This study applies the vendored [plumbing review instructions](../../backend/review/prompts/assets/disciplines/plumbing.md): reconcile fixture marks and quantities, room uses, and existing/new scope across drawings. ## Result @@ -114,7 +114,7 @@ or accuracy savings. Check held-out rooms, overlapping crop coverage, missed Muse candidates, visible marks, and new/removed scope. Keep schedule quantities out of grounding prompts. -Moke source: the [review instructions](../../backend/review/prompts/README.md) at revision +Instruction source: the [review instructions](../../backend/review/prompts/README.md) at revision `8a0e2648738018631eaa6d24ee0b6ebaa239afb1`: master instructions, shared method, and plumbing discipline. API grounding follows Meta's [normalized coordinate guidance](https://dev.meta.ai/docs/image-understanding#perception-grounding) diff --git a/docs/experiments/sam-drawing-expansion.md b/docs/experiments/sam-drawing-expansion.md index 0c54760f..45d0f265 100644 --- a/docs/experiments/sam-drawing-expansion.md +++ b/docs/experiments/sam-drawing-expansion.md @@ -2,7 +2,7 @@ ## Question and inputs -Can the current SAM 3.1 API help Moke count electrical devices, inspect other +Can the current SAM 3.1 API help Second Set count electrical devices, inspect other drawing objects, and review revisions? This continues the [plumbing study](plumbing-segmentation.md). Research dates from 2026-10-27. Drawing experiments use the original @@ -17,7 +17,7 @@ The fixture pack has these relevant sheets: | C | E-2, page 7 | E-3, page 8 | M-2 page 5; P-1/P-2 pages 1/2 | | B | No electrical sheets in supplied pack | None | B-2 page 2, sparse residential architecture | -Moke's [electrical method](../../backend/review/prompts/assets/disciplines/electrical.md) +The vendored [electrical method](../../backend/review/prompts/assets/disciplines/electrical.md) calls for fixture marks, circuits, controls, room use and schedule coordination. Its [mechanical method](../../backend/review/prompts/assets/disciplines/mechanical.md) calls for terminal airflow, equipment marks and system tracing. An object count diff --git a/docs/experiments/sam-expanded-sets.md b/docs/experiments/sam-expanded-sets.md index 57380387..34337f24 100644 --- a/docs/experiments/sam-expanded-sets.md +++ b/docs/experiments/sam-expanded-sets.md @@ -100,7 +100,7 @@ the recipe files. This preflight needs no PDF files, API credentials, or model calls. Historical scores for the 15 branches remain in `captured-locations.json` and `expected-metrics.json`; the test suite no longer reruns the scoring helper. -Source review methods: Moke's +Source review methods: the vendored [electrical](../../backend/review/prompts/assets/disciplines/electrical.md), [structural](../../backend/review/prompts/assets/disciplines/structural.md), [civil](../../backend/review/prompts/assets/disciplines/civil.md), diff --git a/docs/experiments/segmentation-test.md b/docs/experiments/segmentation-test.md index 831dcacc..55e9888e 100644 --- a/docs/experiments/segmentation-test.md +++ b/docs/experiments/segmentation-test.md @@ -1,7 +1,7 @@ # SAM 3.1 drawing experiment The subsequent [plumbing coordination study](plumbing-segmentation.md) tests -Moke fixture inventory, schedule reconciliation, and corrective prompts. +fixture inventory, schedule reconciliation, and corrective prompts. This is the first perception experiment from the October 26 planning thread. It tests proposed object boundaries on six drawing crops before choosing an diff --git a/docs/fixtures/hero-case.md b/docs/fixtures/hero-case.md index 762eab83..de4c4b9b 100644 --- a/docs/fixtures/hero-case.md +++ b/docs/fixtures/hero-case.md @@ -1,7 +1,7 @@ # Hero case: fixture-count coordination The first public case uses the original schematic generated by -`tests/fixtures/hero-case.ts`. Moke authored this test source; it contains no +`tests/fixtures/hero-case.ts`. Second Set authored this test source; it contains no client drawings. It has one floor, two related sheets, two room fixture classes, an independent control detail, and Revision B. It tests software behavior and evidence handling rather than accuracy on unseen professional drawings. diff --git a/docs/inference.md b/docs/inference.md index 24a58da4..458c4f3c 100644 --- a/docs/inference.md +++ b/docs/inference.md @@ -35,7 +35,7 @@ prompts and completions to train Meta's models, and it is not zero-retention. An refuses it. - **Traffic.** Go is designed for coding agents and monitors traffic for abuse. Parallel drawing - reviews are not typical coding traffic. Moke identifies itself as `moke/`, sends a session + reviews are not typical coding traffic. Second Set identifies itself as `second-set/`, sends a session ID per conversation and runs 3 sheet reviewers at a time. See [OpenCode Go](#opencode-go-1). - **Images.** Go has no Files API, so images travel inline. Reviews keep the eight-image window Baseten uses, so a reviewer inspects a batch before requesting the next. @@ -53,7 +53,7 @@ use `meta-models/muse-spark-open`, which is a different configured model ID. SAM key is configured, packet reviews use it with either chat provider; see [SAM in packet reviews](#sam-in-packet-reviews). -`MOKE_PROVIDER` chooses the preferred startup provider, with `meta` as the +`SECOND_SET_PROVIDER` chooses the preferred startup provider, with `meta` as the default. Each review saves the model selected in the workbench. Changing the menu does not change an existing run. When Go is connected, a follow-up whose earlier model the menu no longer offers moves to the first model it does. The @@ -64,7 +64,7 @@ Import the PDF packet and send a natural-language review request. Auto routes a whole-set QA request or a named discipline; an explicit reviewer selects that discipline. No count check or manual sheet confirmation is required to start a packet review. A note is saved without calling a model. The model receives the -full Moke AGENTS.md, shared method and applicable discipline instructions as its +full vendored AGENTS.md, shared method and applicable discipline instructions as its Pi system instructions. See the [prompt documentation](../backend/review/prompts/README.md). ## Model capabilities and operational budgets @@ -169,7 +169,7 @@ for compatibility. Those limits do not define ordinary packet review. ## Prompt caching and request images -Meta and Baseten cache prompt prefixes automatically. Moke routes requests that +Meta and Baseten cache prompt prefixes automatically. Second Set routes requests that share a prefix to the same cache and keeps earlier turns unchanged, aiming for at least 90% of prompt tokens served from cache (`cacheRead / (input + cacheRead)`). @@ -216,7 +216,7 @@ the Console harnesses page (`oc_sk_...`). In development, `OPENCODE_API_KEY` in `.env` does the same; Pi and OpenCode read the same variable. The model menu then offers `muse-spark-1.3-contributor`, which reads images and calls tools over Responses. -Moke offers only Meta's model through Go. Earlier reviews on MiMo-V2.6-Flash, +Second Set offers only Meta's model through Go. Earlier reviews on MiMo-V2.6-Flash, GLM-5.3-Flash or GPT-6 Luna stay readable, and a saved connection that names one of them still opens, but no menu offers them for new reviews. @@ -227,7 +227,7 @@ clients for a stable `x-opencode-session` per conversation and rejects requests without one (HTTP 400 `MissingSessionID`, checked 2026-11-01). Pi sends it from each durable conversation's session ID, so every sheet reviewer keeps its own session across restarts; Baseten's shared prefix key is not used. Requests also -identify MOKE with `User-Agent: moke/` and `x-opencode-client: moke`, +identify Second Set with `User-Agent: second-set/` and `x-opencode-client: second-set`, as OpenCode asks of third-party clients. Free Go models are not offered: OpenCode answers them with HTTP 403 outside its own app. @@ -238,14 +238,14 @@ other coding agents", ask clients to "send typical coding agent traffic", and monitor "for abuse that degrades the experience for other users". OpenCode's [terms](https://opencode.ai/legal/terms-of-service) also bar "placing an unreasonable load" on its infrastructure and using the service on behalf of a -third party. MOKE is an agent but not a coding agent, so it keeps its traffic +third party. Second Set is an agent but not a coding agent, so it keeps its traffic modest, identifies itself honestly, and should not run client work on a Go key: - A packet review starts three sheet reviewers at once, about one person's agents. One reviewer sends about three requests a minute (measured on Muse Spark 1.3, 2026-10-30), so a 29-sheet review takes about an hour at that speed. - The pacer caps every use of the key at 60 requests and 5M tokens a minute. This - is MOKE's own ceiling, not a published limit. + is Second Set's own ceiling, not a published limit. - A `GoUsageLimitError`, `BudgetLimitError` or `CreditLimitExceeded` 429 is never resent, matching OpenCode's own client. The review pauses until the reviewer resumes it. @@ -359,7 +359,7 @@ A session file accepts one provider configuration or both: } ``` -Set `MOKE_SESSION_CREDENTIAL_FILE` to that private file for the launch. Unix files +Set `SECOND_SET_SESSION_CREDENTIAL_FILE` to that private file for the launch. Unix files must be owner-only; Windows uses file ACLs. Importing one provider preserves the other provider's saved connection. Restart after changing the session file. A model whose provider is unconfigured stays disabled. Connected in settings diff --git a/docs/screenshots/README.md b/docs/screenshots/README.md index c42e06ee..da35de65 100644 --- a/docs/screenshots/README.md +++ b/docs/screenshots/README.md @@ -1,40 +1,38 @@ # Screenshot sources -The README screenshots are full frames from `moke-demo-voiceover-final.mp4`, the -local final narrated demo cut rendered on November 4, 2026. They retain the -recorded app UI, cursor, and footage labels at the video resolution of -2560 × 1440. The video edit already cropped the window title bar and scaled the -footage. Frame extraction adds no further cropping or content edits. +The README screenshots are captures of the application at 1600 × 900 and a 1.6 device +scale, which gives 2560 × 1440 frames like the narrated demo's. They were taken on +2026-11-09 from a copy of the workspace that recorded the demo, opened in a private +display with providers configured but never called. The capture adds no cropping or +content edits. One empty project in that copy, then named "MOKE Demo", was renamed +"Second Set Demo" before capture. -[Watch the demo on YouTube](https://youtu.be/wl8wwq_Tlrw). +[Watch the demo on YouTube](https://youtu.be/wl8wwq_Tlrw). The demo was recorded before the +rename, so it shows the MOKE name. -Source video SHA-256: +| Image | Content | +| -------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | +| [workbench.png](workbench.png) | Five-sheet civil review with 19 findings, coverage gaps, and findings navigation | +| [correction.png](correction.png) | Source evidence with a SAM highlight and a saved false-alarm decision explaining that identifying information was removed for the demo | -```text -a9fc56262ddd25e83f0aef285f5fd33f95bbe3375d2a0a5840e96d3339afe5cb -``` - -| Image | Video timestamp | Content | -| -------------------------------- | --------------- | -------------------------------------------------------------------------------------------------------------------------------------- | -| [workbench.png](workbench.png) | 00:01:32 | Five-sheet civil review with 19 findings, coverage gaps, and findings navigation | -| [correction.png](correction.png) | 00:01:48 | Source evidence with a SAM highlight and a saved false-alarm decision explaining that identifying information was removed for the demo | -| [revision.png](revision.png) | 00:02:02 | Earlier 13-sheet review showing an unresolved Rev 1 area discrepancy and the saved reviewer decision | +The demo's October 31 revision frame, from a 13-sheet set, is not in the workspace copy, +so the README links to that moment in the video instead. -The revision frame retains the video's "Oct 31 footage, 13-sheet set" label. -It is from a different review than the five-sheet civil example. +## Capture the screenshots -## Extract the frames - -With FFmpeg installed, run these commands from the repository root. Set -`MOKE_DEMO_VIDEO` to the local copy of the source video; the video itself is not -stored in this repository. +`tests/e2e/readme-screenshots.spec.ts` opens the "Single Family Lot Drainage and Grading +Set" review and writes both frames to `test-results/`. It edits the workspace it is given, +so point it at a disposable copy, never at a live data folder. It is skipped unless +`SECOND_SET_SCREENSHOT_WORKSPACE` is set: ```sh -export MOKE_DEMO_VIDEO=/path/to/moke-demo-voiceover-final.mp4 -ffmpeg -hide_banner -loglevel error -y -ss 92 -i "$MOKE_DEMO_VIDEO" -frames:v 1 -threads 2 docs/screenshots/workbench.png -ffmpeg -hide_banner -loglevel error -y -ss 108 -i "$MOKE_DEMO_VIDEO" -frames:v 1 -threads 2 docs/screenshots/correction.png -ffmpeg -hide_banner -loglevel error -y -ss 122 -i "$MOKE_DEMO_VIDEO" -frames:v 1 -threads 2 docs/screenshots/revision.png +cp -a /path/to/workspace-snapshot /tmp/second-set-screenshots +SECOND_SET_SCREENSHOT_WORKSPACE=/tmp/second-set-screenshots \ + bun scripts/isolated-desktop.ts tests/e2e/readme-screenshots.spec.ts --project=desktop ``` -`bun run test:screenshots` captures the offline synthetic fixture into -`test-results/`. It does not replace these demo frames. +The snapshot needs `app.sqlite`, `artifacts/` and `execution-journals/`. Take the database +with SQLite's backup API so a running application cannot leave it inconsistent. + +`bun run test:screenshots` captures the offline synthetic fixture into `test-results/`. It +does not replace these frames. diff --git a/docs/screenshots/correction.png b/docs/screenshots/correction.png index a5d8b527..bcbb45ba 100644 Binary files a/docs/screenshots/correction.png and b/docs/screenshots/correction.png differ diff --git a/docs/screenshots/revision.png b/docs/screenshots/revision.png deleted file mode 100644 index f2096640..00000000 Binary files a/docs/screenshots/revision.png and /dev/null differ diff --git a/docs/screenshots/workbench.png b/docs/screenshots/workbench.png index 015e5b31..0833c495 100644 Binary files a/docs/screenshots/workbench.png and b/docs/screenshots/workbench.png differ diff --git a/docs/validation/cloud-validation.md b/docs/validation/cloud-validation.md index ea64639a..94724138 100644 --- a/docs/validation/cloud-validation.md +++ b/docs/validation/cloud-validation.md @@ -2,7 +2,7 @@ Observed October 29, 2026 UTC (October 28 in New York), using Bun 1.4.4, `modal@0.11.1`, the `moke-reviews` App, and `moke-review-engine:latest`. -Only Moke's authored two-sheet schematic was uploaded. No customer drawings +Only Second Set's authored two-sheet schematic was uploaded. No customer drawings were used. Account credentials were delivered privately and are excluded from the repository and worker image. diff --git a/docs/validation/core-validation.md b/docs/validation/core-validation.md index b1423ac7..ad15fd5a 100644 --- a/docs/validation/core-validation.md +++ b/docs/validation/core-validation.md @@ -6,7 +6,7 @@ The primary route is Meta Responses, `muse-spark-1.3`. SAM uses its separate ## Packaged acceptance -`MOKE_E2E_LIVE=1 MOKE_SESSION_CREDENTIAL_FILE=/private/session.json DISPLAY=:0 +`SECOND_SET_E2E_LIVE=1 SECOND_SET_SESSION_CREDENTIAL_FILE=/private/session.json DISPLAY=:0 bun run test:packaged` passed both desktop tests. The production flow took 41.8 seconds. Electron loaded the explicit session credential and passed it to the compiled Bun backend through its private stdin channel. The installed app diff --git a/docs/validation/hackathon-readiness.md b/docs/validation/hackathon-readiness.md index b7f68364..fb20026a 100644 --- a/docs/validation/hackathon-readiness.md +++ b/docs/validation/hackathon-readiness.md @@ -7,8 +7,8 @@ it does not certify participant eligibility or guarantee a judging outcome. | Requirement | Repository evidence | | ------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Address a problem statement and incorporate Meta models | Moke addresses Build for Your World through engineering drawing review. [Model routes](../../src/lib/review/models.ts) include Muse Spark, Muse Wonder, Muse Spark Open through Baseten, and Muse Spark 1.3 Contributor through OpenCode Go. [SAM evidence](../experiments/adaptive-meta-review.md) documents segmentation. | -| Public repository | GitHub reported `sc-frederick/moke-meta-hackathon` as public during this check. | +| Address a problem statement and incorporate Meta models | Second Set addresses Build for Your World through engineering drawing review. [Model routes](../../src/lib/review/models.ts) include Muse Spark, Muse Wonder, Muse Spark Open through Baseten, and Muse Spark 1.3 Contributor through OpenCode Go. [SAM evidence](../experiments/adaptive-meta-review.md) documents segmentation. | +| Public repository | GitHub reported `sc-frederick/moke-meta-hackathon`, now `Surfside-Software/second-set`, as public during this check. | | Identify work built during the event | The application history starts October 27, 2026 at `0c5e8716c`. The [README](../../README.md#hackathon-provenance), [manifest](../../.project-kit/manifest.json), and [prompt provenance](../../backend/review/prompts/README.md) distinguish the new application from the project-kit starter, earlier Moke prompts, the pre-existing [2023 Florida code reference](../../backend/review/code-index/README.md), and dependencies. The code index tooling and additions were introduced in the October 30 implementation. | | Respect code, data, and asset rights | The [MIT License](../../LICENSE), [design attribution](../design.md), and [Tabler license](../../src/lib/review/TABLER-LICENSE) document source attribution. The licensed Forge reference pack and local drawing fixture directory are excluded from tracked files. The [demo frames](../screenshots/README.md) retain the recorded footage and its labels. These checks do not independently establish ownership of every dependency or drawing. | | Three-minute demo showing the new work | The local narrated demo cut used for the screenshots is 151.541 seconds. It shows the review results, source evidence, saved decisions, and a labeled earlier revision review. [Frame provenance](../screenshots/README.md) identifies its source and timestamps. | diff --git a/docs/validation/muse-spark-open.md b/docs/validation/muse-spark-open.md index c5b2ea72..bcbdf36d 100644 --- a/docs/validation/muse-spark-open.md +++ b/docs/validation/muse-spark-open.md @@ -1,6 +1,6 @@ # Muse Spark Open reviews -Muse Spark Open is one of Meta's open-weight models. Moke runs it through Baseten as +Muse Spark Open is one of Meta's open-weight models. Second Set runs it through Baseten as `meta-models/muse-spark-open-1026`. ## Completed reviews diff --git a/electron-builder.json b/electron-builder.json index 205db088..ba51eec3 100644 --- a/electron-builder.json +++ b/electron-builder.json @@ -1,6 +1,6 @@ { - "appId": "com.scfrederick.moke-meta-hackathon", - "productName": "moke-meta-hackathon", + "appId": "com.scfrederick.second-set", + "artifactName": "second-set-${version}-${arch}.${ext}", "directories": { "output": "release" }, @@ -41,6 +41,7 @@ "signExecutable": false }, "linux": { + "executableName": "second-set", "icon": "desktop/icons/icon.png", "target": ["AppImage", "deb"], "category": "Utility", diff --git a/migrations/0014_second_set_identifiers.sql b/migrations/0014_second_set_identifiers.sql new file mode 100644 index 00000000..df533000 --- /dev/null +++ b/migrations/0014_second_set_identifiers.sql @@ -0,0 +1,36 @@ +-- The application was renamed from MOKE to Second Set. Saved review metadata takes the new +-- instruction identifiers, so moke-packet-review-v10 reads second-set-packet-review-v10. +-- Provider attempts, tool results and SAM records stay byte for byte: they are the record of what +-- was sent and returned. +UPDATE review_execution_runs +SET prompt_version = 'second-set-' || substr(prompt_version, 6) +WHERE prompt_version LIKE 'moke-selected-coordination-v%'; + +UPDATE packet_reviews +SET payload = replace(replace(payload, + '"moke-packet-review-v', '"second-set-packet-review-v'), + '"moke-selected-coordination-v', '"second-set-selected-coordination-v') +WHERE payload LIKE '%"moke-packet-review-v%' OR payload LIKE '%"moke-selected-coordination-v%'; + +UPDATE review_records +SET payload = replace(replace(payload, + '"moke-packet-review-v', '"second-set-packet-review-v'), + '"moke-selected-coordination-v', '"second-set-selected-coordination-v') +WHERE payload LIKE '%"moke-packet-review-v%' OR payload LIKE '%"moke-selected-coordination-v%'; + +UPDATE review_commands +SET receipt = replace(replace(receipt, + '"moke-packet-review-v', '"second-set-packet-review-v'), + '"moke-selected-coordination-v', '"second-set-selected-coordination-v') +WHERE receipt LIKE '%"moke-packet-review-v%' OR receipt LIKE '%"moke-selected-coordination-v%'; + +UPDATE review_execution_commands +SET response_json = replace(replace(response_json, + '"moke-packet-review-v', '"second-set-packet-review-v'), + '"moke-selected-coordination-v', '"second-set-selected-coordination-v') +WHERE response_json LIKE '%"moke-packet-review-v%' + OR response_json LIKE '%"moke-selected-coordination-v%'; + +UPDATE review_events +SET message = 'Second Set review response saved.' +WHERE message = 'MOKE review response saved.'; diff --git a/package.json b/package.json index d8170bd4..7b42df52 100644 --- a/package.json +++ b/package.json @@ -1,9 +1,10 @@ { - "name": "moke-meta-hackathon", - "version": "0.2.2", + "name": "second-set", + "productName": "Second Set", + "version": "0.3.0", "private": true, "description": "A desktop workbench for evidence-backed engineering drawing review", - "homepage": "https://github.com/sc-frederick/moke-meta-hackathon", + "homepage": "https://github.com/Surfside-Software/second-set", "author": "Stephen Frederick", "license": "MIT", "type": "module", @@ -91,7 +92,7 @@ "bun": ">=1.4.4" }, "packageManager": "bun@1.4.4", - "desktopName": "moke-meta-hackathon.desktop", + "desktopName": "second-set.desktop", "trustedDependencies": [ "electron", "esbuild" diff --git a/scripts/build-icons.ts b/scripts/build-icons.ts new file mode 100644 index 00000000..2e029158 --- /dev/null +++ b/scripts/build-icons.ts @@ -0,0 +1,61 @@ +/** + * Render the application icon rasters from their SVG sources with resvg: + * `desktop/icons/icon.png`, `icon.icns` and `icon.ico`. Usage: `bun scripts/build-icons.ts`. + */ +import { Resvg } from "@resvg/resvg-js"; +import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +const directory = join(import.meta.dir, "..", "desktop", "icons"); + +const render = (svg: string, size: number) => + new Resvg(svg, { fitTo: { mode: "width", value: size } }).render().asPng(); + +/** An ICO file of PNG entries, which Windows reads from Vista on. */ +const ico = (images: ReadonlyArray<{ size: number; png: Uint8Array }>) => { + const header = Buffer.alloc(6 + images.length * 16); + header.writeUInt16LE(0, 0); + header.writeUInt16LE(1, 2); + header.writeUInt16LE(images.length, 4); + let offset = header.length; + + images.forEach(({ size, png }, index) => { + const entry = 6 + index * 16; + header.writeUInt8(size >= 256 ? 0 : size, entry); + header.writeUInt8(size >= 256 ? 0 : size, entry + 1); + header.writeUInt16LE(1, entry + 4); + header.writeUInt16LE(32, entry + 6); + header.writeUInt32LE(png.length, entry + 8); + header.writeUInt32LE(offset, entry + 12); + offset += png.length; + }); + + return Buffer.concat([header, ...images.map(({ png }) => Buffer.from(png))]); +}; + +const icon = await readFile(join(directory, "icon.svg"), "utf8"); +const fullBleed = await readFile(join(directory, "icon-full-bleed.svg"), "utf8"); + +await writeFile(join(directory, "icon.png"), render(icon, 1024)); + +const icns = await mkdtemp(join(tmpdir(), "second-set-icns-")); + +try { + for (const size of [16, 32, 64, 128, 256, 512, 1024]) + await writeFile(join(icns, `${size}.png`), render(icon, size)); + + const packer = Bun.spawn( + [process.execPath, join(import.meta.dir, "build-icns.ts"), icns, join(directory, "icon.icns")], + { stdout: "inherit", stderr: "inherit" }, + ); + + if ((await packer.exited) !== 0) throw new Error("The ICNS file could not be written."); +} finally { + await rm(icns, { recursive: true, force: true }); +} + +await writeFile( + join(directory, "icon.ico"), + ico([16, 24, 32, 48, 64, 128, 256].map((size) => ({ size, png: render(fullBleed, size) }))), +); diff --git a/scripts/desktop-test-environment.ts b/scripts/desktop-test-environment.ts index 5760a678..a9cb1a73 100644 --- a/scripts/desktop-test-environment.ts +++ b/scripts/desktop-test-environment.ts @@ -4,14 +4,15 @@ export const isHostedDesktopRunner = (environment: NodeJS.ProcessEnv) => /** Fail closed before Electron can access any local graphical session. */ export function assertDesktopTestEnvironment(environment: NodeJS.ProcessEnv) { - if (!environment.MOKE_TEST_RUNTIME) + if (!environment.SECOND_SET_TEST_RUNTIME) throw new Error("Desktop tests require a private runtime/profile directory."); - if (environment.MOKE_TEST_HOSTED_RUNNER === "1" && isHostedDesktopRunner(environment)) return; + if (environment.SECOND_SET_TEST_HOSTED_RUNNER === "1" && isHostedDesktopRunner(environment)) + return; if ( - !environment.MOKE_TEST_ISOLATED_DISPLAY || - environment.DISPLAY !== environment.MOKE_TEST_ISOLATED_DISPLAY || + !environment.SECOND_SET_TEST_ISOLATED_DISPLAY || + environment.DISPLAY !== environment.SECOND_SET_TEST_ISOLATED_DISPLAY || environment.WAYLAND_DISPLAY || environment.DBUS_SESSION_BUS_ADDRESS !== "unix:path=/dev/null" ) diff --git a/scripts/isolated-desktop.ts b/scripts/isolated-desktop.ts index 5d006b07..d0070ab5 100644 --- a/scripts/isolated-desktop.ts +++ b/scripts/isolated-desktop.ts @@ -6,7 +6,7 @@ import { isHostedDesktopRunner } from "./desktop-test-environment.ts"; // Hosted runners are disposable OS test sessions, including native Windows/macOS. // This gate deliberately excludes generic CI and self-hosted Actions runners. if (isHostedDesktopRunner(process.env)) { - const directory = await mkdtemp(join(tmpdir(), "moke-hosted-desktop-")); + const directory = await mkdtemp(join(tmpdir(), "second-set-hosted-desktop-")); try { const child = Bun.spawn( @@ -14,9 +14,9 @@ if (isHostedDesktopRunner(process.env)) { { env: { ...process.env, - MOKE_TEST_RUNTIME: directory, - MOKE_TEST_HOSTED_RUNNER: "1", - MOKE_E2E_LIVE: "0", + SECOND_SET_TEST_RUNTIME: directory, + SECOND_SET_TEST_HOSTED_RUNNER: "1", + SECOND_SET_E2E_LIVE: "0", }, stdin: "inherit", stdout: "inherit", @@ -36,7 +36,7 @@ if (process.platform !== "linux") throw new Error("Local desktop proof requires the private Linux isolation runner."); // Separate network and mount namespaces hide both abstract and filesystem X11 sockets. -if (process.platform === "linux" && process.env.MOKE_TEST_MOUNT_NAMESPACE !== "1") { +if (process.platform === "linux" && process.env.SECOND_SET_TEST_MOUNT_NAMESPACE !== "1") { const sandbox = Bun.which("unshare"); if (!sandbox) throw new Error("unshare is required for a private X11 socket namespace."); @@ -56,13 +56,13 @@ if (process.platform === "linux" && process.env.MOKE_TEST_MOUNT_NAMESPACE !== "1 "/bin/sh", "-c", 'set -eu; mount -t tmpfs -o mode=1777 tmpfs /tmp/.X11-unix; if [ -d /mnt/wslg ]; then mount -t tmpfs -o mode=0700 tmpfs /mnt/wslg; fi; ip link set lo up; exec "$@"', - "moke-private-session", + "second-set-private-session", process.execPath, import.meta.path, ...process.argv.slice(2), ], { - env: { ...process.env, MOKE_TEST_MOUNT_NAMESPACE: "1" }, + env: { ...process.env, SECOND_SET_TEST_MOUNT_NAMESPACE: "1" }, stdin: "inherit", stdout: "inherit", stderr: "inherit", @@ -73,22 +73,22 @@ if (process.platform === "linux" && process.env.MOKE_TEST_MOUNT_NAMESPACE !== "1 } // Never inherit a user's graphical session. Xvfb chooses and owns a fresh display. -const directory = await mkdtemp(join(tmpdir(), "moke-private-display-")); +const directory = await mkdtemp(join(tmpdir(), "second-set-private-display-")); const runtime = join(directory, "runtime"); await mkdir(runtime, { mode: 0o700 }); -const executable = process.env.MOKE_XVFB_BINARY ?? Bun.which("Xvfb"); +const executable = process.env.SECOND_SET_XVFB_BINARY ?? Bun.which("Xvfb"); if (!executable) throw new Error( - "Private Xvfb is required. Set MOKE_XVFB_BINARY; desktop tests never use the inherited display.", + "Private Xvfb is required. Set SECOND_SET_XVFB_BINARY; desktop tests never use the inherited display.", ); const environment = Object.fromEntries( Object.entries(process.env).flatMap(([key, value]) => value === undefined || - /^(DISPLAY|WAYLAND_DISPLAY|DBUS_|SESSION_MANAGER|PULSE_|PIPEWIRE_|XDG_CURRENT_DESKTOP|DESKTOP_SESSION|XAUTHORITY|ELECTRON_RUN_AS_NODE|MOKE_E2E_LIVE)/.test( + /^(DISPLAY|WAYLAND_DISPLAY|DBUS_|SESSION_MANAGER|PULSE_|PIPEWIRE_|XDG_CURRENT_DESKTOP|DESKTOP_SESSION|XAUTHORITY|ELECTRON_RUN_AS_NODE|SECOND_SET_E2E_LIVE)/.test( key, ) ? [] @@ -160,8 +160,8 @@ try { env: { ...environment, DISPLAY: privateDisplay, - MOKE_TEST_ISOLATED_DISPLAY: privateDisplay, - MOKE_TEST_RUNTIME: runtime, + SECOND_SET_TEST_ISOLATED_DISPLAY: privateDisplay, + SECOND_SET_TEST_RUNTIME: runtime, XDG_RUNTIME_DIR: runtime, XDG_SESSION_TYPE: "x11", GDK_BACKEND: "x11", diff --git a/scripts/modal.ts b/scripts/modal.ts index df7f4796..6197ac66 100644 --- a/scripts/modal.ts +++ b/scripts/modal.ts @@ -41,7 +41,7 @@ const main = Effect.scoped( return yield* new CloudReviewError({ reason: "configuration", message: - "Configure Modal using MOKE_MODAL_CONFIG or MODAL_TOKEN_ID and MODAL_TOKEN_SECRET first. See docs/cloud-reviews.md.", + "Configure Modal using SECOND_SET_MODAL_CONFIG or MODAL_TOKEN_ID and MODAL_TOKEN_SECRET first. See docs/cloud-reviews.md.", }); const client = yield* Effect.acquireRelease( @@ -84,7 +84,7 @@ const main = Effect.scoped( const base = client.images .fromRegistry("oven/bun:1.4.4") .dockerfileCommands([ - "WORKDIR /opt/moke", + "WORKDIR /opt/second-set", "RUN bun add --exact @napi-rs/canvas@1.1.0 @napi-rs/canvas-linux-x64-gnu@1.1.0 pdfjs-dist@6.5.438", ]); @@ -104,12 +104,15 @@ const main = Effect.scoped( ); yield* call(() => - sandbox.filesystem.copyFromLocal(join(output, "worker-main.mjs"), "/opt/moke/worker.mjs"), + sandbox.filesystem.copyFromLocal( + join(output, "worker-main.mjs"), + "/opt/second-set/worker.mjs", + ), ); yield* call(() => sandbox.filesystem.copyFromLocal( join(output, "pdf-worker-main.mjs"), - "/opt/moke/pdf-worker.mjs", + "/opt/second-set/pdf-worker.mjs", ), ); const migrations = yield* call(() => readdir("migrations")); @@ -118,12 +121,12 @@ const main = Effect.scoped( yield* call(() => sandbox.filesystem.copyFromLocal( join("migrations", file), - `/opt/moke/migrations/${file}`, + `/opt/second-set/migrations/${file}`, ), ); const text = yield* call(async () => { - const process = await sandbox.exec(["bun", "/opt/moke/worker.mjs", "--identity"]); + const process = await sandbox.exec(["bun", "/opt/second-set/worker.mjs", "--identity"]); const text = await process.stdout.readText(); if ((await process.wait()) !== 0) throw failed(); @@ -143,7 +146,7 @@ const main = Effect.scoped( return yield* new CloudReviewError({ reason: "configuration", message: - "Set MOKE_MODAL_POOL_QUEUE to opt into a warm sandbox. Warm capacity uses credits even before a review starts.", + "Set SECOND_SET_MODAL_POOL_QUEUE to opt into a warm sandbox. Warm capacity uses credits even before a review starts.", }); const queue = yield* call(() => @@ -184,7 +187,7 @@ const main = Effect.scoped( experimentalOptions: { enable_exit_snapshot: true }, experimentalOutboundPolicy: emptyReviewOutboundPolicy(), readinessProbe: modalReadinessProbe(), - tags: { "moke-kind": "warm" }, + tags: { "second-set-kind": "warm" }, }), ); diff --git a/scripts/test-packaged.ts b/scripts/test-packaged.ts index 5b7bebb2..0751157a 100644 --- a/scripts/test-packaged.ts +++ b/scripts/test-packaged.ts @@ -1,9 +1,9 @@ import { Match } from "effect"; const patterns = { - linux: "release/linux*-unpacked/moke-meta-hackathon", - win32: "release/win*-unpacked/moke-meta-hackathon.exe", - darwin: "release/mac*/moke-meta-hackathon.app/Contents/MacOS/moke-meta-hackathon", + linux: "release/linux*-unpacked/second-set", + win32: "release/win*-unpacked/Second Set.exe", + darwin: "release/mac*/Second Set.app/Contents/MacOS/Second Set", }; const pattern = Match.value(process.platform).pipe( diff --git a/scripts/validate-cloud.ts b/scripts/validate-cloud.ts index 1c344964..174814e2 100644 --- a/scripts/validate-cloud.ts +++ b/scripts/validate-cloud.ts @@ -28,13 +28,13 @@ const main = Effect.gen(function* () { } const modal = yield* readModalConfiguration(false); - const providerFile = Bun.env.MOKE_SESSION_CREDENTIAL_FILE; + const providerFile = Bun.env.SECOND_SET_SESSION_CREDENTIAL_FILE; if (modal === undefined || providerFile === undefined) return yield* new CloudReviewError({ reason: "configuration", message: - "Set MOKE_MODAL_CONFIG and MOKE_SESSION_CREDENTIAL_FILE to private connection files before live acceptance.", + "Set SECOND_SET_MODAL_CONFIG and SECOND_SET_SESSION_CREDENTIAL_FILE to private connection files before live acceptance.", }); const inference = yield* Effect.tryPromise({ diff --git a/scripts/validate-core.ts b/scripts/validate-core.ts index f74b1120..e4c6f35d 100644 --- a/scripts/validate-core.ts +++ b/scripts/validate-core.ts @@ -67,7 +67,7 @@ const program = Effect.gen(function* () { projectId, expectedVersion: version(), source, - name: "Original Moke schematic, Rev A", + name: "Original Second Set schematic, Rev A", }); state = yield* api.ReviewSnapshot({ projectId, afterSequence: 0 }); diff --git a/src/app.d.ts b/src/app.d.ts index 6caff814..3c1ab9aa 100644 --- a/src/app.d.ts +++ b/src/app.d.ts @@ -1,7 +1,7 @@ declare global { interface Window { /** Stage an actual OS-dropped file at the trusted Electron boundary. Paths stay in Electron. */ - mokeFiles: { stageDrop: (file: File) => Promise<{ token: string; name: string } | null> }; + secondSetFiles: { stageDrop: (file: File) => Promise<{ token: string; name: string } | null> }; } } diff --git a/src/lib/review/components/CitationRow.svelte b/src/lib/review/components/CitationRow.svelte index af6d1234..72ad9a9f 100644 --- a/src/lib/review/components/CitationRow.svelte +++ b/src/lib/review/components/CitationRow.svelte @@ -39,8 +39,8 @@ ); const help = $derived({ - verified: "The quote was found in the official text Moke retrieved.", - indexed: `This link comes from Moke's Florida code index. ${reference?.access === "open_in_browser" ? "Official text isn't available for automated reading. " : ""}Its wording is unconfirmed. Open the section to check it.`, + verified: "The quote was found in the official text Second Set retrieved.", + indexed: `This link comes from Second Set's Florida code index. ${reference?.access === "open_in_browser" ? "Official text isn't available for automated reading. " : ""}Its wording is unconfirmed. Open the section to check it.`, unverified: "The wording hasn't been confirmed against retrieved official text. Check it by hand.", incorrect: "The citation doesn't match the governing text.", diff --git a/src/lib/review/components/CloudReviewControls.svelte b/src/lib/review/components/CloudReviewControls.svelte index 6801e9d4..1e9832c5 100644 --- a/src/lib/review/components/CloudReviewControls.svelte +++ b/src/lib/review/components/CloudReviewControls.svelte @@ -1,5 +1,6 @@

- Finish or pause the running review to change the endpoint; saving restarts MOKE. + Finish or pause the running review to change the endpoint; saving restarts Second Set.

{/if}

{message}

diff --git a/src/lib/review/components/OpenCodeGoSettings.svelte b/src/lib/review/components/OpenCodeGoSettings.svelte index d87e8437..8ff96240 100644 --- a/src/lib/review/components/OpenCodeGoSettings.svelte +++ b/src/lib/review/components/OpenCodeGoSettings.svelte @@ -75,7 +75,7 @@ {#if disabled}

- Finish or pause the running review to change the key; saving restarts MOKE. + Finish or pause the running review to change the key; saving restarts Second Set.

{/if}

{message}

diff --git a/src/lib/review/components/ReviewWorkbench.svelte b/src/lib/review/components/ReviewWorkbench.svelte index 8c68c70a..7546ff0c 100644 --- a/src/lib/review/components/ReviewWorkbench.svelte +++ b/src/lib/review/components/ReviewWorkbench.svelte @@ -17,6 +17,7 @@ RevisionId, } from "#lib/review/contracts.ts"; import { newCommandId, reviewCommand, type ReviewClient } from "#lib/review/client.ts"; + import { readStoredSetting, storedSettingKeys } from "#lib/review/stored-setting.ts"; import CitationRow from "./CitationRow.svelte"; import CloudReviewControls from "./CloudReviewControls.svelte"; import { @@ -170,13 +171,11 @@ let firmChecklistMessage = $state(""); // The reviewer's model for Initial Review and new conversations, kept in this window's storage. - const preferredModelKey = "moke.preferredReviewModel"; + const preferredModelKey = storedSettingKeys.preferredReviewModel; let preferredModelChoice = $state( Option.getOrUndefined( - Schema.decodeUnknownOption(ReviewModelId)( - globalThis.localStorage?.getItem(preferredModelKey), - ), + Schema.decodeUnknownOption(ReviewModelId)(readStoredSetting(preferredModelKey)), ), ); @@ -904,7 +903,7 @@ if (!result.ok) return result.error.message; - return result.value ? "Saved. MOKE is restarting to connect the endpoint." : "Not saved."; + return result.value ? "Saved. Second Set is restarting to connect the endpoint." : "Not saved."; } async function removeCustomEndpoint() { @@ -912,7 +911,7 @@ if (!result.ok) return result.error.message; - return result.value ? "Removed. MOKE is restarting." : "The endpoint is still connected."; + return result.value ? "Removed. Second Set is restarting." : "The endpoint is still connected."; } async function saveOpenCodeGo(settings: typeof OpenCodeGoSettingsInput.Type) { @@ -920,7 +919,7 @@ if (!result.ok) return result.error.message; - return result.value ? "Saved. MOKE is restarting to connect OpenCode Go." : "Not saved."; + return result.value ? "Saved. Second Set is restarting to connect OpenCode Go." : "Not saved."; } async function removeOpenCodeGo() { @@ -928,7 +927,7 @@ if (!result.ok) return result.error.message; - return result.value ? "Removed. MOKE is restarting." : "OpenCode Go is still connected."; + return result.value ? "Removed. Second Set is restarting." : "OpenCode Go is still connected."; } function choosePreferredModel(value: string) { @@ -936,7 +935,7 @@ if (Option.isNone(chosen)) return; preferredModelChoice = chosen.value; - globalThis.localStorage?.setItem(preferredModelKey, chosen.value); + globalThis.localStorage?.setItem(preferredModelKey.key, chosen.value); } function openPacketFocus(focus: typeof PacketFindingFocus.Type) { @@ -1247,7 +1246,8 @@ steeringCommand = null; receivePacketReview(result.value); clearSubmittedDraft(submitted); - notice = "Follow-up queued for this review. MOKE will apply it at the next tool boundary."; + notice = + "Follow-up queued for this review. Second Set will apply it at the next tool boundary."; await refresh(); } else error = result.error.message; @@ -1445,7 +1445,7 @@ } try { - const staged = await window.mokeFiles.stageDrop(file); + const staged = await window.secondSetFiles.stageDrop(file); if (request !== dropRequest || !importDialog?.open) return; droppedPdf = staged; @@ -2079,7 +2079,7 @@ {#snippet mark(size = 24)} - + {/snippet} {#snippet packetPanel(review: PacketReview, historical = false)} @@ -2127,7 +2127,7 @@ aria-label="Toggle conversation sidebar" {...tip("Show or hide projects and conversations.")} onclick={toggleSidebar}>{@render mark()}MOKE + >{@render mark()}Second Set