Skip to content

Production cutover 1.0: manifest, doctor, OW4 /chat proxy, workspace image - #165

Merged
Travis-Gilbert merged 3 commits into
mainfrom
feat/production-cutover-1-0
Aug 3, 2026
Merged

Production cutover 1.0: manifest, doctor, OW4 /chat proxy, workspace image#165
Travis-Gilbert merged 3 commits into
mainfrom
feat/production-cutover-1-0

Conversation

@Travis-Gilbert

@Travis-Gilbert Travis-Gilbert commented Aug 3, 2026

Copy link
Copy Markdown
Owner

Summary

  • Grow .commonplace-canonical into the cutover register manifest and gate it in console CI.
  • Add production doctor (scripts/doctor.sh / /doctor / /api/doctor) with staging red proofs and authenticated upstream smokes on main.
  • Route /chat through middleware to the Railway workspace openwork door (OW4); collapse workspace chat onto /chat; stamp openwork.chat / model-canvas.owox.
  • Fix workspace image for Railway: drop Dockerfile VOLUME, Node 22 for pnpm stages, honor PORT=8787 for healthchecks.

Test plan

  • node scripts/check-register-manifest.mjs green
  • node scripts/doctor-staging-reds.mjs green
  • Chat route vitests green
  • Workspace https://commonplace-workspace-production.up.railway.app/health returns 200
  • After merge: redeploy commonplace-console; bash scripts/doctor.sh against https://v2.theoremharness.com
  • Live /chat shows data-register-impl="openwork.chat"
  • GL8 deletions only after live openwork observation

Summary by CodeRabbit

  • New Features
    • Added a production health dashboard showing service status, environment checks, route observations, and retirement warnings.
    • Chat access now uses a unified /chat entry point with workspace-aware routing and clearer fallback messaging.
    • Added inline message composition in the agent rail.
  • Bug Fixes
    • Improved workspace chat redirects and handling for unavailable or misconfigured chat services.
  • Quality Improvements
    • Added automated manifest validation, staging checks, production diagnostics, and authenticated smoke tests.
    • Updated deployment tooling and documentation for more reliable production cutovers.

…orkspace image.

Make v2 cutover enforceable: canonical register manifest + CI, doctor observers with staging reds and upstream smokes, /chat reverse-proxy to the workspace openwork door, and a Railway-compatible workspace image (no Dockerfile VOLUME, Node for pnpm, PORT=8787).
Copilot AI review requested due to automatic review settings August 3, 2026 15:07
@ecc-tools

ecc-tools Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

ECC bundle files are already tracked in this repository. Skipping generation of another bundle PR.

@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@Travis-Gilbert, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 40 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 74dc8c4d-f729-44ce-995f-7a89cdce172e

📥 Commits

Reviewing files that changed from the base of the PR and between b367aae and 39c6b56.

📒 Files selected for processing (2)
  • .commonplace-canonical
  • packaging/workspace/entrypoint.sh
📝 Walkthrough

Walkthrough

The pull request establishes a versioned production manifest, adds manifest and deployment health checks, routes console chat through OpenWork, exposes implementation markers, updates workspace packaging, and documents production cutover requirements.

Changes

CommonPlace production cutover

Layer / File(s) Summary
Canonical manifest and cutover contracts
.commonplace-canonical, .harness/checklists/..., CONVENTIONS.md, docs/plans/..., docs/records/..., scripts/assert-canonical-root.mjs, scripts/check-register-manifest.mjs, apps/console/package.json
The repository now uses a versioned canonical manifest. Validation, conventions, checklists, and cutover specifications define register ownership, production evidence, and retirement requirements.
Doctor validation and production gates
.github/workflows/production-doctor.yml, apps/console/src/app/api/doctor/route.ts, apps/console/src/app/doctor/page.tsx, scripts/doctor*.mjs, scripts/doctor.sh
The doctor API, page, scripts, staging proofs, upstream smokes, and workflow jobs check environment contracts, routes, implementation markers, upstream services, and retired files.
OpenWork chat routing
apps/console/src/app/chat/..., apps/console/src/app/workspace/..., apps/console/src/middleware.ts, apps/console/src/views/OpenworkChatRegister.tsx, apps/console/src/views/registry.tsx
Chat routes render the OpenWork register or redirect to /chat. Middleware proxies chat requests to the workspace service and marks HTML responses with openwork.chat.
Implementation identity and shell wiring
apps/console/src/lib/register-impl.ts, apps/console/src/components/blocks/*, apps/console/src/components/chat/ChatPage.tsx, apps/console/src/views/model/ModelView.tsx, apps/console/src/views/ThreadView.tsx, apps/console/src/lib/thread-runtime-available.ts
Descriptor registrations are exposed on rendered surfaces. The shared thread-runtime context moves to a library module. The agent rail uses an inline submission form.
Workspace image and port configuration
packaging/workspace/Dockerfile, packaging/workspace/entrypoint.sh
The workspace image uses Node 22 and pnpm through Corepack. Port selection now supports OPENWORK_PORT, PORT, and the default port. External volume persistence replaces the Dockerfile volume declaration.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ProductionDoctorWorkflow
  participant DoctorScripts
  participant ConsoleDoctorAPI
  participant ProductionServices
  ProductionDoctorWorkflow->>DoctorScripts: Run manifest and staging validation
  DoctorScripts->>ConsoleDoctorAPI: Probe /api/doctor
  ConsoleDoctorAPI->>ProductionServices: Probe registered routes and upstream services
  ProductionServices-->>ConsoleDoctorAPI: Return health observations
  ConsoleDoctorAPI-->>DoctorScripts: Return structured results
  DoctorScripts-->>ProductionDoctorWorkflow: Return pass or fail status
Loading
sequenceDiagram
  participant Browser
  participant ChatMiddleware
  participant WorkspaceService
  Browser->>ChatMiddleware: Request /chat
  ChatMiddleware->>WorkspaceService: Forward request data
  WorkspaceService-->>ChatMiddleware: Return chat response
  ChatMiddleware-->>Browser: Return response with openwork.chat marker
Loading

Possibly related PRs

Suggested reviewers: copilot

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 5.56% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the pull request's main changes: the production manifest, doctor checks, /chat proxy, and workspace image updates.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 2
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/production-cutover-1-0

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

code-server honors \$PORT over --bind-addr, so exporting PORT=8787 for Railway healthchecks made the IDE steal the chat door and fail the deploy.
@Travis-Gilbert

Copy link
Copy Markdown
Owner Author

Follow-up commit: code-server was binding $PORT (8787) and colliding with OpenWork. Entrypoint now starts code-server with env -u PORT while Railway still probes chat on 8787.

@ecc-tools

ecc-tools Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

ECC bundle files are already tracked in this repository. Skipping generation of another bundle PR.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR implements the “Production cutover 1.0” execution scaffolding by promoting .commonplace-canonical into a JSON manifest, adding a production-boundary “doctor” (script + /doctor + /api/doctor + CI workflow), routing /chat through a console-origin proxy to the Railway workspace openwork door (OW4), and adjusting the Railway workspace image build/runtime behavior.

Changes:

  • Promote .commonplace-canonical into a v1 JSON manifest and add CI gates to keep it consistent with registry.tsx and register-impl mappings.
  • Add a production doctor across scripts + console routes, and wire it into a deploy-blocking GitHub Actions workflow.
  • Implement OW4 /chat fetch-proxy middleware and update workspace chat routes/tests; update workspace Dockerfile + entrypoint for Railway constraints.

Reviewed changes

Copilot reviewed 37 out of 37 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
scripts/doctor.sh Bash entrypoint delegating to scripts/doctor.mjs.
scripts/doctor.mjs CLI doctor that probes production health, manifest routes, and upstreams.
scripts/doctor-upstream-smokes.mjs Unauth health + auth smokes for cutover upstream services.
scripts/doctor-staging-reds.mjs “Fail closed” staging-red proofs for manifest/doctor assumptions.
scripts/check-register-manifest.mjs Gate: validates .commonplace-canonical vs registry.tsx + register-impl map.
scripts/assert-canonical-root.mjs Extends canonical-root gate to validate JSON schema when present.
packaging/workspace/entrypoint.sh Prefer PORT for Railway healthchecks and export it for the service.
packaging/workspace/Dockerfile Fix build stages for pnpm workspace + remove VOLUME for Railway.
docs/records/013-vscode-surface.md Adds GL9 production-boundary amendment note.
docs/records/012-twenty-ui-fork.md Adds GL9 production-boundary amendment note.
docs/plans/console/SPEC-COMMONPLACE-OPENWORK-FORK-1.0.md Adds GL9 amendment note.
docs/plans/console/SPEC-COMMONPLACE-MODEL-CANVAS-FORK-1.0.md Adds GL9 amendment note.
docs/plans/commonplace-production-cutover/SPEC-COMMONPLACE-PRODUCTION-CUTOVER-1.0.md New cutover spec defining GL1–GL9 deliverables and acceptance.
docs/plans/commonplace-production-cutover/EXECUTE-REPORT.md New execution report tracking progress and risks.
CONVENTIONS.md New conventions file including GL9 production-boundary rules.
apps/console/src/views/ThreadView.tsx Moves ThreadRuntimeAvailable context out to a stable lib export.
apps/console/src/views/registry.tsx Repoints chat descriptors to OpenworkChatRegister; updates sourcing metadata.
apps/console/src/views/OpenworkChatRegister.tsx New fallback “openwork chat register” UI with data-register-impl.
apps/console/src/views/model/ModelView.tsx Stamps model surface with data-register-impl="model-canvas.owox".
apps/console/src/middleware.ts New OW4 /chat reverse-proxy middleware (console-origin fetch proxy).
apps/console/src/lib/thread-runtime-available.ts New shared ThreadRuntimeAvailable context module.
apps/console/src/lib/register-impl.ts New descriptor-id → manifest impl mapping used for data-register-impl.
apps/console/src/components/ConsoleApp.tsx Updates import for ThreadRuntimeAvailable.
apps/console/src/components/chat/ChatPage.tsx Stamps assistant-ui ChatPage with data-register-impl for superseded tracking.
apps/console/src/components/blocks/BlockShell.tsx Stamps block shell with data-register-impl derived from descriptor id.
apps/console/src/components/blocks/AgentRailBlock.tsx Replaces embedded composer with lightweight submit form using submitThreadText.
apps/console/src/app/workspace/[workspaceSlug]/chat/page.tsx Collapses workspace chat route onto /chat (OW4 proxy owns the body).
apps/console/src/app/workspace/[workspaceSlug]/chat/page.test.tsx Updates tests to assert redirects rather than server-rendered ChatPage.
apps/console/src/app/doctor/page.tsx New human-glance doctor page reading /api/doctor.
apps/console/src/app/chat/page.tsx Keeps /chat as the canonical entry route; returns openwork fallback when proxy absent.
apps/console/src/app/chat/[threadId]/page.tsx Thread route fallback to openwork register when proxy absent.
apps/console/src/app/chat/[threadId]/page.test.tsx Updates tests to assert fallback register behavior and redirect behavior.
apps/console/src/app/api/doctor/route.ts New server-side doctor endpoint with env + route + resurrection inventory.
apps/console/package.json Adds gate:register-manifest and includes it in gates.
.harness/checklists/commonplace-production-cutover--plan-local-20260803.json Adds local harness checklist for GL tracking.
.github/workflows/production-doctor.yml New workflow: staging reds on PRs, live doctor + auth upstream smokes on main.
.commonplace-canonical Converts sentinel marker into v1 JSON manifest including registers/services/env contract.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +20 to +31
const headers = new Headers();
const accept = request.headers.get('accept');
const requestContentType = request.headers.get('content-type');
const cookie = request.headers.get('cookie');
if (accept) headers.set('accept', accept);
if (requestContentType) headers.set('content-type', requestContentType);
if (cookie) headers.set('cookie', cookie);
headers.set('x-forwarded-host', request.headers.get('host') ?? '');
headers.set('x-forwarded-proto', request.nextUrl.protocol.replace(':', ''));
const token = process.env.CONSOLE_WORKSPACE_TOKEN?.trim();
if (token) headers.set('authorization', `Bearer ${token}`);

Comment on lines +59 to +75
const upstreamContentType = upstream.headers.get('content-type') ?? '';
const responseHeaders = new Headers(upstream.headers);
responseHeaders.set('x-register-impl', 'openwork.chat');

if (upstreamContentType.includes('text/html')) {
const html = await upstream.text();
const stamped = html.includes('data-register-impl=')
? html
: html.replace(
/<html([^>]*)>/i,
'<html$1 data-register-impl="openwork.chat">',
);
return new NextResponse(stamped, {
status: upstream.status,
headers: responseHeaders,
});
}
Comment on lines +55 to +60
async function probeRoute(base: string, route: string): Promise<{ status: number; impl: string | null; body: string }> {
const url = route === '/' ? base : `${base}${route}`;
const response = await fetch(url, { redirect: 'manual' });
const body = await response.text();
return { status: response.status, impl: extractImpl(body), body };
}
Comment on lines +28 to +32
async function probe(url, init) {
const response = await fetch(url, init);
const text = await response.text();
return { response, text };
}

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b367aaeab5

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +29 to +30
const token = process.env.CONSOLE_WORKSPACE_TOKEN?.trim();
if (token) headers.set('authorization', `Bearer ${token}`);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Authenticate before injecting the workspace token

When CONSOLE_WORKSPACE_URL and CONSOLE_WORKSPACE_TOKEN are configured, middleware intercepts /chat/* before the page's resolveHarnessPrincipal() check and attaches a shared collaborator bearer token to every request. Consequently, an unauthenticated caller can request paths such as /chat/workspaces or mutation endpoints, have them translated to the corresponding workspace API route, and act with the injected token; forwarding the cookie does not mitigate this because the workspace container explicitly refuses the console cookie-signing secret. Validate the console session and workspace membership before proxying or injecting the credential.

AGENTS.md reference: apps/console/AGENTS.md:L119-L121

Useful? React with 👍 / 👎.

Comment on lines +83 to +85
export const config = {
matcher: ['/chat', '/chat/:path*'],
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Route the Openwork app's root-relative requests through the proxy

With the workspace proxy enabled, /chat returns the Vite app, but the matcher only proxies URLs beginning with /chat. The app is built with base: "/", so its HTML requests /assets/*, and resolveOpenworkConnection() selects window.location.origin, causing API calls such as /session/console and /workspaces; all of those go to the Next console instead of the workspace service. A normal /chat load therefore cannot fetch its bundle or use its API unless the build and connection base are changed to /chat or the required root paths are also proxied.

Useful? React with 👍 / 👎.

Comment on lines +169 to +173
const pass =
response.status === 200 ||
response.status === 401 ||
response.status === 403 ||
response.status === 404;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Require authenticated probes to succeed

When the proactivity token is expired, incorrect, or the configured endpoint is missing, this "authenticated" smoke treats 401, 403, and even 404 as success, so the production-doctor workflow can report green without establishing either authentication or endpoint availability. The same false-positive pattern appears in authDataApi() and authHarness(); authenticated checks should require the service's expected successful response rather than merely proving that some HTTP door answered.

Useful? React with 👍 / 👎.

Comment thread .commonplace-canonical
}
],
"retired": [],
"env_contract": [

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Add the workspace proxy credentials to the env contract

The new canonical chat path depends on CONSOLE_WORKSPACE_URL and CONSOLE_WORKSPACE_TOKEN, but neither appears in env_contract, and /api/doctor derives all required-environment checks solely from this list. In particular, a deployment can have the URL configured but omit the token, still return and stamp the public Openwork HTML for /chat, and be reported green even though workspace API requests cannot authenticate. Include both proxy variables in the required contract so the cutover doctor fails closed.

Useful? React with 👍 / 👎.

Comment on lines +95 to +97
const ok =
(probed.status >= 200 && probed.status < 400 && probed.impl === row.manifest_impl) ||
(probed.status >= 200 && probed.status < 400 && loginish && probed.impl === null);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Observe register implementations after client hydration

For the Console routes in the manifest, this check cannot observe the implementation it claims to validate: /records, /Data-model, /documents, /workspace, and / all render ConsoleApp, whose server snapshot is only the aria-busy placeholder, while data-register-impl is added by client-rendered BlockShell instances after hydration. Because probeRoute() merely scans the raw response HTML, these healthy routes produce observed_impl: null; alternatively, the loginish branch accepts login HTML with no implementation marker at all. The production doctor will therefore either reject a healthy deployment or pass an authentication page without proving the registered surface, so this needs a server-visible route stamp or a browser-based authenticated probe.

Useful? React with 👍 / 👎.

Comment on lines +73 to +80
- name: Wait for console health
run: |
for i in $(seq 1 30); do
code=$(curl -sS -o /tmp/healthz.json -w '%{http_code}' https://v2.theoremharness.com/api/healthz || true)
if [ "$code" = "200" ]; then
cat /tmp/healthz.json
exit 0
fi

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Run the live doctor after the matching deployment

On a main push, this health loop exits as soon as the currently deployed Console returns 200, without checking that production is running the commit which triggered the workflow. The workflow therefore races Railway and usually tests the previous revision; moreover, the file explicitly says Railway should wait for this check, which creates a cycle where the new /doctor implementation cannot deploy until a check requiring that implementation has passed. Trigger the live job from deployment completion or wait for a revision/build identifier matching github.sha before running the doctor.

Useful? React with 👍 / 👎.

Comment on lines +81 to +86
const doctorApi = path.join(repoRoot, 'apps/console/src/app/api/doctor/route.ts');
const doctorSource = readFileSync(doctorApi, 'utf8');
if (!/pending_retirement|resurrection|retired/.test(doctorSource)) {
fail('doctor API missing resurrection / pending_retirement handling');
}
console.log('ok red.doctor_source: doctor API carries resurrection checks');

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Execute the resurrection red instead of scanning source

This staging proof never invokes the doctor or asserts a red result; it only searches the route source for any of three words. In the committed manifest the condition at line 69 is also false because the chat superseded entry already has paths, so the planted corpse is never added to retired and would not be part of the doctor's inventory anyway. The CI step can therefore remain green even if resurrection handling always reports success; construct a manifest that names the corpse and execute the actual observation logic, asserting a failing status.

Useful? React with 👍 / 👎.

// SOURCING: none. Chat page route (CH1). Unscoped /chat redirects into the
// active workspace chat once membership and scope are verified.
// SOURCING: none. Chat index stays on /chat so OW4 middleware can reverse-proxy
// the workspace openwork door. Do not redirect into /workspace/*/chat — that

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Replace the banned em dash in the chat route comment

This newly added comment contains an em dash, but the scoped Console constitution explicitly bans em and en dashes in code comments as well as UI strings and Markdown. Replace it with permitted punctuation so the changed file follows the app's writing rule.

AGENTS.md reference: apps/console/AGENTS.md:L169-L172

Useful? React with 👍 / 👎.

@ecc-tools

ecc-tools Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

ECC bundle files are already tracked in this repository. Skipping generation of another bundle PR.

@Travis-Gilbert
Travis-Gilbert merged commit 02cd964 into main Aug 3, 2026
5 of 10 checks passed

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 19

🧹 Nitpick comments (7)
packaging/workspace/Dockerfile (1)

16-18: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Pin the Node image used by both stages.

node:22-bookworm-slim is a mutable tag. Docker recommends digest pinning for reproducible image content. (docs.docker.com) The final stage also repeats the image reference at Lines 59-60. Use one immutable, verified reference and copy the runtime files from that named stage.

Proposed change
-FROM node:22-bookworm-slim AS web
+FROM node:22-bookworm-slim@sha256:<verified-digest> AS web
...
-COPY --from=node:22-bookworm-slim /usr/local/bin/node /usr/local/bin/node
-COPY --from=node:22-bookworm-slim /usr/local/lib/node_modules /usr/local/lib/node_modules
+COPY --from=web /usr/local/bin/node /usr/local/bin/node
+COPY --from=web /usr/local/lib/node_modules /usr/local/lib/node_modules

Also applies to: 57-66

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packaging/workspace/Dockerfile` around lines 16 - 18, Pin the Node base image
used by both the web and final stages to one verified immutable digest, define
that reference once, and reuse the named web stage for copying runtime files
instead of repeating the mutable image reference. Update the stage declarations
around the web stage and the final-stage COPY flow while preserving the existing
runtime contents.

Source: MCP tools

.github/workflows/production-doctor.yml (1)

45-46: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Both checkout steps persist the job token. actions/checkout@v4 stores the credential in .git/config by default. No step in this workflow pushes to the repository, and both jobs execute repository scripts.

  • .github/workflows/production-doctor.yml#L45-L46: add with: persist-credentials: false to the staging-reds checkout.
  • .github/workflows/production-doctor.yml#L65-L66: add with: persist-credentials: false to the doctor checkout.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/production-doctor.yml around lines 45 - 46, Disable
credential persistence for both checkout steps in
.github/workflows/production-doctor.yml at lines 45-46 (staging-reds) and 65-66
(doctor) by configuring each actions/checkout@v4 step with persist-credentials
set to false.

Source: Linters/SAST tools

scripts/check-register-manifest.mjs (2)

81-87: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

The gate validates by literal source text, so formatting changes change gate coverage. Both checks assume single quotes, one space after each colon, and specific newline placement in TypeScript sources. A formatter run or a quote-style change either silences the swap rule or reports every mapping as missing.

  • scripts/check-register-manifest.mjs#L81-L87: accept both quote styles and flexible whitespace when extracting descriptor ids, or read ids from a generated JSON export of the registry.
  • scripts/check-register-manifest.mjs#L132-L139: replace the implSource.includes(expected) literal match with a regex that allows either quote style and flexible whitespace around the colon.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/check-register-manifest.mjs` around lines 81 - 87, Make the manifest
checks formatting-tolerant: in scripts/check-register-manifest.mjs lines 81-87,
update the registry id extraction used by registryIds to accept both quote
styles and flexible whitespace; in lines 132-139, replace the
implSource.includes(expected) literal comparison with a regex allowing either
quote style and flexible whitespace around the colon.

110-112: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Add a production_route uniqueness check.

The loop rejects duplicate registry_entry values but accepts duplicate production_route values. .commonplace-canonical currently maps both editor and plan to /workspace, and scripts/doctor.mjs cannot satisfy two impl stamps on one route. A uniqueness check here catches that class of manifest error in CI.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/check-register-manifest.mjs` around lines 110 - 112, Add duplicate
detection for row.production_route in the manifest validation loop alongside the
existing required-field check, tracking previously seen routes and pushing a
validation error when a route repeats. Preserve the existing registry_entry
uniqueness behavior and missing-production_route validation.
scripts/doctor-upstream-smokes.mjs (1)

47-144: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Authenticated smokes lack the timeout pattern used by the SSE probe.

authGraphql, authDataApi, and authHarness call fetch with no AbortController/timeout, unlike authProactivity (Lines 146-185), which already uses one. A hung upstream here stalls the whole script until the CI job's own timeout intervenes, instead of failing fast with a clear per-probe message.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/doctor-upstream-smokes.mjs` around lines 47 - 144, Add the same
AbortController-based timeout pattern used by authProactivity to authGraphql,
authDataApi, and authHarness, passing the signal and timeout to each probe
request and cleaning up the timer afterward. Ensure timeout failures are caught
and recorded through each probe’s existing record call with a clear per-probe
error message.
apps/console/src/app/doctor/page.tsx (1)

8-44: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider extracting doctor logic into a shared module instead of an SSR self-fetch.

This page re-declares DoctorPayload to mirror the API route's EnvRow/RouteRow/ResurrectionRow types, then fetches its own /api/doctor route over HTTP during server rendering. Extracting the manifest read, env check, and route-probe logic into a shared module (e.g. lib/doctor.ts) that both route.ts and this page call directly would remove the network hop, remove the type duplication, and remove the dependency on DOCTOR_PUBLIC_BASE_URL/RAILWAY_PUBLIC_DOMAIN resolving identically from two separate call sites.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/console/src/app/doctor/page.tsx` around lines 8 - 44, Extract the doctor
manifest, environment validation, and route-probe logic from the API route into
a shared module such as lib/doctor.ts, and have both route.ts and loadDoctor
call that module directly during server rendering. Reuse the shared result types
instead of redeclaring DoctorPayload and its nested rows in page.tsx, and remove
the page’s SSR self-fetch plus DOCTOR_PUBLIC_BASE_URL/RAILWAY_PUBLIC_DOMAIN
base-resolution logic while preserving the existing response and error behavior.
apps/console/src/views/registry.tsx (1)

208-259: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Use an existing package for the chat source metadata.

chat.thread, chat.surface, and thread.list point source.package to @commonplace/chat, but no workspace package declares that package and OpenworkChatRegister is imported from ./OpenworkChatRegister inside apps/console. Use an existing package path, or add the package definition consistently before resolving through the library ledger.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/console/src/views/registry.tsx` around lines 208 - 259, Update the
source metadata for CHAT_THREAD, CHAT_SURFACE, and THREAD_LIST to use the
existing package path that owns the locally imported OpenworkChatRegister, or
consistently add `@commonplace/chat` to the workspace package definitions before
ledger resolution. Ensure all three descriptors resolve through the same valid
library-ledger package entry.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.commonplace-canonical:
- Around line 134-159: Resolve the duplicate production route declared by the
editor and plan register entries in .commonplace-canonical. Either assign
distinct production_route values, or update the /workspace document and its
doctor validation path so it carries both manifest_impl stamps; ensure
checkRegisterRoutes can satisfy both console.code.file and console.goal.stack
without relying on the loginish fallback.

In @.github/workflows/production-doctor.yml:
- Around line 73-89: Update the “Wait for console health” step to require both
HTTP 200 and the deployed commit identity from /api/healthz matching github.sha
before succeeding; otherwise continue polling and fail after the existing
retries. Add curl’s --max-time option to bound each health request, and expose
or parse the health response’s commit SHA without changing the subsequent
doctor.sh invocation.

In `@apps/console/src/app/api/doctor/route.ts`:
- Around line 55-60: Update probeRoute to use an AbortController-based timeout
with fetch, matching the existing timeout pattern in the doctor upstream smoke
script, and ensure the signal is cleaned up after each probe. Update the
manifest probe loop around probeRoute to run probes concurrently rather than
awaiting them sequentially, while preserving the existing result collection and
response behavior.
- Around line 119-148: Update the ok calculation in the doctor route to fail
when any resurrections entry from manifest.retired has pending_retirement false
and absent false, while preserving the existing reporting for superseded
entries. Include this resurrection check alongside envOk and routesOk so
restored retired files make the doctor red and deploy-blocking.
- Line 57: Update the sign-in fallback around the fetch using redirect: 'manual'
to recognize a real 3xx response by checking response.headers.get('location').
Accept the redirect-based auth gate without requiring probed.impl === null,
while preserving the existing response/body handling for other cases.

In `@apps/console/src/app/chat/page.tsx`:
- Around line 15-17: Update the unavailable-state branch in the chat page to
derive settingsHref from resolution.principal.workspaceId: pass
/workspace/${resolution.principal.workspaceId}/settings when a workspace ID
exists, and retain null only when no workspace is known. Keep the existing
ChatUnavailable condition and rendering behavior otherwise unchanged.

In `@apps/console/src/app/doctor/page.tsx`:
- Around line 25-44: Add an explicit timeout to the fetch in loadDoctor by using
an AbortController or equivalent signal with a bounded timeout, and ensure
timeout failures follow the existing catch fallback. Keep the request URL and
no-store caching behavior unchanged.

In `@apps/console/src/components/blocks/AgentRailBlock.tsx`:
- Line 14: Update the AgentRailBlock rail-field submission flow to route
submitted text through submitThreadText, preserving the existing thread-submit
contract and ensuring /do action instructions are sent as user input rather than
opening the action sheet. If a native form handler remains, add equivalent /do
routing there.

In `@apps/console/src/lib/register-impl.ts`:
- Around line 19-25: Update registerImplForDescriptor to verify descriptorId is
an own key of REGISTER_IMPL_BY_DESCRIPTOR before indexing it; return undefined
for inherited or unknown keys such as constructor and __proto__, while
preserving the existing empty-id behavior and valid descriptor lookups.

In `@apps/console/src/middleware.ts`:
- Around line 44-48: Update the catch block in the middleware’s `/chat` request
handling to log the caught error server-side, then replace the interpolated
error details in the 502 HTML response with a generic workspace-unavailable
message. Do not expose `error.message` or its stringified equivalent to
unauthenticated callers.
- Around line 17-18: Update the upstream target construction around upstreamPath
and target to prevent paths beginning with // from becoming a different URL
host; reject or canonicalize doubled leading slashes after removing /chat. Build
the URL from WORKSPACE by assigning the sanitized pathname and
request.nextUrl.search separately, preserving the intended proxy origin.
- Around line 41-43: Update the upstream proxy fetch in the middleware around
fetch(target, init) to use an AbortController with an explicit timeout, passing
its signal through the request init and clearing the timer after completion.
Ensure timeout-triggered aborts follow the existing upstream error handling
path.

In `@apps/console/src/views/OpenworkChatRegister.tsx`:
- Line 20: Update the inline font weight on the “Openwork chat register” element
in OpenworkChatRegister to use the appropriate Int UI weight token, keeping the
element within the existing Int UI token system and removing the
--rec-weight-cap reference.
- Around line 22-27: Replace the internal-navigation anchor in
OpenworkChatRegister with Next.js’s Link component imported from next/link,
preserving the existing className, href="/chat", and link text.

In `@packaging/workspace/entrypoint.sh`:
- Around line 18-21: Update the entrypoint port initialization and related
Dockerfile configuration: remove the image-level OPENWORK_PORT default, export
the shell-resolved port variable without overriding runtime-provided PORT
values, and make the healthcheck use that same resolved port rather than
performing a separate OPENWORK_PORT lookup.

In `@scripts/doctor-staging-reds.mjs`:
- Around line 58-89: The Red 2 proof must execute the doctor’s
retirement/resurrection evaluation instead of grepping route source. Extract the
relevant logic from the doctor API route into an importable function or module,
have the route reuse it, and update this script to invoke that function against
the mutated marker and planted corpse, asserting a non-green result.
- Around line 62-65: Update the generated content in writeFileSync to replace
the em dash in the corpse file comment with allowed punctuation, while
preserving the rest of the comment and export statement.

In `@scripts/doctor.mjs`:
- Around line 71-74: Replace the broad body-text-based loginish fallback near
the route check with validation of an actual authentication redirect: compare
response.url to the expected sign-in path, or use a manual redirect request and
accept only a 3xx redirect targeting that path. Ensure routes without the
cutover marker cannot pass merely because their HTML contains terms such as
“login” or “callbackUrl”.
- Around line 41-48: Update fetchText to apply an abort timeout to every fetch
request, including calls that provide init options, so hung probes terminate
within a bounded interval and report failure. Use the existing fetch signal when
present or compose the timeout signal without overriding caller configuration.

---

Nitpick comments:
In @.github/workflows/production-doctor.yml:
- Around line 45-46: Disable credential persistence for both checkout steps in
.github/workflows/production-doctor.yml at lines 45-46 (staging-reds) and 65-66
(doctor) by configuring each actions/checkout@v4 step with persist-credentials
set to false.

In `@apps/console/src/app/doctor/page.tsx`:
- Around line 8-44: Extract the doctor manifest, environment validation, and
route-probe logic from the API route into a shared module such as lib/doctor.ts,
and have both route.ts and loadDoctor call that module directly during server
rendering. Reuse the shared result types instead of redeclaring DoctorPayload
and its nested rows in page.tsx, and remove the page’s SSR self-fetch plus
DOCTOR_PUBLIC_BASE_URL/RAILWAY_PUBLIC_DOMAIN base-resolution logic while
preserving the existing response and error behavior.

In `@apps/console/src/views/registry.tsx`:
- Around line 208-259: Update the source metadata for CHAT_THREAD, CHAT_SURFACE,
and THREAD_LIST to use the existing package path that owns the locally imported
OpenworkChatRegister, or consistently add `@commonplace/chat` to the workspace
package definitions before ledger resolution. Ensure all three descriptors
resolve through the same valid library-ledger package entry.

In `@packaging/workspace/Dockerfile`:
- Around line 16-18: Pin the Node base image used by both the web and final
stages to one verified immutable digest, define that reference once, and reuse
the named web stage for copying runtime files instead of repeating the mutable
image reference. Update the stage declarations around the web stage and the
final-stage COPY flow while preserving the existing runtime contents.

In `@scripts/check-register-manifest.mjs`:
- Around line 81-87: Make the manifest checks formatting-tolerant: in
scripts/check-register-manifest.mjs lines 81-87, update the registry id
extraction used by registryIds to accept both quote styles and flexible
whitespace; in lines 132-139, replace the implSource.includes(expected) literal
comparison with a regex allowing either quote style and flexible whitespace
around the colon.
- Around line 110-112: Add duplicate detection for row.production_route in the
manifest validation loop alongside the existing required-field check, tracking
previously seen routes and pushing a validation error when a route repeats.
Preserve the existing registry_entry uniqueness behavior and
missing-production_route validation.

In `@scripts/doctor-upstream-smokes.mjs`:
- Around line 47-144: Add the same AbortController-based timeout pattern used by
authProactivity to authGraphql, authDataApi, and authHarness, passing the signal
and timeout to each probe request and cleaning up the timer afterward. Ensure
timeout failures are caught and recorded through each probe’s existing record
call with a clear per-probe error message.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 21165cde-ea0c-4d61-a769-1a353fba2d61

📥 Commits

Reviewing files that changed from the base of the PR and between 4477274 and b367aae.

📒 Files selected for processing (37)
  • .commonplace-canonical
  • .github/workflows/production-doctor.yml
  • .harness/checklists/commonplace-production-cutover--plan-local-20260803.json
  • CONVENTIONS.md
  • apps/console/package.json
  • apps/console/src/app/api/doctor/route.ts
  • apps/console/src/app/chat/[threadId]/page.test.tsx
  • apps/console/src/app/chat/[threadId]/page.tsx
  • apps/console/src/app/chat/page.tsx
  • apps/console/src/app/doctor/page.tsx
  • apps/console/src/app/workspace/[workspaceSlug]/chat/page.test.tsx
  • apps/console/src/app/workspace/[workspaceSlug]/chat/page.tsx
  • apps/console/src/components/ConsoleApp.tsx
  • apps/console/src/components/blocks/AgentRailBlock.tsx
  • apps/console/src/components/blocks/BlockShell.tsx
  • apps/console/src/components/chat/ChatPage.tsx
  • apps/console/src/lib/register-impl.ts
  • apps/console/src/lib/thread-runtime-available.ts
  • apps/console/src/middleware.ts
  • apps/console/src/views/OpenworkChatRegister.tsx
  • apps/console/src/views/ThreadView.tsx
  • apps/console/src/views/model/ModelView.tsx
  • apps/console/src/views/registry.tsx
  • docs/plans/commonplace-production-cutover/EXECUTE-REPORT.md
  • docs/plans/commonplace-production-cutover/SPEC-COMMONPLACE-PRODUCTION-CUTOVER-1.0.md
  • docs/plans/console/SPEC-COMMONPLACE-MODEL-CANVAS-FORK-1.0.md
  • docs/plans/console/SPEC-COMMONPLACE-OPENWORK-FORK-1.0.md
  • docs/records/012-twenty-ui-fork.md
  • docs/records/013-vscode-surface.md
  • packaging/workspace/Dockerfile
  • packaging/workspace/entrypoint.sh
  • scripts/assert-canonical-root.mjs
  • scripts/check-register-manifest.mjs
  • scripts/doctor-staging-reds.mjs
  • scripts/doctor-upstream-smokes.mjs
  • scripts/doctor.mjs
  • scripts/doctor.sh

Comment thread .commonplace-canonical
Comment on lines +134 to +159
{
"id": "editor",
"canonical_package": "apps/console CodeFileView + editor-model seam (+ vscode-surface pack when parked/live)",
"production_route": "/workspace",
"registry_entry": "code.file",
"manifest_impl": "console.code.file",
"superseded": [],
"parked": [
{
"impl": "vscode-surface.pack",
"reason": "PR #162/#163 merged; V7 desktop/web workbench smoke still pending before doctor can observe a live IDE door.",
"prs": [
162,
163
]
}
]
},
{
"id": "plan",
"canonical_package": "apps/console GoalStackView",
"production_route": "/workspace",
"registry_entry": "goal.stack",
"manifest_impl": "console.goal.stack",
"superseded": []
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Two registers claim the same production_route, which the doctor cannot both satisfy.

editor and plan both declare production_route: "/workspace" with distinct manifest_impl values (console.code.file, console.goal.stack). scripts/doctor.mjs checkRegisterRoutes fetches each row's route and requires data-register-impl="<manifest_impl>" in the HTML. A single /workspace response can carry only one of the two stamps, so one row stays red unless the loginish fallback masks it. Either stamp both impl ids on the /workspace document, or give each register a distinct route.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.commonplace-canonical around lines 134 - 159, Resolve the duplicate
production route declared by the editor and plan register entries in
.commonplace-canonical. Either assign distinct production_route values, or
update the /workspace document and its doctor validation path so it carries both
manifest_impl stamps; ensure checkRegisterRoutes can satisfy both
console.code.file and console.goal.stack without relying on the loginish
fallback.

Comment on lines +73 to +89
- name: Wait for console health
run: |
for i in $(seq 1 30); do
code=$(curl -sS -o /tmp/healthz.json -w '%{http_code}' https://v2.theoremharness.com/api/healthz || true)
if [ "$code" = "200" ]; then
cat /tmp/healthz.json
exit 0
fi
sleep 10
done
echo "console health did not become ready"
exit 1

- name: Run doctor.sh against production
run: bash scripts/doctor.sh
env:
DOCTOR_BASE_URL: https://v2.theoremharness.com

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

The health wait does not prove the new commit is live, so the doctor can grade the previous deploy.

On a push to main, this job starts immediately. The already-running deployment returns 200 from /api/healthz, so the loop exits on the first attempt. scripts/doctor.mjs then checks routes and impl markers against the old build. The job reports green for a cutover that is not deployed yet.

Gate the wait on deploy identity, not liveness. Return the commit SHA from /api/healthz and loop until it equals github.sha. Alternatively, trigger this job from a Railway deployment success event rather than from the push.

Also add --max-time to the curl call so one hung request does not consume a full 10 second interval plus an unbounded connect wait.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/production-doctor.yml around lines 73 - 89, Update the
“Wait for console health” step to require both HTTP 200 and the deployed commit
identity from /api/healthz matching github.sha before succeeding; otherwise
continue polling and fail after the existing retries. Add curl’s --max-time
option to bound each health request, and expose or parse the health response’s
commit SHA without changing the subsequent doctor.sh invocation.

Comment on lines +55 to +60
async function probeRoute(base: string, route: string): Promise<{ status: number; impl: string | null; body: string }> {
const url = route === '/' ? base : `${base}${route}`;
const response = await fetch(url, { redirect: 'manual' });
const body = await response.text();
return { status: response.status, impl: extractImpl(body), body };
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

No timeout on production route probes; a hung upstream can block this request indefinitely.

probeRoute (Lines 55-60) calls fetch(url, { redirect: 'manual' }) with no AbortSignal/timeout. The loop at Lines 90-117 then awaits this sequentially, once per manifest register. If the /chat OW4 proxy or any other probed route hangs (e.g., a slow Railway workspace door), this dynamic = 'force-dynamic' route has no bound on total request time, and doctor/page.tsx (which itself fetches this route with no timeout) inherits the hang.

scripts/doctor-upstream-smokes.mjs already uses AbortController with a timeout for its SSE probe; apply the same pattern here.

🔒️ Proposed fix to add a timeout and probe concurrently
 async function probeRoute(base: string, route: string): Promise<{ status: number; impl: string | null; body: string }> {
   const url = route === '/' ? base : `${base}${route}`;
-  const response = await fetch(url, { redirect: 'manual' });
+  const response = await fetch(url, { redirect: 'manual', signal: AbortSignal.timeout(5000) });
   const body = await response.text();
   return { status: response.status, impl: extractImpl(body), body };
 }
-  const routes: RouteRow[] = [];
-  for (const row of manifest.registers ?? []) {
-    try {
-      const probed = await probeRoute(base, row.production_route);
-      ...
-    } catch (error) {
-      routes.push({...});
-      void error;
-    }
-  }
+  const routes: RouteRow[] = await Promise.all(
+    (manifest.registers ?? []).map(async (row) => {
+      try {
+        const probed = await probeRoute(base, row.production_route);
+        const loginish = /callbackUrl|sign.?in|\/login/i.test(probed.body);
+        const ok = /* unchanged */;
+        return { id: row.id, route: row.production_route, expected_impl: row.manifest_impl, status: probed.status, observed_impl: probed.impl, ok };
+      } catch (error) {
+        console.error(`doctor: probe failed for ${row.id}`, error);
+        return { id: row.id, route: row.production_route, expected_impl: row.manifest_impl, status: null, observed_impl: null, ok: false };
+      }
+    }),
+  );

Also applies to: 90-117

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/console/src/app/api/doctor/route.ts` around lines 55 - 60, Update
probeRoute to use an AbortController-based timeout with fetch, matching the
existing timeout pattern in the doctor upstream smoke script, and ensure the
signal is cleaned up after each probe. Update the manifest probe loop around
probeRoute to run probes concurrently rather than awaiting them sequentially,
while preserving the existing result collection and response behavior.


async function probeRoute(base: string, route: string): Promise<{ status: number; impl: string | null; body: string }> {
const url = route === '/' ? base : `${base}${route}`;
const response = await fetch(url, { redirect: 'manual' });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

Node.js 22 undici fetch redirect manual response status and body behavior

💡 Result:

In Node.js 22, the Undici-based fetch implementation handles the redirect: 'manual' option by returning the actual HTTP response from the server, rather than the opaque-redirect filtered response defined by the web browser Fetch standard [1][2]. Key behaviors include: 1. Status and Body: Unlike browser fetch—which returns an opaque-redirect response (status 0, null body, empty headers) [3]—Undici returns the complete response, including the actual 3xx status code (e.g., 301, 302, 307, 308) [1][4]. You can read the response body and access the Location header to determine where the redirect points [5][4]. 2. Rationale: This approach is designed to align with server-side environments like Deno and Cloudflare Workers, where it is not possible for the client to automatically handle certain redirect scenarios that might be expected in a browser [1][2]. 3. Implementation: This is a deviation from strict WHATWG Fetch specification compliance regarding the 'manual' redirect mode, which mandates an opaque-redirect response [1][6][4]. Because Undici returns the full response, the developer is responsible for manually inspecting the status, parsing the Location header, and initiating a new request if they wish to follow the redirect [5][4]. In summary, when you use { redirect: 'manual' } in Node.js 22, you receive the full 3xx response object, allowing you to access the status, headers, and body just as you would for any other successful fetch request [1][4].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Tracked route files:"
git ls-files | rg 'apps/console/src/app/api/doctor/route\.ts$|apps/console' | head -50

echo
echo "Target route excerpt:"
if [ -f apps/console/src/app/api/doctor/route.ts ]; then
  wc -l apps/console/src/app/api/doctor/route.ts
  sed -n '1,140p' apps/console/src/app/api/doctor/route.ts | cat -n
fi

echo
echo "Package/runtime declarations:"
for f in apps/package.json package.json apps/console/package.json apps/console/pnpm-workspace.yaml pnpm-workspace.yaml; do
  [ -f "$f" ] && { echo "--- $f"; sed -n '1,120p' "$f"; }
done

echo
echo "Search for auth redirect/sign-in patterns under apps/console:"
rg -n --glob '!**/node_modules/**' 'Location|callbackUrl|sign(.* )?in|/login|THEOREM_|CONSOLE_HARNESS_URL|fetch\(|redirect:\s*["'\'']manual["'\'']' apps/console/src apps/console/app 2>/dev/null | head -200

Repository: Travis-Gilbert/CommonPlace

Length of output: 3055


🏁 Script executed:

#!/bin/bash
set -euo fish -o pipefail

echo "Repository top files:"
git ls-files | sed -n '1,80p'

echo
echo "Find doctor route by filename (case-insensitive):"
git ls-files | grep -E '(^|/)doctor/route\.ts$|/doctor/route\.ts$' || true

echo
echo "Find Next app package files:"
git ls-files | grep -E '(^|/)package\.json$' | xargs -r -n1 sh -c 'echo "--- $0"; grep -En "\"(name|engines|node|next|undici)\"|\"type\":\"module\"|\"type\":\"commonjs\"|\"next\":|^" || true' "$0" | sed -n '1,120p'

echo
echo "Search fetch/manual redirect and login body checks:"
git ls-files -z | xargs -0 grep -InE 'fetch\(|redirect:\s*"?manual"?|console\.text\(probed|probed\.body|callbackUrl|sign(.* )?in|/login|Location|response\.status' apps 2>/dev/null | sed -n '1,240p'

Repository: Travis-Gilbert/CommonPlace

Length of output: 209


🏁 Script executed:

#!/bin/bash
set -u

echo "Repository top files:"
git ls-files | sed -n '1,80p'

echo
echo "Find doctor route by filename:"
git ls-files | grep -E '(^|/)doctor/route\.ts$|/doctor/route\.ts$' || true

echo
echo "Find relevant package/runtime declarations:"
tmp="$(mktemp)"
git ls-files '*package.json' > "$tmp"
while IFS= read -r f; do
  echo "--- $f"
  grep -En '"(name|engines|next|undici|node)"|type|scripts|devDependencies|dependencies' "$f" 2>/dev/null | sed -n '1,140p' || true
done < "$tmp"
rm -f "$tmp"

echo
echo "Search fetch/manual redirect and login body checks under apps:"
git ls-files | xargs grep -InE 'fetch\(|redirect:\s*"?manual"?|console\.text\(probed|probed\.body|callbackUrl|sign(.* )?in|/login|Location|response\.status' apps 2>/dev/null | sed -n '1,240p' || true

Repository: Travis-Gilbert/CommonPlace

Length of output: 47814


🏁 Script executed:

#!/bin/bash
set -u

echo "Target route excerpt:"
sed -n '1,140p' apps/console/src/app/api/doctor/route.ts | cat -n

echo
echo "Console package relevant fields:"
python3 - <<'PY'
import json
from pathlib import Path
p = Path("apps/console/package.json")
if p.exists():
    data = json.loads(p.read_text())
    for key in ("name", "version", "scripts", "dependencies", "devDependencies", "packageManager", "engines"):
        if key in data:
            print(f"{key}:")
            for k, v in data[key].items() if isinstance(data[key], dict) else enumerate([data[key]]):
                print(f"  {k}: {v}")
PY

echo
echo "Root package relevant fields:"
python3 - <<'PY'
import json
from pathlib import Path
p = Path("package.json")
if p.exists():
    data = json.loads(p.read_text())
    for key in ("name", "version", "scripts", "dependencies", "devDependencies", "packageManager", "engines"):
        if key in data:
            print(f"{key}:")
            for k, v in data[key].items() if isinstance(data[key], dict) else enumerate([data[key]]):
                print(f"  {k}: {v}")
PY

echo
echo "Node version file:"
for f in .nvmrc node_modules/node/.version node_modules/.bin/node; do
  [ -e "$f" ] && { echo "--- $f"; cat "$f" | sed -n '1,16p'; }
done

Repository: Travis-Gilbert/CommonPlace

Length of output: 11972


Check the sign-in fallback for the actual response shape.

Node 22 Undici returns the real 3xx response with headers/body for redirect: 'manual', so the Location header is available for a redirect-based auth gate. If this route can be served by one, also test response.headers.get('location') rather than relying only on probed.body, and avoid requiring probed.impl === null before accepting the redirect page.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/console/src/app/api/doctor/route.ts` at line 57, Update the sign-in
fallback around the fetch using redirect: 'manual' to recognize a real 3xx
response by checking response.headers.get('location'). Accept the redirect-based
auth gate without requiring probed.impl === null, while preserving the existing
response/body handling for other cases.

Comment on lines +119 to +148
const resurrections: ResurrectionRow[] = [];
for (const row of manifest.registers ?? []) {
for (const item of row.superseded ?? []) {
for (const filePath of item.paths ?? []) {
const abs = path.join(repoRoot(), filePath);
resurrections.push({
id: item.impl,
path: filePath,
absent: !existsSync(abs),
pending_retirement: true,
});
}
}
}
for (const row of manifest.retired ?? []) {
for (const filePath of row.paths ?? []) {
const abs = path.join(repoRoot(), filePath);
resurrections.push({
id: row.id,
path: filePath,
absent: !existsSync(abs),
pending_retirement: false,
});
}
}

// Until retirements land, superseded paths still exist. Doctor reports them
// without failing the overall cutover while deletion_deadline has not passed.
const routesOk = routes.every((row) => row.ok);
const ok = envOk && routesOk;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

ok ignores resurrection results; a resurrected retired file will not turn the doctor red.

resurrections is computed at Lines 119-143, but Line 147-148 computes ok from envOk and routesOk only. No resurrections-derived value ever feeds into ok.

For entries with pending_retirement: false (built from manifest.retired), absent: false means a deleted implementation has come back. GL8 in SPEC-COMMONPLACE-PRODUCTION-CUTOVER-1.0.md requires: "the doctor's resurrection smokes are green and demonstrably red when a deleted path is restored in a staging test." As written, restoring a retired file cannot fail the doctor: /api/doctor still reports ok: true and /doctor's header still renders "Green" even while the corresponding resurrection row shows "STILL PRESENT." This defeats the doctor's core purpose as a deploy-blocking gate for GL8.

Gate ok on retired-and-resurfaced entries too.

🐛 Proposed fix to gate `ok` on resurrections
   // Until retirements land, superseded paths still exist. Doctor reports them
   // without failing the overall cutover while deletion_deadline has not passed.
   const routesOk = routes.every((row) => row.ok);
-  const ok = envOk && routesOk;
+  const retiredResurfaced = resurrections.filter(
+    (row) => row.pending_retirement === false && !row.absent,
+  );
+  const resurrectionsOk = retiredResurfaced.length === 0;
+  const ok = envOk && routesOk && resurrectionsOk;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const resurrections: ResurrectionRow[] = [];
for (const row of manifest.registers ?? []) {
for (const item of row.superseded ?? []) {
for (const filePath of item.paths ?? []) {
const abs = path.join(repoRoot(), filePath);
resurrections.push({
id: item.impl,
path: filePath,
absent: !existsSync(abs),
pending_retirement: true,
});
}
}
}
for (const row of manifest.retired ?? []) {
for (const filePath of row.paths ?? []) {
const abs = path.join(repoRoot(), filePath);
resurrections.push({
id: row.id,
path: filePath,
absent: !existsSync(abs),
pending_retirement: false,
});
}
}
// Until retirements land, superseded paths still exist. Doctor reports them
// without failing the overall cutover while deletion_deadline has not passed.
const routesOk = routes.every((row) => row.ok);
const ok = envOk && routesOk;
const resurrections: ResurrectionRow[] = [];
for (const row of manifest.registers ?? []) {
for (const item of row.superseded ?? []) {
for (const filePath of item.paths ?? []) {
const abs = path.join(repoRoot(), filePath);
resurrections.push({
id: item.impl,
path: filePath,
absent: !existsSync(abs),
pending_retirement: true,
});
}
}
}
for (const row of manifest.retired ?? []) {
for (const filePath of row.paths ?? []) {
const abs = path.join(repoRoot(), filePath);
resurrections.push({
id: row.id,
path: filePath,
absent: !existsSync(abs),
pending_retirement: false,
});
}
}
// Until retirements land, superseded paths still exist. Doctor reports them
// without failing the overall cutover while deletion_deadline has not passed.
const routesOk = routes.every((row) => row.ok);
const retiredResurfaced = resurrections.filter(
(row) => row.pending_retirement === false && !row.absent,
);
const resurrectionsOk = retiredResurfaced.length === 0;
const ok = envOk && routesOk && resurrectionsOk;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/console/src/app/api/doctor/route.ts` around lines 119 - 148, Update the
ok calculation in the doctor route to fail when any resurrections entry from
manifest.retired has pending_retirement false and absent false, while preserving
the existing reporting for superseded entries. Include this resurrection check
alongside envOk and routesOk so restored retired files make the doctor red and
deploy-blocking.

Comment thread packaging/workspace/entrypoint.sh Outdated
Comment on lines +18 to +21
# Railway healthchecks probe $PORT. Prefer OPENWORK_PORT, then PORT, then 8787.
OPENWORK_PORT="${OPENWORK_PORT:-${PORT:-8787}}"
CODE_SERVER_PORT="${CODE_SERVER_PORT:-8080}"
export PORT="${OPENWORK_PORT}"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== candidate files =="
git ls-files | rg '(^|/)entrypoint\.sh$|(^|/)Dockerfile$|config\.ts$' | sed -n '1,120p'

echo
echo "== entrypoint.sh relevant lines =="
fd -a 'entrypoint\.sh$' . | while read -r f; do
  echo "--- $f"
  nl -ba "$f" | sed -n '1,80p'
done

echo
echo "== search OPENWORK_PORT/PORT references =="
rg -n "OPENWORK_PORT|CODE_SERVER_PORT|process\.env\.(OPENWORK_PORT|PORT)|PORT\s*=" -S . | sed -n '1,220p'

echo
echo "== config.ts likely section =="
git ls-files | rg 'apps/chat-server/src/config\.ts$' | while read -r f; do
  echo "--- $f"
  nl -ba "$f" | sed -n '290,330p'
done

echo
echo "== workspace Dockerfile relevant lines =="
git ls-files | rg 'packaging/workspace/Dockerfile$' | while read -r f; do
  echo "--- $f"
  nl -ba "$f" | sed -n '90,150p'
done

Repository: Travis-Gilbert/CommonPlace

Length of output: 1409


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== entrypoint.sh =="
awk '{printf "%5d  %s\n", NR, $0}' packaging/workspace/entrypoint.sh | sed -n '1,180p'

echo
echo "== workspace Dockerfile relevant lines =="
awk '{printf "%5d  %s\n", NR, $0}' packaging/workspace/Dockerfile | sed -n '1,190p'

echo
echo "== chat-server config OPENWORK_PORT section =="
awk '{printf "%5d  %s\n", NR, $0}' apps/chat-server/src/config.ts | sed -n '280,340p'

echo
echo "== all OPENWORK_PORT/PORT references in relevant files =="
for f in apps/chat-server/src/config.ts apps/chat-server/src/openwork-runtime-config.ts packaging/workspace/entrypoint.sh packaging/workspace/Dockerfile; do
  if [ -f "$f" ]; then
    echo "--- $f"
    rg -n "OPENWORK_PORT|PORT|export |ENV " "$f" || true
  fi
done

echo
echo "== shell default semantics probe =="
python3 - <<'PY'
import os, subprocess, tempfile, textwrap
cases = [
    ("unset_env_unset", {}, ""),
    ("unset_env_empty", {}, ""),
    ("unset_env_default", {}, ""),
    ("port_only", {"PORT": "3000"}, ""),
    ("openwork_only", {}, "OPENWORK_PORT=9999"),
    ("both_override_port", {"PORT": "3000"}, "OPENWORK_PORT=9999"),
    ("openwork_override_default", {}, "OPENWORK_PORT=9999"),
]
for name, env_vars, extra in cases:
    cfg = dict(os.environ)
    cfg.pop("OPENWORK_PORT", None)
    cfg.pop("PORT", None)
    cfg.update(env_vars)
    script = textwrap.dedent(f'''
        set -e
        : {extra}
        export OPENWORK_PORT="${{OPENWORK_PORT:-{{PORT:-8787}}}}"
        export PORT="${{OPENWORK_PORT}}"
        echo "$OPENWORK_PORT $PORT"
    ''').strip()
    result = subprocess.check_output(["/bin/sh", "-c", script], env=cfg, text=True).strip()
    print(f"{name}: {result}")
PY

Repository: Travis-Gilbert/CommonPlace

Length of output: 21746


Do not override runtime port variables with image defaults.

packaging/workspace/Dockerfile defines ENV OPENWORK_PORT=8787, so Railway-provided PORT cannot flow through the entrypoint or chat-server config. Remove the image-level default, export the variable that the shell resolves, and let the healthcheck use the same precedence instead of its own OPENWORK_PORT lookup.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packaging/workspace/entrypoint.sh` around lines 18 - 21, Update the
entrypoint port initialization and related Dockerfile configuration: remove the
image-level OPENWORK_PORT default, export the shell-resolved port variable
without overriding runtime-provided PORT values, and make the healthcheck use
that same resolved port rather than performing a separate OPENWORK_PORT lookup.

Source: MCP tools

Comment on lines +58 to +89
// Red 2: plant a superseded corpse under a retired path name and assert doctor
// API logic would treat it as present. We call the resurrection helper shape by
// writing a file the doctor resurrection inventory names.
mkdirSync(corpseDir, { recursive: true });
writeFileSync(
corpse,
'// doctor staging red corpse — must not remain after this script\nexport {};\n',
);

const restored = JSON.parse(readFileSync(marker, 'utf8'));
const chat = restored.registers.find((row) => row.id === 'chat');
if (!chat?.superseded?.[0]?.paths?.length && !restored.retired?.length) {
// Force a retired entry pointing at the planted corpse for this proof.
restored.retired = [
{
id: 'doctor-staging-red-corpse',
paths: ['apps/console/src/components/chat/__doctor_red_corpse__.tsx'],
retired_at: 'staging-red',
},
];
writeFileSync(marker, `${JSON.stringify(restored, null, 2)}\n`);
}

const doctorApi = path.join(repoRoot, 'apps/console/src/app/api/doctor/route.ts');
const doctorSource = readFileSync(doctorApi, 'utf8');
if (!/pending_retirement|resurrection|retired/.test(doctorSource)) {
fail('doctor API missing resurrection / pending_retirement handling');
}
console.log('ok red.doctor_source: doctor API carries resurrection checks');

if (!existsSync(corpse)) fail('failed to plant corpse');
console.log('ok red.corpse_present: planted superseded path for staging proof');

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift

Red 2 does not prove that the doctor fails closed.

The header at Lines 3-4 states the script proves the doctor fails closed when a superseded corpse path still exists. Red 2 does three things instead: it plants a file, it optionally writes a retired row into the marker, and it greps apps/console/src/app/api/doctor/route.ts for the strings pending_retirement|resurrection|retired. A source grep passes even when the resurrection logic is inverted or unreachable. The mutated marker written at Lines 71-78 is never consumed by any check.

Invoke the retirement/resurrection evaluation directly and assert a non-green result. If the logic lives inside the route handler, extract it into an importable module so both the route and this proof call the same function.

Do you want me to draft that extraction and the corresponding assertion?

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/doctor-staging-reds.mjs` around lines 58 - 89, The Red 2 proof must
execute the doctor’s retirement/resurrection evaluation instead of grepping
route source. Extract the relevant logic from the doctor API route into an
importable function or module, have the route reuse it, and update this script
to invoke that function against the mutated marker and planted corpse, asserting
a non-green result.

Comment on lines +62 to +65
writeFileSync(
corpse,
'// doctor staging red corpse — must not remain after this script\nexport {};\n',
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Remove the em dash from the generated file content.

The corpse file is written to apps/console/src/components/chat/__doctor_red_corpse__.tsx, and its comment contains an em dash.

✏️ Proposed fix
-  '// doctor staging red corpse — must not remain after this script\nexport {};\n',
+  '// doctor staging red corpse: must not remain after this script\nexport {};\n',

As per coding guidelines: "Do not use em dashes or en dashes in code comments, UI strings, or markdown."

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
writeFileSync(
corpse,
'// doctor staging red corpse must not remain after this script\nexport {};\n',
);
writeFileSync(
corpse,
'// doctor staging red corpse: must not remain after this script\nexport {};\n',
);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/doctor-staging-reds.mjs` around lines 62 - 65, Update the generated
content in writeFileSync to replace the em dash in the corpse file comment with
allowed punctuation, while preserving the rest of the comment and export
statement.

Source: Coding guidelines

Comment thread scripts/doctor.mjs
Comment on lines +41 to +48
async function fetchText(url, init) {
const response = await fetch(url, {
redirect: 'follow',
...init,
});
const text = await response.text();
return { response, text };
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Add a timeout to every probe.

fetch has no timeout here. A hung upstream leaves the doctor job blocked until the CI runner limit expires, instead of reporting red. .github/workflows/production-doctor.yml runs this script as a gate.

🛡️ Proposed fix
 async function fetchText(url, init) {
   const response = await fetch(url, {
     redirect: 'follow',
+    signal: AbortSignal.timeout(Number(process.env.DOCTOR_TIMEOUT_MS || 15000)),
     ...init,
   });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/doctor.mjs` around lines 41 - 48, Update fetchText to apply an abort
timeout to every fetch request, including calls that provide init options, so
hung probes terminate within a bounded interval and report failure. Use the
existing fetch signal when present or compose the timeout signal without
overriding caller configuration.

Comment thread scripts/doctor.mjs
Comment on lines +71 to +74
// Unauthenticated routes may redirect to login HTML without the marker.
// Accept 200 with marker, or a login redirect that still returns HTML.
const loginish = response.status === 200 && /callbackUrl|sign.?in|login/i.test(text);
const pass = response.ok && (hasMarker || loginish);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

The loginish escape hatch makes the impl marker check nearly unenforceable.

loginish passes when the status is 200 and the body matches /callbackUrl|sign.?in|login/i. Most authenticated app shells render a "Sign in" or "Sign out" control, and Auth.js pages and links embed callbackUrl. A route that renders the wrong register, or no register at all, therefore passes the check. The route gate then reports green while the cutover marker is absent.

Narrow the fallback to an actual auth redirect. Compare response.url against the sign-in path, or send a request with redirect: 'manual' and accept only a 3xx to the sign-in route. Better, run the doctor with a session cookie so the marker assertion is the only accepted pass.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/doctor.mjs` around lines 71 - 74, Replace the broad body-text-based
loginish fallback near the route check with validation of an actual
authentication redirect: compare response.url to the expected sign-in path, or
use a manual redirect request and accept only a 3xx redirect targeting that
path. Ensure routes without the cutover marker cannot pass merely because their
HTML contains terms such as “login” or “callbackUrl”.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants