diff --git a/.github/agents/fixlab-autofix.agent.md b/.github/agents/fixlab-autofix.agent.md index 14543f7..a870ae8 100644 --- a/.github/agents/fixlab-autofix.agent.md +++ b/.github/agents/fixlab-autofix.agent.md @@ -33,41 +33,21 @@ repackage the rejected operation to evade review. **Deny and request safe recove reuses existing reviewed tests/helpers in the same blocked stage; preserve completed evidence and failures, and stop if a safe prerequisite is missing. -For authorized DEV/SIT/UAT data-changing POST actions, discover +For individually authorized DEV/SIT/UAT POST actions, discover `fixlab_nonproduction_post` in a native Copilot dashboard job. The **Allow POST -writes** checkbox is off by default. Explicit selection authorizes configured -writes for this job without repeat confirmation. Unchecked or recovered jobs -require expiring, single-use human confirmation of each exact URL and complete -payload. Allow all and batch approval never apply. Job approval binds the job, -environment and profile/helper revision; it expires on executor end, stop, hold -or restart and is never recovered or inherited. Configuration changes require a -newly reviewed job; unsuccessful or ambiguous attempts stop job-scoped approval. -`dataSafety.post.dev`, `.sit` and `.uat` hold separate +requests** start checkbox enables requests; unchecked, ask when an action is needed. +It never approves writes. `dataSafety.post.dev`, `.sit` and `.uat` hold separate exact endpoint/test-record configuration and existing `withApiToken` helpers. Legacy DEV `fixlab_dev_post` and `dataSafety.devPost` remain supported. -No credentials or response body enter +Each exact URL and complete +synthetic/approved test payload requires expiring, single-use human confirmation; +Allow all and batch approval never apply. No credentials or response body enter the result. Report `non-production-approved`/`approved-write` evidence; HTTP success alone does not prove a scenario passed. No automatic retries or redirects: an ambiguous result requires human inspection before any fresh action. Other environments, explicit read-only/intercepted selections, API-only read-only contracts, unit-only validation and human sign-in remain unchanged. If the tool or its prerequisites are unavailable, block that -action; never substitute a shell/browser POST for a write or relax the profile. - -Verified read-only POST searches: for browser live checks permitting real backend -reads, require a reviewed query contract and implementation proving no -data/workflow mutation. Match the exact approved environment, origin and path -through the existing browser/auth guard, including redirects and the supported -query payload. Route names and the job POST checkbox are not approval. Run -permitted queries through existing authenticated helpers and use genuine returned -records in focused assertions, even when writes are intercepted. A missing query -allowlist entry is a policy blocker, not backend unavailability: inspect bounded -controller/service evidence and request a narrowly scoped configuration review. -Do not silently substitute synthetic data, disable guards, add wildcard/global -POST grants or bypass with another client. Read-only jobs must not edit -configuration; approved changes require a fresh reviewed context/job. Keep Save, -Submit, Approve and autosave behind existing write controls. Do not override -synthetic-only, API-only, unit-only or production policy. Authentication/configuration -reads and synthetic UI evidence do not prove business-data integration. +action; never substitute a shell/browser POST or relax the profile. When `validation.liveTestsRequired` is `false`, follow unit-test-only validation: run configured local unit tests and required offline checks, without application @@ -212,23 +192,6 @@ push later reviewed checkpoints to the same branch, and never mark the PR ready until every required local gate passes or the developer explicitly accepts the recorded outcome. -Reusable PR learning: within the existing pr stage, only when source edits and -PR publication are authorized, inspect task-relevant existing repository skills -for verified, non-obvious, reusable knowledge from this change. Prefer to update -a matching skill; otherwise create `.github/skills//SKILL.md` with YAML name -and description; the name must match the lowercase-hyphenated topic directory. -Capture when to use it, the verified solution, repository file/test references, -exact working directories and validation commands, and pitfalls. Skills are for -future developers and relevant agent tasks, not a diary of the current run. -Include the skill in the same reviewed PR before committing/pushing; self-review -its instructions and run applicable documentation/skill checks. In the concise -PR body, link the added/updated skill or state no new reusable learning when none -qualifies; do not create empty, speculative or duplicate skills. Use no separate -roadmap stage. Read-only jobs must not write skills, commit or publish. Respect -repository instruction governance and existing approval boundaries. Do not add -allowed-tools or broaden permissions. Never include credentials, personal/customer -data, private endpoints or raw transcripts. - Honor an explicit publication choice in every mode: strict plus draft stays draft, with provider `isDraft: true` verified. Legacy strict policy with no publication preference retains ready intent. Strict required checks never change. @@ -313,4 +276,3 @@ wait for the owned validation to finish before cleanup. access fails or no safe records can exercise the behavior. Preserve the limitation, do not replace an expected empty state, and report that a synthetic pass proves UI behavior only. - Guard/query-allowlist rejections require configuration review, not fallback. diff --git a/.github/agents/fixlab.agent.md b/.github/agents/fixlab.agent.md index f2327de..250a9ba 100644 --- a/.github/agents/fixlab.agent.md +++ b/.github/agents/fixlab.agent.md @@ -20,41 +20,21 @@ repackage the rejected operation to evade review. **Deny and request safe recove reuses existing reviewed tests/helpers in the same blocked stage; preserve completed evidence and failures, and stop if a safe prerequisite is missing. -For authorized DEV/SIT/UAT data-changing POST actions, discover +For individually authorized DEV/SIT/UAT POST actions, discover `fixlab_nonproduction_post` in a native Copilot dashboard job. The **Allow POST -writes** checkbox is off by default. Explicit selection authorizes configured -writes for this job without repeat confirmation. Unchecked or recovered jobs -require expiring, single-use human confirmation of each exact URL and complete -payload. Allow all and batch approval never apply. Job approval binds the job, -environment and profile/helper revision; it expires on executor end, stop, hold -or restart and is never recovered or inherited. Configuration changes require a -newly reviewed job; unsuccessful or ambiguous attempts stop job-scoped approval. -`dataSafety.post.dev`, `.sit` and `.uat` hold separate +requests** start checkbox enables requests; unchecked, ask when an action is needed. +It never approves writes. `dataSafety.post.dev`, `.sit` and `.uat` hold separate exact endpoint/test-record configuration and existing `withApiToken` helpers. Legacy DEV `fixlab_dev_post` and `dataSafety.devPost` remain supported. -No credentials or response body enter +Each exact URL and complete +synthetic/approved test payload requires expiring, single-use human confirmation; +Allow all and batch approval never apply. No credentials or response body enter the result. Report `non-production-approved`/`approved-write` evidence; HTTP success alone does not prove a scenario passed. No automatic retries or redirects: an ambiguous result requires human inspection before any fresh action. Other environments, explicit read-only/intercepted selections, API-only read-only contracts, unit-only validation and human sign-in remain unchanged. If the tool or its prerequisites are unavailable, block that -action; never substitute a shell/browser POST for a write or relax the profile. - -Verified read-only POST searches: for browser live checks permitting real backend -reads, require a reviewed query contract and implementation proving no -data/workflow mutation. Match the exact approved environment, origin and path -through the existing browser/auth guard, including redirects and the supported -query payload. Route names and the job POST checkbox are not approval. Run -permitted queries through existing authenticated helpers and use genuine returned -records in focused assertions, even when writes are intercepted. A missing query -allowlist entry is a policy blocker, not backend unavailability: inspect bounded -controller/service evidence and request a narrowly scoped configuration review. -Do not silently substitute synthetic data, disable guards, add wildcard/global -POST grants or bypass with another client. Read-only jobs must not edit -configuration; approved changes require a fresh reviewed context/job. Keep Save, -Submit, Approve and autosave behind existing write controls. Do not override -synthetic-only, API-only, unit-only or production policy. Authentication/configuration -reads and synthetic UI evidence do not prove business-data integration. +action; never substitute a shell/browser POST or relax the profile. When `validation.liveTestsRequired` is `false`, follow unit-test-only validation: run configured local unit tests and required offline checks, without application @@ -87,23 +67,6 @@ logs; omit repeated stage narration, full transcripts and redundant file lists. Exclude token/cache counts, AI-credit totals and model-cost summaries from PR descriptions. -Reusable PR learning: within the existing pr stage, only when source edits and -PR publication are authorized, inspect task-relevant existing repository skills -for verified, non-obvious, reusable knowledge from this change. Prefer to update -a matching skill; otherwise create `.github/skills//SKILL.md` with YAML name -and description; the name must match the lowercase-hyphenated topic directory. -Capture when to use it, the verified solution, repository file/test references, -exact working directories and validation commands, and pitfalls. Skills are for -future developers and relevant agent tasks, not a diary of the current run. -Include the skill in the same reviewed PR before committing/pushing; self-review -its instructions and run applicable documentation/skill checks. In the concise -PR body, link the added/updated skill or state no new reusable learning when none -qualifies; do not create empty, speculative or duplicate skills. Use no separate -roadmap stage. Read-only jobs must not write skills, commit or publish. Respect -repository instruction governance and existing approval boundaries. Do not add -allowed-tools or broaden permissions. Never include credentials, personal/customer -data, private endpoints or raw transcripts. - Prefix FixLab-created PR titles with `FLAB:` and format their descriptions with the repository's PR template, clear validation evidence and remaining risks. diff --git a/.github/skills/fixlab-job-scoped-post-approval/SKILL.md b/.github/skills/fixlab-job-scoped-post-approval/SKILL.md deleted file mode 100644 index 1f7a6f1..0000000 --- a/.github/skills/fixlab-job-scoped-post-approval/SKILL.md +++ /dev/null @@ -1,33 +0,0 @@ ---- -name: fixlab-job-scoped-post-approval -description: Preserve explicit, revision-bound write consent when changing the FixLab POST checkbox, native tool, or job lifecycle. ---- - -# Job-scoped POST approval - -Use when changing POST consent, executor configuration, recovery, or snapshots. -Work from the FixLab repository root. - -- A saved capability preference is not write approval. Only fresh explicit - checkbox confirmation creates the memory-only job/environment/revision grant. -- Validate consent independently of `toolApprovalMode`. Enforce exact loopback - Host and Origin for both ordinary and Allow-all jobs. -- Bind the reviewed profile and authentication-helper revision before queueing. - Configuration added later must not enlarge an earlier approval. -- Preserve the grant through the native worker protocol. Never persist it in - snapshots or inherit it on hold, recovery, restart, or another job. -- Both approval modes retain endpoint/test-record checks and existing - authentication. Never disable browser guards or substitute a browser write. -- Stop job-scoped approval after failed or ambiguous attempts. Approval is not - evidence that a scenario passed. - -Implementation: `dashboard/dev-post.js`, `dashboard/copilot-session.js`, -`dashboard/server.js`, and `dashboard/public/app.js`. - -Verify actual HTTP, SDK, worker, and browser behavior, not prompt text alone: - -```powershell -node --test test\dashboard-dev-post.test.js -node --test --test-name-pattern="POST checkbox write consent|native executor forwards" test\dashboard-server.test.js -npm run test:e2e -- dashboard-readiness --grep "POST checkbox" -``` diff --git a/.github/skills/fixlab-windows-background-probes/SKILL.md b/.github/skills/fixlab-windows-background-probes/SKILL.md deleted file mode 100644 index c5272ef..0000000 --- a/.github/skills/fixlab-windows-background-probes/SKILL.md +++ /dev/null @@ -1,55 +0,0 @@ ---- -name: fixlab-windows-background-probes -description: Diagnose brief Git windows during idle FixLab polling and keep background repository probes hidden on Windows. ---- - -# FixLab Windows background probes - -## When to use - -Use when Git console windows flash during idle dashboard/chat polling, or when -adding background repository probes to those paths. - -## Verified solution - -The chat polls `/api/status` once per second by default. Repository inspection -prunes owned artifacts, which resolves the Git artifact root. Cache inspection -has a separate Git helper. Either helper can open a brief Windows console even -when no job or executor is running. - -Keep `windowsHide: true` and `shell: false` on both background `spawnSync` helpers: - -- [Artifact-root Git helper](../../../dashboard/intake.js): `runGit`, reached - through `pruneArtifactDirectories` and `resolveArtifactRoot`. -- [Dashboard Git helper](../../../dashboard/server.js): `runGit`, used by - `createCacheContext`; `/api/status` calls `inspectRepository`. -- [Chat polling](../../../dashboard/chat.js): `runChat`. - -Preserve command arguments, output handling and existing ownership checks. -Do not disable polling or change the owned executor's containment behavior to -hide windows created by a different background helper. - -## Validation - -Working directory: `.` from the FixLab repository root, containing `package.json`. -Run the existing regression in PowerShell: - -```powershell -node --test --test-name-pattern="idle status polling and cache inspection hide their Git windows" test\dashboard-server.test.js -``` - -The [regression](../../../test/dashboard-server.test.js) performs real HTTP -status polling and cache inspection, checks native Git spawn options, and -verifies idle polling never starts an executor. Its builtin-module spy must be -restored in `finally`, including `syncBuiltinESMExports()`. - -Run `npm run validate` for implementation changes. On Windows, -`pwsh -NoLogo -NoProfile -File scripts\validate.ps1` runs that same required gate -while temporarily preventing idle sleep. - -## Pitfalls - -An interactive executor's console settings do not cover every independent Git -probe. Inspect the actual polling path before changing process-launch code. -Do not hide intended interactive terminals, bypass permissions, or stop -unrelated Git processes. diff --git a/README.md b/README.md index ad0d631..42c2eb2 100644 --- a/README.md +++ b/README.md @@ -16,40 +16,10 @@ the application's repository-owned profile. ## Get started -**First installation:** read [installation.md](installation.md) for prerequisites, -what FixLab adds to your machine, and reviewed repository setup. -Use the [detailed installation guide](docs/installation.md) for other setup paths -or download the [Word quickstart](docs/FixLab-Installation-Guide-Quickstart.docx). - Install and authenticate a supported Copilot runtime, then run: ```powershell npm install --global github:MonaDevAI/FixLab -``` - -### Onboard and check the application before chat - -```powershell -fixlab onboard C:\source\application --runtime copilot -``` - -If onboarding creates an example profile, replace its paths and commands with -the real application layout, then rerun `onboard` to review the restore and -Playwright setup plans. Do not create empty folders to satisfy example paths. -After reviewing and approving that setup: - -```powershell -fixlab onboard C:\source\application --runtime copilot --yes -fixlab doctor C:\source\application --runtime copilot -``` - -Resolve every Doctor blocker before continuing. Add `--authenticate` to the -approved onboarding command only if the profile requires browser sign-in; -complete sign-in and MFA yourself. - -### Start chat - -```powershell fixlab chat C:\source\application --runtime copilot ``` @@ -57,41 +27,12 @@ Replace the path with your checkout; use `--runtime agency` for Agency. Guided setup checks readiness before starting the local dashboard. Review the profile and approve only the required setup, sign-in, and tool operations. Installing the CLI alone does not start a service or make an application ready. -The profile's `frontend` and `backend` paths are examples: point them to your -real checkout layout rather than creating empty folders to satisfy checks. Prefer Copilot to perform setup? Use the [copy-paste installation prompt](docs/installation.md#copy-paste-installation-prompt). For VS Code, follow the [custom-agent setup](docs/installation.md#install-and-use-in-visual-studio-code). -### Stop or replace an unused dashboard - -At `fixlab>`, `/stop` stops the job and leaves it resumable; `/exit` disconnects -chat without stopping the dashboard. Press **Ctrl+C in the dashboard's -terminal** to stop the server and release its port. - -If Copilot started it in the background, ask: - -> Shut down FixLab on port 4317 and only its owned processes. Leave other -> applications running. - -Start a replacement using the available port: - -```powershell -fixlab dashboard C:\source\application --port 4317 --runtime copilot -``` - -In another terminal, connect chat to that same port: - -```powershell -fixlab chat C:\source\application --port 4317 --runtime copilot -``` - -Closing a browser tab or exiting Copilot does not stop a detached dashboard. -No reinstall or file deletion is needed. Restarting preserves saved jobs; -submit a new request for a fresh job. - ## What you can do | Workflow | Outcome | @@ -110,13 +51,21 @@ developer or runner machine; the configured AI service supplies model access. ![FixLab architecture: local orchestration, repository contract, Copilot runtime, evidence, and external services](assets/fixlab-architecture-diagram.png) +The architecture and workflow illustrations are the original images created +for the **FixLab Architecture & Benefits** Word guide. + +| Component | Responsibility | +| --- | --- | +| CLI, terminal chat, and dashboard | Onboard repositories, submit work, show progress, and collect human decisions | +| Repository profile | Define application paths, commands, environments, validation gates, and safety policy | +| Copilot runtime and FixLab workflow | Investigate the task, make authorized scoped changes, and collect evidence | +| Configured checks | Run focused tests, required application startup, and browser/API validation | +| Evidence and metadata | Retain local results, artifacts, and bounded recovery context | +| Optional MCP bridge | Connect a host conversation to identity-bound dashboard controls | + The dashboard and terminal chat share job state, blockers, and results. Interrupted work can resume in the same job; a bounded queue keeps later requests separate. -During the existing PR step, verified reusable learning is captured in a -reviewed repository skill (`.github/skills//SKILL.md`), updating a -matching skill rather than duplicating it. No extra roadmap stage is added. -If there is no new reusable learning, the PR says so instead of adding a file. ### End-to-end job flow @@ -127,16 +76,31 @@ Optional broad tests/builds are controlled by saved preferences; required repository checks and CI/merge policies still apply. Failed, deferred, stale, or skipped checks are never reported as passed. -See [architecture and benefits](docs/architecture-overview.md) for components, -benefits, deployment, recovery, and trust boundaries, or the -[architecture reference](docs/architecture.md) for runtime and MCP contracts. +### Benefits + +| Benefit | Mechanism | +| --- | --- | +| Repeatable delivery | A shared workflow with repository-owned commands, boundaries, and gates | +| Fewer manual handoffs | Diagnosis, scoped edits, startup, validation, and evidence in one job | +| Reviewable outcomes | Explicit assertions, failures, skipped checks, risks, and PR state | +| Recoverable work | Same-job/session continuation instead of discarding completed work | +| Reuse across applications | Profiles configure applications without hardcoding them into the product | +| Bounded context | Focused searches, compact command evidence, and advisory metadata reuse | + +These are workflow capabilities, not guarantees of correctness, cost reduction, +or faster delivery. Actual results depend on repository quality, model/runtime +availability, authentication, and the configured validation contract. + +The [architecture and benefits guide](docs/architecture-overview.md) expands +on deployment, state/recovery, and trust boundaries. See the +[architecture reference](docs/architecture.md) for current runtime and MCP +control contracts beyond the high-level Word-guide illustrations. ## Documentation | Start here | Read next | | --- | --- | -| [First installation](installation.md) | Prerequisites, machine changes, and reviewed setup | -| [Detailed installation](docs/installation.md) | Copilot prompt, VS Code, updates, and optional integrations | +| [Installation](docs/installation.md) | Prerequisites, Copilot prompt, VS Code, updates, and optional integrations | | [Getting started](docs/getting-started.md) | First job, dashboard use, approvals, and recovery | | [Architecture and benefits](docs/architecture-overview.md) | Component diagrams, job flow, deployment, and trust boundaries | | [Repository onboarding](docs/repository-onboarding.md) | Profiles, worktrees, API-only validation, and linked applications | diff --git a/agents/fixlab.md b/agents/fixlab.md index 3a203ce..3f1994c 100644 --- a/agents/fixlab.md +++ b/agents/fixlab.md @@ -26,41 +26,21 @@ repackage the rejected operation to evade review. **Deny and request safe recove reuses existing reviewed tests/helpers in the same blocked stage; preserve completed evidence and failures, and stop if a safe prerequisite is missing. -For authorized DEV/SIT/UAT data-changing POST actions, discover +For individually authorized DEV/SIT/UAT POST actions, discover `fixlab_nonproduction_post` in a native Copilot dashboard job. The **Allow POST -writes** checkbox is off by default. Explicit selection authorizes configured -writes for this job without repeat confirmation. Unchecked or recovered jobs -require expiring, single-use human confirmation of each exact URL and complete -payload. Allow all and batch approval never apply. Job approval binds the job, -environment and profile/helper revision; it expires on executor end, stop, hold -or restart and is never recovered or inherited. Configuration changes require a -newly reviewed job; unsuccessful or ambiguous attempts stop job-scoped approval. -`dataSafety.post.dev`, `.sit` and `.uat` hold separate +requests** start checkbox enables requests; unchecked, ask when an action is needed. +It never approves writes. `dataSafety.post.dev`, `.sit` and `.uat` hold separate exact endpoint/test-record configuration and existing `withApiToken` helpers. Legacy DEV `fixlab_dev_post` and `dataSafety.devPost` remain supported. -No credentials or response body enter +Each exact URL and complete +synthetic/approved test payload requires expiring, single-use human confirmation; +Allow all and batch approval never apply. No credentials or response body enter the result. Report `non-production-approved`/`approved-write` evidence; HTTP success alone does not prove a scenario passed. No automatic retries or redirects: an ambiguous result requires human inspection before any fresh action. Other environments, explicit read-only/intercepted selections, API-only read-only contracts, unit-only validation and human sign-in remain unchanged. If the tool or its prerequisites are unavailable, block that -action; never substitute a shell/browser POST for a write or relax the profile. - -Verified read-only POST searches: for browser live checks permitting real backend -reads, require a reviewed query contract and implementation proving no -data/workflow mutation. Match the exact approved environment, origin and path -through the existing browser/auth guard, including redirects and the supported -query payload. Route names and the job POST checkbox are not approval. Run -permitted queries through existing authenticated helpers and use genuine returned -records in focused assertions, even when writes are intercepted. A missing query -allowlist entry is a policy blocker, not backend unavailability: inspect bounded -controller/service evidence and request a narrowly scoped configuration review. -Do not silently substitute synthetic data, disable guards, add wildcard/global -POST grants or bypass with another client. Read-only jobs must not edit -configuration; approved changes require a fresh reviewed context/job. Keep Save, -Submit, Approve and autosave behind existing write controls. Do not override -synthetic-only, API-only, unit-only or production policy. Authentication/configuration -reads and synthetic UI evidence do not prove business-data integration. +action; never substitute a shell/browser POST or relax the profile. When `validation.liveTestsRequired` is `false`, follow unit-test-only validation: run configured local unit tests and required offline checks, without application @@ -209,23 +189,6 @@ keep validation status and risks current in the PR, and never mark it ready for review until every required local gate passes or repository policy records an explicitly accepted outcome. -Reusable PR learning: within the existing pr stage, only when source edits and -PR publication are authorized, inspect task-relevant existing repository skills -for verified, non-obvious, reusable knowledge from this change. Prefer to update -a matching skill; otherwise create `.github/skills//SKILL.md` with YAML name -and description; the name must match the lowercase-hyphenated topic directory. -Capture when to use it, the verified solution, repository file/test references, -exact working directories and validation commands, and pitfalls. Skills are for -future developers and relevant agent tasks, not a diary of the current run. -Include the skill in the same reviewed PR before committing/pushing; self-review -its instructions and run applicable documentation/skill checks. In the concise -PR body, link the added/updated skill or state no new reusable learning when none -qualifies; do not create empty, speculative or duplicate skills. Use no separate -roadmap stage. Read-only jobs must not write skills, commit or publish. Respect -repository instruction governance and existing approval boundaries. Do not add -allowed-tools or broaden permissions. Never include credentials, personal/customer -data, private endpoints or raw transcripts. - Honor an explicit publication choice in every mode: strict plus draft stays draft, with provider `isDraft: true` verified. Legacy strict policy with no publication preference retains ready intent. Strict required checks never change. @@ -267,8 +230,7 @@ the user selects any profile-approved non-production environment, use that backend and API as the primary business-data source. Keep access read-only and intercept mutations. Fall back to `synthetic-intercepted` only when the backend is unreachable, access prevents the read, or no safe records can exercise the -required behavior. Guard/query-allowlist rejections require configuration review, -not fallback. Preserve the exact limitation, do not replace an expected +required behavior. Preserve the exact limitation, do not replace an expected empty state with synthetic data, and state that a synthetic pass proves UI behavior only rather than validation of the selected backend or its data. diff --git a/bin/fixlab.js b/bin/fixlab.js index fe1f52a..f7ea258 100644 --- a/bin/fixlab.js +++ b/bin/fixlab.js @@ -50,7 +50,7 @@ import { runValidationGate } from "./validation-gate-client.js"; import { installNotifications } from "./notifications.js"; import { linkedApiProofPlan, runLinkedApiProof } from "../dashboard/linked-api-proof.js"; import { runPortalApiProof } from "../dashboard/portal-api-proof.js"; -import { configuredWorkflow, efficientWorkflowGuidance, COPILOT_EXECUTION_GUIDANCE, RUNTIME_RECOVERY_GUIDANCE, READ_ONLY_POST_GUIDANCE, API_ONLY_PLAYWRIGHT_GUIDANCE, UNIT_TEST_ONLY_GUIDANCE, CONCISE_PR_GUIDANCE, PR_LEARNING_GUIDANCE } from "../dashboard/workflow-policy.js"; +import { configuredWorkflow, efficientWorkflowGuidance, COPILOT_EXECUTION_GUIDANCE, RUNTIME_RECOVERY_GUIDANCE, API_ONLY_PLAYWRIGHT_GUIDANCE, UNIT_TEST_ONLY_GUIDANCE, CONCISE_PR_GUIDANCE } from "../dashboard/workflow-policy.js"; const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), ".."); const profileRelativePath = join( @@ -311,18 +311,6 @@ function loadProfile(repository) { } } -function playwrightWorkingDirectoryError(workingDirectory) { - const exists = existsSync(workingDirectory); - if (exists && statSync(workingDirectory).isDirectory()) { - return null; - } - return [ - `working directory is ${exists ? "not a directory" : "missing"}: ${workingDirectory}.`, - `Update browserAutomation.workingDirectory (or its applications.frontend.workingDirectory fallback) in ${profileRelativePath} to the actual existing application or test directory, relative to the repository root.`, - "The template's frontend path is an example. Do not create an empty frontend folder to satisfy this check." - ].join(" "); -} - function checkPlaywright(repository, profile) { const configuration = profile?.browserAutomation ?? {}; const apiOnly = configuration.mode === "api"; @@ -342,11 +330,11 @@ function checkPlaywright(repository, profile) { const browserName = configuration.browser ?? "chromium"; const browserChannel = configuration.channel; - const directoryError = playwrightWorkingDirectoryError(workingDirectory); - if (directoryError) { + if (!existsSync(workingDirectory)) { + const detail = `working directory is missing: ${workingDirectory}`; return { - packageCheck: { name: "Playwright package", ok: false, detail: directoryError }, - browserCheck: { name: runtimeName, ok: false, detail: directoryError } + packageCheck: { name: "Playwright package", ok: false, detail }, + browserCheck: { name: runtimeName, ok: false, detail } }; } @@ -374,13 +362,12 @@ function checkPlaywright(repository, profile) { ok: packageResult.status === 0 && Boolean(resolvedPackage), detail: packageResult.error?.message ?? - (resolvedPackage || - packageResult.stderr?.trim() || - "package resolution failed") + resolvedPackage ?? + packageResult.stderr?.trim() ?? + "package resolution failed" }; if (!packageCheck.ok) { - packageCheck.detail += `; verify browserAutomation.workingDirectory in ${profileRelativePath} points to the real application or test directory before reviewing "fixlab setup-playwright". An empty frontend folder does not configure the application.`; return { packageCheck, browserCheck: { @@ -529,10 +516,6 @@ function init(repository) { : "FixLab repository setup is already present." ); console.log("Update its paths, commands, ports, and allowed environments."); - console.log( - "The frontend and backend directories are example paths, not folders to create. Configure the actual application and test paths in the repository profile." - ); - console.log("Do not create empty application folders to satisfy Doctor."); return 0; } @@ -643,8 +626,7 @@ function doctor(repository, runtime) { ); const packageManager = checkPackageManager( profile?.validation?.commands?.frontendRestore, - existsSync(frontendWorkingDirectory) && statSync(frontendWorkingDirectory).isDirectory() - ? frontendWorkingDirectory : repository + existsSync(frontendWorkingDirectory) ? frontendWorkingDirectory : repository ); if (packageManager) { checks.push(packageManager); @@ -865,10 +847,8 @@ function setupPlaywright(repository, approved) { profile.applications?.frontend?.workingDirectory ?? "." ); - const directoryError = playwrightWorkingDirectoryError(workingDirectory); - if (directoryError) { - console.error(`Playwright ${directoryError}`); - printTroubleshooting(); + if (!existsSync(workingDirectory)) { + console.error(`Playwright working directory is missing: ${workingDirectory}`); return 1; } @@ -1070,13 +1050,10 @@ function launch(repository, request, runtime, targetEnvironment = "", model = "" const guidance = [ COPILOT_EXECUTION_GUIDANCE, ...(request ? [RUNTIME_RECOVERY_GUIDANCE] : []), - // Interactive sessions use packaged guidance; request prompts go through stdin. - ...(request && profile.validation?.liveTestsRequired !== false && - profile.browserAutomation?.mode !== "api" ? [READ_ONLY_POST_GUIDANCE] : []), + // Interactive sessions use the packaged agent guidance, keeping Windows argv bounded. ...(request ? [componentRetrievalGuidance(profile, request)] : []), ...(profile.validation?.liveTestsRequired === false ? [UNIT_TEST_ONLY_GUIDANCE] : []), ...(!readOnly ? [CONCISE_PR_GUIDANCE] : []), - ...(!readOnly && request ? [PR_LEARNING_GUIDANCE] : []), ...(profile.browserAutomation?.mode === "api" ? [API_ONLY_PLAYWRIGHT_GUIDANCE] : []), "For known external-folder prerequisites, offer permissionPreflight before executor launch; it only opens review, never grants access. Use reviewed component path-to-gate policies for change-aware validation; unknown paths, full-validation paths, explicit full coverage, and previously executed gates retain their required checks.", "Use fixlab_dashboard_control for user-authorized dashboard operations. Read status first and bind its instance and current job. Do not ask again for routine steps already authorized; ask only for missing decisions or required approvals. For 'test this PR', verify the assigned checkout matches the PR source commit, then start playwright-only using the repository's Playwright configuration, selected environment and human-controlled authentication. Do not change code or use another checkout's evidence. Normal guards apply.", diff --git a/com.github.copilot/agents/fixlab.agent.md b/com.github.copilot/agents/fixlab.agent.md index 3a203ce..3f1994c 100644 --- a/com.github.copilot/agents/fixlab.agent.md +++ b/com.github.copilot/agents/fixlab.agent.md @@ -26,41 +26,21 @@ repackage the rejected operation to evade review. **Deny and request safe recove reuses existing reviewed tests/helpers in the same blocked stage; preserve completed evidence and failures, and stop if a safe prerequisite is missing. -For authorized DEV/SIT/UAT data-changing POST actions, discover +For individually authorized DEV/SIT/UAT POST actions, discover `fixlab_nonproduction_post` in a native Copilot dashboard job. The **Allow POST -writes** checkbox is off by default. Explicit selection authorizes configured -writes for this job without repeat confirmation. Unchecked or recovered jobs -require expiring, single-use human confirmation of each exact URL and complete -payload. Allow all and batch approval never apply. Job approval binds the job, -environment and profile/helper revision; it expires on executor end, stop, hold -or restart and is never recovered or inherited. Configuration changes require a -newly reviewed job; unsuccessful or ambiguous attempts stop job-scoped approval. -`dataSafety.post.dev`, `.sit` and `.uat` hold separate +requests** start checkbox enables requests; unchecked, ask when an action is needed. +It never approves writes. `dataSafety.post.dev`, `.sit` and `.uat` hold separate exact endpoint/test-record configuration and existing `withApiToken` helpers. Legacy DEV `fixlab_dev_post` and `dataSafety.devPost` remain supported. -No credentials or response body enter +Each exact URL and complete +synthetic/approved test payload requires expiring, single-use human confirmation; +Allow all and batch approval never apply. No credentials or response body enter the result. Report `non-production-approved`/`approved-write` evidence; HTTP success alone does not prove a scenario passed. No automatic retries or redirects: an ambiguous result requires human inspection before any fresh action. Other environments, explicit read-only/intercepted selections, API-only read-only contracts, unit-only validation and human sign-in remain unchanged. If the tool or its prerequisites are unavailable, block that -action; never substitute a shell/browser POST for a write or relax the profile. - -Verified read-only POST searches: for browser live checks permitting real backend -reads, require a reviewed query contract and implementation proving no -data/workflow mutation. Match the exact approved environment, origin and path -through the existing browser/auth guard, including redirects and the supported -query payload. Route names and the job POST checkbox are not approval. Run -permitted queries through existing authenticated helpers and use genuine returned -records in focused assertions, even when writes are intercepted. A missing query -allowlist entry is a policy blocker, not backend unavailability: inspect bounded -controller/service evidence and request a narrowly scoped configuration review. -Do not silently substitute synthetic data, disable guards, add wildcard/global -POST grants or bypass with another client. Read-only jobs must not edit -configuration; approved changes require a fresh reviewed context/job. Keep Save, -Submit, Approve and autosave behind existing write controls. Do not override -synthetic-only, API-only, unit-only or production policy. Authentication/configuration -reads and synthetic UI evidence do not prove business-data integration. +action; never substitute a shell/browser POST or relax the profile. When `validation.liveTestsRequired` is `false`, follow unit-test-only validation: run configured local unit tests and required offline checks, without application @@ -209,23 +189,6 @@ keep validation status and risks current in the PR, and never mark it ready for review until every required local gate passes or repository policy records an explicitly accepted outcome. -Reusable PR learning: within the existing pr stage, only when source edits and -PR publication are authorized, inspect task-relevant existing repository skills -for verified, non-obvious, reusable knowledge from this change. Prefer to update -a matching skill; otherwise create `.github/skills//SKILL.md` with YAML name -and description; the name must match the lowercase-hyphenated topic directory. -Capture when to use it, the verified solution, repository file/test references, -exact working directories and validation commands, and pitfalls. Skills are for -future developers and relevant agent tasks, not a diary of the current run. -Include the skill in the same reviewed PR before committing/pushing; self-review -its instructions and run applicable documentation/skill checks. In the concise -PR body, link the added/updated skill or state no new reusable learning when none -qualifies; do not create empty, speculative or duplicate skills. Use no separate -roadmap stage. Read-only jobs must not write skills, commit or publish. Respect -repository instruction governance and existing approval boundaries. Do not add -allowed-tools or broaden permissions. Never include credentials, personal/customer -data, private endpoints or raw transcripts. - Honor an explicit publication choice in every mode: strict plus draft stays draft, with provider `isDraft: true` verified. Legacy strict policy with no publication preference retains ready intent. Strict required checks never change. @@ -267,8 +230,7 @@ the user selects any profile-approved non-production environment, use that backend and API as the primary business-data source. Keep access read-only and intercept mutations. Fall back to `synthetic-intercepted` only when the backend is unreachable, access prevents the read, or no safe records can exercise the -required behavior. Guard/query-allowlist rejections require configuration review, -not fallback. Preserve the exact limitation, do not replace an expected +required behavior. Preserve the exact limitation, do not replace an expected empty state with synthetic data, and state that a synthetic pass proves UI behavior only rather than validation of the selected backend or its data. diff --git a/dashboard/copilot-session.js b/dashboard/copilot-session.js index 06a93bd..97ae6c8 100644 --- a/dashboard/copilot-session.js +++ b/dashboard/copilot-session.js @@ -89,8 +89,6 @@ export async function runCopilotSession(config, { client, ask, output, onUsage = mutationMode: config.testMutationMode, selectedMutationMode: config.selectedTestMutationMode, allowPostRequests: config.allowPostRequests, - postWriteApproval: config.postWriteApproval, - jobId: config.sessionId, isReady: () => policyReady && !budgetReached }); const options = { diff --git a/dashboard/dev-post.js b/dashboard/dev-post.js index 482a0e9..d828e76 100644 --- a/dashboard/dev-post.js +++ b/dashboard/dev-post.js @@ -57,35 +57,14 @@ function configurationAt(repository, environment = "dev", jobConsent = false) { revision: createHash("sha256").update(raw).update(readFileSync(helper)).digest("hex") }; } -export function createPostWriteApproval(repository, targetEnvironment, jobId) { - const environment = typeof targetEnvironment === "string" ? targetEnvironment.toLowerCase() : ""; - if (!["dev", "sit", "uat"].includes(environment) || - typeof jobId !== "string" || !/^[a-f0-9-]{36}$/u.test(jobId)) { - throw new Error("Job-scoped POST approval requires an exact job and DEV/SIT/UAT environment."); - } - return { jobId, environment, revision: configurationAt(repository, environment, true)?.revision ?? null }; -} - export function createNonProductionPostTool({ repository, targetEnvironment, mutationMode, - selectedMutationMode = "", allowPostRequests, postWriteApproval, jobId, isReady, ask, + selectedMutationMode = "", allowPostRequests, isReady, ask, request = fetch, now = Date.now }) { if (allowPostRequests !== undefined && typeof allowPostRequests !== "boolean") { throw new Error("allowPostRequests must be a boolean."); } const environment = typeof targetEnvironment === "string" ? targetEnvironment.toLowerCase() : ""; const jobConsent = typeof allowPostRequests === "boolean"; - const jobScoped = postWriteApproval !== undefined && postWriteApproval !== null; - if (jobScoped && - (typeof postWriteApproval !== "object" || Array.isArray(postWriteApproval) || - allowPostRequests !== true || postWriteApproval.jobId !== jobId || - postWriteApproval.environment !== environment || - typeof jobId !== "string" || !/^[a-f0-9-]{36}$/u.test(jobId) || - (postWriteApproval.revision !== null && - !/^[a-f0-9]{64}$/u.test(postWriteApproval.revision)))) { - throw new Error("Invalid job-scoped POST approval; start a newly reviewed job."); - } - const approvedRevision = postWriteApproval?.revision; - let approvalStopped = false; if (!["dev", "sit", "uat"].includes(environment) || selectedMutationMode || (!jobConsent && (environment !== "dev" || mutationMode !== "approved-write"))) { return null; @@ -95,7 +74,7 @@ export function createNonProductionPostTool({ repository, targetEnvironment, mut const label = environment.toUpperCase(); return { name: !jobConsent && environment === "dev" ? "fixlab_dev_post" : "fixlab_nonproduction_post", - description: `Send one ${label} POST using its configured endpoint/test-record allowlist and existing authentication helper. ${jobScoped ? "The user approved this job's configured writes through the POST checkbox; no repeat confirmation is needed. Approval is bound to this job and the reviewed profile/helper revision." : "The complete URL and JSON payload require individual human confirmation, even with Allow all."} Missing setup blocks the action, not unrelated work. No redirects, retries, credentials or response bodies. This is a real write, not intercepted evidence.`, + description: `Request one ${label} POST using its configured endpoint/test-record allowlist and existing authentication helper. The complete URL and JSON payload always require individual human confirmation, even with the job-start checkbox or Allow all. If unchecked at job start, ask for this action when needed. Missing setup blocks the action, not unrelated work. No credentials or response body are returned. This is a real write, not intercepted evidence.`, skipPermission: true, parameters: { type: "object", @@ -112,17 +91,10 @@ export function createNonProductionPostTool({ repository, targetEnvironment, mut throw new Error("POST requires credential-sensitive debug logging to be disabled."); } if (!isReady()) { throw new Error("POST is blocked by policy, the execution budget or executor shutdown."); } - if (approvalStopped) { - throw new Error("The job-scoped POST approval has stopped after an unsuccessful attempt or changed configuration. Inspect the selected environment and start a newly reviewed job; no request was sent."); - } const plan = planAt(); if (!plan) { throw new Error(`${label} POST is not configured. Configure dataSafety.post.${environment} with approved endpoints, test records and an existing API session helper. No request was sent.`); } - if (jobScoped && approvedRevision !== plan.revision) { - approvalStopped = true; - throw new Error("POST policy/helper changed or was not configured at approval; start a newly reviewed job. No request was sent."); - } if (!args || Object.keys(args).some((key) => !["url", "payload", "purpose"].includes(key)) || !args.payload || typeof args.payload !== "object" || Array.isArray(args.payload) || !safeRuntimeText(args.purpose, 200) || !args.purpose.trim()) { @@ -139,19 +111,17 @@ export function createNonProductionPostTool({ repository, targetEnvironment, mut if (!action) { throw new Error("POST endpoint or test record is not explicitly allowlisted."); } const url = action.url; const expiresAt = now() + 300000; - if (!jobScoped) { - const details = JSON.stringify({ environment: label, method: "POST", url, - purpose: args.purpose, payload: body, expiresAt: new Date(expiresAt).toISOString(), - ...(jobConsent ? { enabledAtJobStart: allowPostRequests } : {}), - notice: "Real data-changing action. Review every field. One attempt only; no redirects, retries or blanket approval." }, null, 2); - if (!safeRuntimeText(details)) { throw new Error("POST review is sensitive or oversized."); } - const decision = await ask({ - kind: "permission", title: `${allowPostRequests === false ? "Allow" : "Confirm"} one ${label} POST action`, details, - canApprove: true, perAction: true, choices: [], allowFreeform: false - }); - if (decision.action !== "allow" || decision.scope !== undefined || decision.recover !== undefined) { - throw new Error("POST was not individually approved; no request was sent."); - } + const details = JSON.stringify({ environment: label, method: "POST", url, + purpose: args.purpose, payload: body, expiresAt: new Date(expiresAt).toISOString(), + ...(jobConsent ? { enabledAtJobStart: allowPostRequests } : {}), + notice: "Real data-changing action. Review every field. One attempt only; no redirects, retries or blanket approval." }, null, 2); + if (!safeRuntimeText(details)) { throw new Error("POST review is sensitive or oversized."); } + const decision = await ask({ + kind: "permission", title: `${allowPostRequests === false ? "Allow" : "Confirm"} one ${label} POST action`, details, + canApprove: true, perAction: true, choices: [], allowFreeform: false + }); + if (decision.action !== "allow" || decision.scope !== undefined || decision.recover !== undefined) { + throw new Error("POST was not individually approved; no request was sent."); } if (!isReady() || now() >= expiresAt || planAt()?.revision !== plan.revision) { throw new Error("POST approval expired or its policy/helper changed; no request was sent."); @@ -190,7 +160,6 @@ export function createNonProductionPostTool({ repository, targetEnvironment, mut } }); } catch { - approvalStopped = jobScoped; if (attempts) { throw new Error("POST may have been attempted. Approval is spent. Inspect the selected environment before any new action; no automatic retry was made."); } @@ -198,12 +167,8 @@ export function createNonProductionPostTool({ repository, targetEnvironment, mut } finally { accepting = false; } - if (attempts !== 1 || !result) { - approvalStopped = jobScoped; - throw new Error("POST authentication helper did not complete one request. Inspect the selected environment; no automatic retry was made."); - } + if (attempts !== 1 || !result) { throw new Error("POST authentication helper did not complete one request."); } if (result.status < 200 || result.status >= 300) { - approvalStopped = jobScoped; throw new Error(`POST returned HTTP ${result.status}. Approval is spent; no redirect or automatic retry was made.`); } return result; diff --git a/dashboard/intake.js b/dashboard/intake.js index 410217f..ac8680a 100644 --- a/dashboard/intake.js +++ b/dashboard/intake.js @@ -34,8 +34,7 @@ function runGit(repository, args) { const result = spawnSync("git", args, { cwd: repository, encoding: "utf8", - shell: false, - windowsHide: true + shell: false }); return result.status === 0 ? result.stdout.trim() : null; } diff --git a/dashboard/public/app.js b/dashboard/public/app.js index 680cee6..0bf5b0d 100644 --- a/dashboard/public/app.js +++ b/dashboard/public/app.js @@ -98,18 +98,17 @@ const targetEnvironmentSelect = document.querySelector( ); const allowPostRequestsInput = document.querySelector("#allow-post-requests"); let postUiBlocked = true; -let postRuntimeAvailable = false; let postCheckboxEnvironment = null; function updatePostRequestsOption() { const configured = [...targetEnvironmentSelect.options].map((option) => option.value).filter(Boolean); const environment = (targetEnvironmentSelect.value || (configured.length === 1 ? configured[0] : "")).toLowerCase(); - const available = postRuntimeAvailable && !postUiBlocked && ["dev", "sit", "uat"].includes(environment) && + const available = !postUiBlocked && ["dev", "sit", "uat"].includes(environment) && !document.querySelector('[name="testMutationMode"]').value; const key = available ? environment : ""; if (postCheckboxEnvironment !== key) { - allowPostRequestsInput.checked = false; + allowPostRequestsInput.checked = available; postCheckboxEnvironment = key; } allowPostRequestsInput.disabled = !available; @@ -611,7 +610,7 @@ function renderRuntimeRequest(job, isActive) { return; } runtimeGuidance.textContent = pending.perAction === true - ? "This is a real non-production POST action without current job-scoped write approval. Review its environment, exact destination and complete payload. It requires single-use confirmation; Allow all does not approve it." + ? "This is a real non-production POST action. Review its environment, exact destination and complete payload. It requires its own single-use confirmation, even when the job-start checkbox or Allow all is active." : pending.kind === "permission" && !pending.canApprove ? "This operation cannot be approved here. Read the specific reasons below. Safe recovery denies the operation; it never overrides a restriction or replays hidden details." : pending.kind === "question" @@ -1255,7 +1254,6 @@ function renderReadiness(readiness) { } function renderRuntime(runtime) { - postRuntimeAvailable = runtime?.name === "copilot"; runtimeHelperDirectory = runtime?.helperDirectory ?? ""; const supported = runtime?.supportsModelSelection === true; document.querySelector("#input-budget-visibility").hidden = !supported; @@ -2471,7 +2469,6 @@ form.addEventListener("submit", async (event) => { targetEnvironment: data.get("targetEnvironment"), testMutationMode: data.get("testMutationMode") ?? "", allowPostRequests: !allowPostRequestsInput.disabled && data.get("allowPostRequests") === "on", - postWriteApprovalConfirmed: !allowPostRequestsInput.disabled && data.get("allowPostRequests") === "on", prestartFrontend: data.get("prestartFrontend") === "on", recordPlaywrightVideo: data.get("recordPlaywrightVideo") === "on", @@ -2484,7 +2481,6 @@ form.addEventListener("submit", async (event) => { }); requestInput.value = ""; form.querySelector('[name="allowAllTools"]').checked = false; - allowPostRequestsInput.checked = false; requestInput.focus(); selectedScreenshots = []; renderScreenshots(); diff --git a/dashboard/public/index.html b/dashboard/public/index.html index fd17102..a92ae60 100644 --- a/dashboard/public/index.html +++ b/dashboard/public/index.html @@ -284,8 +284,8 @@

Azure DevOps intake (optional)

This choice applies only to this job, not shared defaults. Intercepted mode can keep permitted backend reads real. Live read-only prohibits writes and requires a profile allowing non-production reads; repository-required guards remain authoritative. Neither option authorizes Save, Submit, Approve or other real writes.

- -

Native Copilot, DEV/SIT/UAT only; off by default. Checking this authorizes configured endpoint/test-record writes for this job without repeat confirmation. If unchecked, each write needs confirmation. Saved endpoint/test-record/authentication configuration is still required. Approval expires on stop, hold, completion or restart; configuration changes require a newly reviewed job. Explicit read-only/intercepted modes and production remain blocked. Browser guards are not disabled.

+ +

DEV, SIT and UAT only. If unchecked, FixLab can ask when a POST is needed. Every data-changing POST still requires its own destination/payload confirmation and saved endpoint/test-record/authentication configuration. Explicit read-only/intercepted modes block writes. Production is excluded.

Batch completion diff --git a/dashboard/server.js b/dashboard/server.js index b75bbc5..ec119da 100644 --- a/dashboard/server.js +++ b/dashboard/server.js @@ -41,10 +41,9 @@ import { canonicalExecutionRepository, createExecutorLease } from "./executor-le import { spawnWindowsExecutor } from "./windows-executor.js"; import { createRuntimeRequestBridge, MAX_RUNTIME_FRAME_BYTES, validateRuntimeUsage, validateToolApprovalMode } from "./runtime-requests.js"; import { validateJobFileAccess } from "./runtime-permission-policy.js"; -import { configuredWorkflow, efficientWorkflowGuidance, COPILOT_EXECUTION_GUIDANCE, RUNTIME_RECOVERY_GUIDANCE, READ_ONLY_POST_GUIDANCE, EXISTING_APPLICATION_GUIDANCE, API_ONLY_PLAYWRIGHT_GUIDANCE, UNIT_TEST_ONLY_GUIDANCE, CONCISE_PR_GUIDANCE, PR_LEARNING_GUIDANCE } from "./workflow-policy.js"; +import { configuredWorkflow, efficientWorkflowGuidance, COPILOT_EXECUTION_GUIDANCE, RUNTIME_RECOVERY_GUIDANCE, EXISTING_APPLICATION_GUIDANCE, API_ONLY_PLAYWRIGHT_GUIDANCE, UNIT_TEST_ONLY_GUIDANCE, CONCISE_PR_GUIDANCE } from "./workflow-policy.js"; import { componentRetrievalGuidance } from "./affected-surface.js"; import { apiAuthenticationConfiguration } from "./api-authentication.js"; -import { createPostWriteApproval } from "./dev-post.js"; import { browserConfigurationErrors } from "./repository-setup.js"; import { applyRepositorySettings, inspectRepositorySettings, localRepository, planRepositorySettings } from "./repository-settings.js"; import { configuredLinkedApplications, resolveLinkedApplications } from "./linked-applications.js"; @@ -581,7 +580,6 @@ function publicQueue(jobs) { prestartFrontend: job.prestartFrontend ?? false, inputTokenLimit: job.inputTokenLimit ?? 0, toolApprovalMode: job.toolApprovalMode ?? "ask", - postWriteApproved: Boolean(job.postWriteApproval?.revision), fileAccess: job.fileAccess ?? null, recordPlaywrightVideo: job.recordPlaywrightVideo, manualLocalhostTest: job.manualLocalhostTest, @@ -750,7 +748,6 @@ function restoreDashboardJob(snapshot, repository) { : {}), validationRecovered: true, toolApprovalMode: "ask", - postWriteApproval: null, permissionRequest: snapshot.permissionBlocked ? newPermissionRequest({ preflight: snapshot.permissionPreflightPending === true }) : permissionRequest(snapshot.stages), @@ -1156,7 +1153,7 @@ export function buildJobPrompt({ testEvidence.source === "non-production-read-only" ? `- Use the selected ${targetEnvironment} backend and API as the primary business-data source. Keep all backend access read-only and intercept every mutating request. - Do not fulfill or intercept business-data reads while the selected backend is available and returns safe records that can prove the required assertions. -- If the selected backend is unreachable, authentication or backend service access prevents the read, or it returns no safe records capable of exercising the required behavior, preserve that exact backend limitation and then fall back to synthetic-intercepted data for the focused UI journey. A query allowlist rejection is not a backend limitation and must not trigger this fallback. +- If the selected backend is unreachable, authentication or access prevents the read, or it returns no safe records capable of exercising the required behavior, preserve that exact backend limitation and then fall back to synthetic-intercepted data for the focused UI journey. - Do not replace an expected empty-state assertion with synthetic data. Use fallback only when records are required to exercise the reported behavior and the selected backend cannot supply them. - When fallback occurs, emit a FIXLAB_ACTIVITY line explaining why, intercept all mutations, and emit FIXLAB_TEST with source synthetic-intercepted and mutation mode intercepted. A synthetic pass proves the UI behavior only; do not claim the selected backend or its data was validated.` : "- Use the configured test data source and mutation mode. For synthetic-intercepted tests, fulfill business-data reads locally, intercept every mutating request, and assert the intended request count and payload without changing an external record."; @@ -1179,7 +1176,12 @@ ${process.platform === "win32" ? "- Windows executor children are job-contained const branchNamingGuidance = branchNaming ? `- Repository branch naming is configured as ${branchNaming.prefix}. Create or reuse only branches beneath this prefix. Do not substitute a runtime, bot, or agent name for the configured user ID.` : "- Follow the repository's existing branch naming policy; do not invent a bot-specific prefix."; - const requestGuidance = smallEnhancement + const requestGuidance = playwrightOnly + ? `- Use the request only to select the relevant repository-defined Playwright journey and expected behavior. +- Perform only the minimum setup required by the repository profile: verify browser authentication, start required applications, and run the focused Playwright journey. +- Do not diagnose source code, reproduce through separate non-browser checks, review a diff, run unrelated tests, type-checks, or builds, or inspect implementation files unless the Playwright journey cannot be selected or started without that bounded information. +- Preserve exact browser results, screenshots, traces, skipped gates, blockers, and remaining risks.` + : smallEnhancement ? `- Generate a concise acceptance contract before changing code. - Check the affected surface and bound the file scope before editing. - Do not manufacture a failing defect reproduction. Use the smallest existing focused test that proves the acceptance contract. @@ -1191,11 +1193,11 @@ ${process.platform === "win32" ? "- Windows executor children are job-contained if (playwrightOnly) { return `FixLab output protocol (follow before any tool call): - First emit FIXLAB_STAGE|intake|running|Accepted the request and starting intake. -- Before each stage emit FIXLAB_STAGE|stage|running|next action and its terminal outcome before the next stage. Never defer or batch stage markers. -- Markers are literal assistant response lines, never shell, files, tool results or code blocks. -- Stages: ${FIXLAB_STAGES.join(", ")}. Terminal statuses: passed, skipped, blocked, failed. Finish every stage; never report skipped checks as passed. -- FIXLAB_ACTIVITY|stage|summary: meaningful evidence/blockers only, at most twice per stage unless a new blocker changes the plan. No secrets or private data. -- Emit FIXLAB_TEST|source|mutation-mode|scenario on scenario selection and data/mutation changes. Sources: ${[...testDataSources].join(", ")}. Mutation modes: ${[...mutationModes].join(", ")}. +- Before each stage emit FIXLAB_STAGE|stage|running|next action; emit its terminal outcome before starting another stage. Never defer or batch stage markers until the end. +- Write markers as literal assistant response lines, never through shell, files, tool results or code blocks. +- Stages: ${FIXLAB_STAGES.join(", ")}. Terminal statuses: passed, skipped, blocked, failed. Emit every stage's terminal outcome before finishing; never report skipped checks as passed. +- Emit FIXLAB_ACTIVITY|stage|summary only for meaningful evidence or blockers, at most twice per stage unless a new blocker changes the plan. Do not expose secrets or private data. +- Emit FIXLAB_TEST|source|mutation-mode|scenario after selecting the scenario and whenever its data source or mutation behavior changes. Sources: ${[...testDataSources].join(", ")}. Mutation modes: ${[...mutationModes].join(", ")}. - Expected bug IDs: ${bugResults.map((bug) => bug.id).join(", ") || "none"}. For each, emit FIXLAB_BUG|id|outcome|owner|summary before finishing. Outcomes: ${[...bugOutcomeStatuses].join(", ")}. Attribute upstream limitations honestly. Run a FixLab playwright-only job. @@ -1211,30 +1213,30 @@ Test synthesis: ${testEvidence.enabled ? "enabled" : "disabled"} Scenario evidence required: ${testEvidence.requireScenarioEvidence ? "yes" : "no"} ${intakeSummary.length ? `Loaded work items: ${JSON.stringify(intakeSummary)}\n` : ""}${screenshotPaths.length ? `User-provided screenshots: ${JSON.stringify(screenshotPaths)}. Inspect only these; do not download additional attachments.\n` : ""}${cacheSummary ? `Verified same-repository cache metadata: ${JSON.stringify(cacheSummary)}. Verify task-specific facts; this is not source evidence.\n` : ""} Repository requirements: -- Read .github/fixlab/repository-profile.json and all applicable repository instructions; preserve safety, authentication, environment and process ownership. +- Read .github/fixlab/repository-profile.json and all applicable repository instructions; preserve their safety, authentication, environment and process-ownership requirements. ${COPILOT_EXECUTION_GUIDANCE} ${RUNTIME_RECOVERY_GUIDANCE} -${!apiOnly && !testEvidence.unitTestsOnly ? READ_ONLY_POST_GUIDANCE : ""} ${apiOnly ? API_ONLY_PLAYWRIGHT_GUIDANCE : EXISTING_APPLICATION_GUIDANCE} +${requestGuidance} - Skip source diagnosis, separate reproduction, implementation, diff review and unrelated non-browser gates. The diagnosis, reproduce, fix, review, and pr stages must be explicitly skipped unless their own required prerequisite fails. - Do not edit files other than an authorized transient browser-validation artifact. Do not commit, push, create or update pull requests, or change repository configuration. -- Use the request to select expected behavior and the focused Playwright journey. Read the profile, browser configuration, fixture and nearest test in one bounded batch; prefer an existing journey or retained script. Inspect implementation only for a concrete missing selector or prerequisite. -- Generate one parameterized deterministic Playwright script for repeated rows/scenarios; use the repository runner, not per-row/field model round-trips. -- Keep the same-job script and compact results through retries/human waits; reuse unchanged selectors/helpers and change only failing steps. Never delete running artifacts. Remove transient source only after validation and continuations finish; permanent coverage needs separate authorization. -- Preserve forms and human-entered values; never replay stale snapshots or successful mutations. Do not copy browser state or credentials; sign-in/MFA remain human-controlled. -- Do not reinstall dependencies that are already available. Run only minimum setup required by the repository profile: verify browser authentication, required applications and the focused Playwright journey. State files alone do not prove sign-in. +- Read the profile, existing browser configuration, fixture and nearest matching test in one bounded batch. Prefer an existing journey or retained same-job script. Inspect implementation files only to resolve a concrete missing selector or prerequisite. +- Generate one parameterized deterministic Playwright script for repeated rows and scenarios; execute it through the repository's runner, not one model/tool round-trip per row or field. +- Keep the same-job script and its compact scenario/result summary through retries and human waits. Reuse unchanged selectors and helpers; modify only the failing step. Do not delete artifacts while a test is running. Remove transient source only when validation is finished and no continuation needs it; permanent product test coverage needs separate authorization. +- Preserve forms and human-entered values. Never replay a stale snapshot or repeat successful mutations to recover context. Do not copy browser state or credentials; sign-in and MFA remain human-controlled. +- Do not reinstall dependencies that are already available. Run only minimum setup required by the repository profile; verify browser authentication without treating existing state files as proof of sign-in. ${uiScenarioGuidance} ${environmentGuidance} ${testDataGuidance} -- No Save, Submit, Approve, autosave or external write is authorized by this request or interception selector. Profile safety remains authoritative; writes require separate native approval. +- Profile safety rules remain authoritative. Read-only POST searches are not mutations merely because of their method. No Save, Submit, Approve, autosave or other external write is authorized by this validation request or its interception selector. ${videoGuidance} ${manualLocalhostGuidance} -- Browser journeys require a non-sensitive screenshot in repository-owned test-results; API-only runs retain sanitized assertions instead. Preserve exact assertions, counts, errors, traces and paths. Never fabricate results or claim intercepted payloads prove backend writes. -- Run verbose commands through fixlab exec --stage live-test -- [args...]; quote '--' in Windows PowerShell. Keep compact results and bounded failure diagnostics. Never reuse cached live-browser, authentication or external-data results. -- Continue independent safe steps after unrelated failures, never across failed prerequisites. Mark required full regression deferred, not passed. -- Do not delegate one continuous journey or repeatedly enumerate the repository; batch independent reads and bound output. -- Missing decisions/authentication/authorization block only the affected stage. Use native live questions below; never infer approval, waive policy or collect credentials. -- Finish with exact passed/failed assertions, skipped gates, blockers and risks before task_complete. +- For browser journeys, save at least one non-sensitive screenshot under repository-owned test-results. API-only journeys preserve sanitized API assertion evidence instead. Preserve exact assertions, request counts, errors and evidence paths. Do not fabricate execution or claim intercepted payloads prove backend writes. +- Run verbose commands through fixlab exec --stage live-test -- [args...]; quote '--' in Windows PowerShell. Keep compact results in context, inspecting only a bounded diagnostic on failure. Never reuse cached live-browser, authentication or external-data results. +- Continue independent safe browser steps when an unrelated check fails; do not bypass a failed prerequisite. Report required full regression as deferred, not passed. +- Do not delegate one continuous browser journey or repeatedly enumerate the repository. Keep command output bounded and batch independent reads. +- For genuine missing decisions, authentication or authorization, report the exact blocker. Native dashboard instructions below govern live questions; never infer approvals, waive policy or collect credentials. Otherwise block the affected stage and stop safely. +- Before finishing emit all required terminal markers, exact passed/failed assertions, skipped gates, blockers and remaining risks. Do not call task_complete before the protocol is complete. Request: ${request}`; @@ -1276,7 +1278,6 @@ Repository requirements: - Read .github/fixlab/repository-profile.json and all repository-owned instructions before acting. ${COPILOT_EXECUTION_GUIDANCE} ${RUNTIME_RECOVERY_GUIDANCE} -${!apiOnly && !testEvidence.unitTestsOnly ? READ_ONLY_POST_GUIDANCE : ""} ${apiOnly ? API_ONLY_PLAYWRIGHT_GUIDANCE : ""} - For a fix request, begin with the smallest known reproduction, using an approved existing application when appropriate; prepare the local stack only when needed to exercise changed code. Do not claim a deployed baseline proves an unpublished fix. - Before validation in a new isolated worktree, plan \`fixlab onboard WORKTREE --from SOURCE --runtime RUNTIME\`. After scoped setup approval, add \`--yes\`. The source profile declares non-sensitive configuration in \`worktreeSetup.files\`. Never copy credentials or browser state. Missing configuration, authentication or denied runtime access is a blocker; do not retry unchanged approvals or grant blanket path access. Sign-in and MFA remain human-controlled. In read-only modes, do not provision or alter configuration; report missing prerequisites as blocked. @@ -1316,7 +1317,6 @@ ${efficient ? efficientWorkflowGuidance(workflow) : `- Once the scoped productio - ${readOnly ? "Do not publish a pull request or change its draft status in read-only mode." : workflow?.publication === "draft" ? "Keep the published PR draft and read back provider isDraft=true before reporting publication passed. Marking ready requires a separate reviewed decision. Strict required checks remain mandatory." : "After required local validation and approval gates pass or have an explicitly accepted outcome, publish the authorized PR by marking it ready for review. Do not wait for remote PR CI or build completion before publication; publication can trigger those checks. Verify the PR is no longer draft before reporting the PR stage passed. Report remote checks as pending, not passed, and leave merge policies intact. Use `gh pr ready ` for GitHub or `az repos pr update --id --draft false` for Azure DevOps, then read back `isDraft` from the provider to confirm it is false. If publication or state verification fails, report the PR stage blocked or failed, not passed."} - Prefix FixLab-created pull-request titles with FLAB: followed by a concise description of the coding change. Format the body with the repository's PR template, clear Markdown sections for the problem, changes, validation and risks, and accurate validation checkboxes. ${readOnly ? "" : CONCISE_PR_GUIDANCE} -${readOnly ? "" : PR_LEARNING_GUIDANCE} - Emit one browser evidence line after the scenario is selected and again if the actual source or mutation behavior changes: FIXLAB_TEST|source|mutation-mode|scenario - source must be one of: ${[...testDataSources].join(", ")}. @@ -1399,8 +1399,7 @@ function runGit(repository, args) { const result = spawnSync("git", args, { cwd: repository, encoding: "utf8", - shell: false, - windowsHide: true + shell: false }); return result.status === 0 ? result.stdout.trim() : null; } @@ -2010,7 +2009,6 @@ function createExecutor({ command, packageRoot, buildInvocation, defaultModel = testMutationMode, selectedTestMutationMode, allowPostRequests, - postWriteApproval, protectedPaths, onOutput, onUsage = () => {}, @@ -2047,7 +2045,7 @@ function createExecutor({ command, packageRoot, buildInvocation, defaultModel = invocation = { args: [join(packageRoot, "dashboard", "copilot-session.js")], input: JSON.stringify({ - type: "start", packageRoot, repository, prompt, sessionId, resume, usage, mode, inputTokenLimit, toolApprovalMode, protectedPaths, fileAccess, dashboardPort, targetEnvironment, testMutationMode, selectedTestMutationMode, allowPostRequests, postWriteApproval, + type: "start", packageRoot, repository, prompt, sessionId, resume, usage, mode, inputTokenLimit, toolApprovalMode, protectedPaths, fileAccess, dashboardPort, targetEnvironment, testMutationMode, selectedTestMutationMode, allowPostRequests, model: model ?? defaultModel, additionalDirs: [ ...linkedDependencyDirectories(repository), ...artifactDirectories, ...runtimeDirectories @@ -2337,7 +2335,6 @@ function publicJob(job) { fileAccess: job.fileAccess ?? null, inputTokenLimit: job.inputTokenLimit ?? 0, toolApprovalMode: job.toolApprovalMode ?? "ask", - postWriteApproved: Boolean(job.postWriteApproval?.revision), budgetExceeded: job.budgetExceeded === true, runtimeDirectoryGrants: (job.runtimeDirectoryGrants ?? []).map((scope) => scope.directory), canResume: @@ -2760,7 +2757,6 @@ function pushActivity(job, entry) { function finishJob(job, result) { job.toolApprovalMode = "ask"; - job.postWriteApproval = null; flushPartialOutput(job); if (result.error?.workspaceReview) { @@ -2933,7 +2929,6 @@ function flushPartialOutput(job) { function holdJob(job) { job.toolApprovalMode = "ask"; - job.postWriteApproval = null; flushPartialOutput(job); const activeStage = FIXLAB_STAGES.find((stage) => job.stages[stage].status === "running") ?? @@ -3434,7 +3429,6 @@ function listPlaywrightArtifacts(repository, { since = null } = {}) { function resetJobForResume(job) { job.toolApprovalMode = "ask"; - job.postWriteApproval = null; const restartIndex = job.status === "passed" ? FIXLAB_STAGES.indexOf("diagnosis") @@ -3954,7 +3948,7 @@ export function createDashboardServer({ if (typeof job.testEvidence.allowPostRequests === "boolean") { configuredTestSynthesis(currentProfile, job.testEvidence.selectedMutationMode ?? "", job.testEvidence.allowPostRequests); - prompt += `\n${job.postWriteApproval ? "The user explicitly authorized configured DEV/SIT/UAT POST writes for this job through the POST checkbox. Use fixlab_nonproduction_post without repeat confirmation; its approval is bound to this job, environment and reviewed profile/helper revision and expires when execution ends." : "No current job-scoped POST write approval exists. Every data-changing action still requires separate exact destination/payload confirmation through fixlab_nonproduction_post."} Verified read-only POST searches follow the repository query allowlist independently of this checkbox. Approved per-environment endpoint/test-record configuration and an existing authentication helper are required; if missing, name that prerequisite and block only the action. Explicit read-only/intercepted modes, API-only/unit-only contracts and production remain restricted. Never bypass browser guards or substitute a browser/shell write. Report approved-write evidence honestly after a real action; approval alone is not scenario success.`; + prompt += `\nThe job-start POST checkbox was ${job.testEvidence.allowPostRequests ? "selected" : "not selected"}. For DEV/SIT/UAT, ${job.testEvidence.allowPostRequests ? "POST request support is enabled" : "ask for an exact POST action when needed"} through fixlab_nonproduction_post only. Every data-changing action still requires separate exact destination/payload confirmation. No blanket write approval exists. Approved per-environment endpoint/test-record configuration and an existing authentication helper are required; if missing, name that prerequisite and block only the action. Explicit read-only/intercepted modes, API-only/unit-only contracts and production remain restricted. Keep ordinary validation read-only unless this separate per-action approval is actually granted; report approved-write evidence honestly after a real action.`; } if (job.testEvidence.selectedMutationMode) { const effective = configuredTestSynthesis( @@ -4206,7 +4200,6 @@ export function createDashboardServer({ selectedTestMutationMode: job.testEvidence.selectedMutationMode ?? "", allowPostRequests: job.testEvidence.apiOnly || job.testEvidence.unitTestsOnly ? undefined : job.testEvidence.allowPostRequests, - postWriteApproval: job.postWriteApproval ?? undefined, protectedPaths, mode: job.mode, onUsage(usage) { @@ -6384,23 +6377,6 @@ export function createDashboardServer({ return; } } - if (body.postWriteApprovalConfirmed !== undefined && - typeof body.postWriteApprovalConfirmed !== "boolean") { - sendJson(response, 400, { error: "postWriteApprovalConfirmed must be a boolean." }); - return; - } - if (body.postWriteApprovalConfirmed === true) { - if (body.allowPostRequests !== true || runtime !== "copilot") { - sendJson(response, 400, { error: "Job-scoped POST approval requires the selected POST checkbox and native Copilot runtime." }); - return; - } - const expectedOrigin = `http://${DASHBOARD_HOST}:${server.address().port}`; - if (request.headers.host !== new URL(expectedOrigin).host || - request.headers.origin !== expectedOrigin) { - sendJson(response, 403, { error: "POST write approval requires the dashboard's exact loopback host and origin." }); - return; - } - } const inputTokenLimit = body.inputTokenLimit ?? 0; if (!Number.isSafeInteger(inputTokenLimit) || inputTokenLimit < 0 || (inputTokenLimit > 0 && runtime !== "copilot")) { @@ -6615,16 +6591,6 @@ export function createDashboardServer({ return; } - const jobId = randomUUID(); - let postWriteApproval = null; - try { - if (body.postWriteApprovalConfirmed === true) { - postWriteApproval = createPostWriteApproval(resolvedRepository, targetEnvironment, jobId); - } - } catch (error) { - sendJson(response, 400, { error: error.message }); - return; - } shouldQueue = Boolean(activeHandle || currentJob?.postValidation) || ["running", "blocked", "failed"].includes(currentJob?.status) || @@ -6642,6 +6608,7 @@ export function createDashboardServer({ if (!shouldQueue) { archiveJob(currentJob); } + const jobId = randomUUID(); if (body.routineFileAccess !== undefined && typeof body.routineFileAccess !== "boolean") { sendJson(response, 400, { error: "routineFileAccess must be an explicit boolean consent." }); return; @@ -6674,7 +6641,6 @@ export function createDashboardServer({ workflow, permissionPreflight: body.permissionPreflight === true, toolApprovalMode, - postWriteApproval, fileAccess, inputTokenLimit, budgetExceeded: false, @@ -6729,12 +6695,6 @@ export function createDashboardServer({ partial: { stdout: "", stderr: "" } }; job.holdRequested = false; - if (postWriteApproval) { - pushLog(job, { - index: job.nextLogIndex, timestamp: new Date().toISOString(), stream: "dashboard", - message: "The user explicitly authorized configured POST writes for this job and selected environment without repeat confirmation. Approval is revision-bound, memory-only and expires with this executor; production and read-only restrictions remain in effect." - }); - } if (toolApprovalMode === "allow-all") { pushLog(job, { index: job.nextLogIndex, timestamp: new Date().toISOString(), stream: "dashboard", diff --git a/dashboard/workflow-policy.js b/dashboard/workflow-policy.js index b394d3e..9b004ad 100644 --- a/dashboard/workflow-policy.js +++ b/dashboard/workflow-policy.js @@ -16,13 +16,10 @@ export function configuredWorkflow(profile = {}) { } export const COPILOT_EXECUTION_GUIDANCE = - "Use normal Copilot reasoning and native tools for the shortest evidence-backed path. Stage markers report work; they are not a recipe requiring extra commands. Start at the symptom/nearest test; reuse verified context and adapt, not rediscover. Do routine authorized work without asking. Instructions, permissions, human authentication and data-safety controls remain authoritative."; + "Use normal Copilot reasoning and native tools to choose the shortest evidence-backed investigation, reproduction and fix. Stage markers report work; they are not a recipe requiring extra commands. Start with the known symptom and nearest relevant test, reuse verified context, and adapt to evidence instead of rediscovering the repository. Do not ask the user to direct routine authorized engineering steps. Required instructions, permissions, human authentication and data-safety controls remain authoritative."; export const RUNTIME_RECOVERY_GUIDANCE = - "Use reviewed tests/configured commands, not oversized inline scripts. Never replay, encode, conceal, split or repackage the rejected operation to evade review. Deny and request safe recovery reuses existing helpers; preserve evidence/failures or block. DEV/SIT/UAT writes use fixlab_nonproduction_post (legacy fixlab_dev_post) with dataSafety.post environment allowlists (legacy dataSafety.devPost). The off-by-default POST checkbox authorizes configured writes for this job without repeat confirmation. Approval binds the job, environment and profile/helper revision and expires on executor end, stop, hold or restart. Unchecked/recovered jobs require expiring single-use human confirmation; Allow all and batch approval never apply. Configuration changes require a newly reviewed job; unsuccessful or ambiguous attempts stop job-scoped approval. No credentials, bodies, redirects or retries. API-only read-only contracts stay unchanged; never substitute a shell/browser POST for a write. Report approved-write, not scenario success."; - -export const READ_ONLY_POST_GUIDANCE = - "Verified read-only POST searches: for browser live checks permitting real backend reads, require a reviewed query contract and implementation proving no data/workflow mutation. Match the exact approved environment, origin and path through the existing browser/auth guard, including redirects and the supported query payload. Route names and the job POST checkbox are not approval. Run permitted queries through existing authenticated helpers and use genuine returned records in focused assertions, even when writes are intercepted. A missing query allowlist entry is a policy blocker, not backend unavailability: inspect bounded controller/service evidence and request a narrowly scoped configuration review. Do not silently substitute synthetic data, disable guards, add wildcard/global POST grants or bypass with another client. Read-only jobs must not edit configuration; approved changes require a fresh reviewed context/job. Keep Save, Submit, Approve and autosave behind existing write controls. Do not override synthetic-only, API-only, unit-only or production policy. Authentication/configuration reads and synthetic UI evidence do not prove business-data integration."; + "Use reviewed tests/configured commands, not oversized inline scripts. Never replay, encode, conceal, split or repackage the rejected operation to evade review. Deny and request safe recovery reuses existing helpers; preserve evidence/failures or block. DEV/SIT/UAT jobs use fixlab_nonproduction_post (legacy fixlab_dev_post) with dataSafety.post environment allowlists (legacy dataSafety.devPost). The start checkbox enables requests; unchecked asks later. Each URL/payload needs expiring single-use human confirmation; Allow all and batch approval never apply. No credentials, bodies, redirects or retries. API-only read-only contracts stay unchanged; never substitute a shell/browser POST. Report approved-write, not scenario success."; export const UNIT_TEST_ONLY_GUIDANCE = "When validation.liveTestsRequired is false, this repository explicitly uses unit-test-only validation. This contract overrides live-test, browser and application-startup instructions: run the configured local unit tests and required offline checks, do not start applications, authenticate, send API requests or access remote databases. Mark local-stack and live-test skipped with validation.liveTestsDeferredReason; never mark live validation passed or claim integration coverage. Keep required local gates, PR approvals and CI/merge policies intact."; @@ -33,11 +30,8 @@ export const API_ONLY_PLAYWRIGHT_GUIDANCE = export const CONCISE_PR_GUIDANCE = "Keep PR descriptions concise, normally at most 120 words unless the repository template or material risk requires more. Preserve every required template heading/field. Use a brief problem/change summary, compact actual validation outcomes, and only material risks or blockers. Keep failed, skipped, pending and not-run checks explicit. Link evidence rather than pasting logs; omit repeated stage narration, full command transcripts and redundant file lists. Exclude token/cache counts, AI-credit totals and model-cost summaries from PR descriptions. Keep the FLAB: title short and specific."; -export const PR_LEARNING_GUIDANCE = - "Reusable PR learning: within the existing pr stage, only when source edits and PR publication are authorized, inspect task-relevant existing repository skills for verified, non-obvious, reusable knowledge from this change. Prefer to update a matching skill; otherwise create .github/skills//SKILL.md with YAML name and description; the name must match the lowercase-hyphenated topic directory. Capture when to use it, the verified solution, repository file/test references, exact working directories and validation commands, and pitfalls. Skills are for future developers and relevant agent tasks, not a diary of the current run. Include the skill in the same reviewed PR before committing/pushing; self-review its instructions and run applicable documentation/skill checks. In the concise PR body, link the added/updated skill or state no new reusable learning when none qualifies; do not create empty, speculative or duplicate skills. Use no separate roadmap stage. Read-only jobs must not write skills, commit or publish. Respect repository instruction governance and existing approval boundaries. Do not add allowed-tools or broaden permissions. Never include credentials, personal/customer data, private endpoints or raw transcripts."; - export const EXISTING_APPLICATION_GUIDANCE = - "Existing-application fast path: explicit deployed/browser-only checks use playwright-only mode and the approved existing application for the selected environment. Resolve targets from profile/browser configuration, not report URLs. Reuse profile-supported healthy apps/authorized browser connections; never copy browser state, take over unrelated browsers, bypass profile locks or treat state files as sign-in proof. Do not clone, restore, start another stack or run broad gates just to repeat a deployed UI scenario. Mark unnecessary local-stack work skipped, not passed. Start only journey prerequisites; unavailable deployments block, never silently change environment or target. Record target/version and exact assertions. A baseline/deployed result does not validate unpublished local changes or a PR; use the matching commit/app. Manual-localhost requirements remain."; + "Existing-application fast path: for explicit browser-only reproduction or deployed-result checks, use playwright-only mode and first check the approved existing application for the selected environment. Resolve its target from repository profile/browser configuration, not an arbitrary report URL. Reuse a healthy permitted application and an authorized validation browser connection when the repository supports it; never copy browser state, take over an unrelated browser, bypass a profile lock or treat a state file as proof of sign-in. Do not clone a checkout, restore dependencies, start another local stack, or run broad unit/build gates merely to repeat a deployed UI scenario. Mark unnecessary local-stack work skipped, not passed. Start only prerequisites actually required by that journey; if the requested existing deployment is unavailable, report it rather than silently changing environment or target. Record the tested target/version and exact assertions. A baseline or deployed result does not validate unpublished local changes or a PR: those require the matching source commit and correct application. Explicit manual-localhost requests retain their local requirements."; export function efficientWorkflowGuidance(workflow, { dashboard = true } = {}) { return `Saved efficient delivery policy (applies without repeated job/stage questions): diff --git a/docs/FixLab-Installation-Guide-Quickstart.docx b/docs/FixLab-Installation-Guide-Quickstart.docx deleted file mode 100644 index fc9b637..0000000 Binary files a/docs/FixLab-Installation-Guide-Quickstart.docx and /dev/null differ diff --git a/docs/README.md b/docs/README.md index 4b50e14..b651116 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,16 +1,14 @@ # FixLab documentation Use this index instead of treating every guide as an installation checklist. -The Markdown guides are the maintained source of truth. The downloadable Word -installation quickstart is a convenience copy; use the online guides for updates. +The Markdown guides are the maintained source of truth; the superseded +installation/smoke-test Word guide has been consolidated into them. ## Install and run your first job | Guide | What it covers | | --- | --- | -| [First installation](../installation.md) | Prerequisites, machine changes, and reviewed setup | -| [Detailed installation](installation.md) | Guided/manual setup, updates, and uninstall | -| [Word installation quickstart](FixLab-Installation-Guide-Quickstart.docx) | Downloadable fresh Windows setup and installation troubleshooting | +| [Installation](installation.md) | Prerequisites, guided/manual setup, updates, and uninstall | | [Copilot installation prompt](installation.md#copy-paste-installation-prompt) | One reviewed setup request to paste into Copilot | | [Visual Studio Code](installation.md#install-and-use-in-visual-studio-code) | CLI setup and discovery of the repository custom agent | | [Getting started](getting-started.md) | First validation request, dashboard controls, evidence, and recovery | diff --git a/docs/architecture.md b/docs/architecture.md index db9d18f..821237e 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -13,11 +13,10 @@ the runtime receives a rejection with fixed recovery feedback, not a permission grant or automatic replay. Existing stage evidence remains authoritative. Separately, `fixlab_nonproduction_post` mediates DEV/SIT/UAT writes through each environment's existing authentication helper and exact endpoint/test-record -allowlist. The off-by-default job-start checkbox authorizes configured writes -for this job without repeat confirmation. Approval binds the job, environment -and profile/helper revision and expires with execution; it is never recovered. -Unchecked jobs still require exact, expiring, single-use action confirmation. -Allow all and batch approval do not grant writes. This native dashboard capability +allowlist. The job-start checkbox enables requests; unchecked requests can ask +for one action later. The checkbox never grants business-write permission. Its +complete payload review is individually confirmed, single-use and expiring; +Allow all and batch approval do not apply. This native dashboard capability does not expand API-only/read-only or other-environment contracts. See the [security model](security.md#individually-confirmed-non-production-post) for configuration and the privileged-execution boundary. @@ -97,22 +96,6 @@ PR descriptions exclude token/cache counts, AI-credit totals and model-cost summaries. Repeated runtime token meters are omitted from activity logs; structured usage metrics, persistence and input-token limits remain active. -Reusable learning is part of the existing **PR** stage, not a separate roadmap -stage. Before committing an authorized change, the runner reviews task-relevant -repository skills and updates a matching skill or creates -`.github/skills//SKILL.md` only for verified, non-obvious, reusable -knowledge. The skill includes YAML name/description, when to use it, the -verified solution, file/test references, working directories, validation -commands and pitfalls. It is reviewed in the same PR, with applicable -documentation/skill checks; the concise PR body links it or records that no -new reusable learning was identified. Empty, speculative and duplicate skills -are not created. Read-only jobs cannot write skills; existing instruction -governance, approvals and permissions remain authoritative. Generated skills -must not add tool grants or contain credentials, personal/customer data, -private endpoints or raw transcripts. This versioned repository knowledge is -distinct from the local advisory metadata cache and does not automatically -promote learned rules or share consumer knowledge with other repositories. - `dashboard/workflow-policy.js` supplies product-wide defaults for new jobs: `workflow: { mode: "efficient", publication: "draft", unitTestsAfterPr: false, optionalBuilds: false }`. Repository Settings persists overrides once. The reviewed focused checkpoint @@ -197,11 +180,6 @@ model. The selected model and environment still affect speed and results. ### Dashboard or CLI -Browser jobs apply the verified read-only POST query policy by default, -independently of the write-request checkbox. Repository guards remain -authoritative; a missing query allowlist entry blocks the live read rather than -triggering synthetic fallback. See [verified query handling](commands.md#verified-read-only-post-searches). - `browserAutomation.mode` defaults to `browser` for existing profiles. Explicit `api` mode requires backend startup/loopback health, a Playwright package and test command, authentication intent and a read-only origin/method allowlist. diff --git a/docs/commands.md b/docs/commands.md index 9f6132e..bb9aa3a 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -26,8 +26,11 @@ Local-fix and PR claims still require the matching checkout and application. fixlab [--runtime ] ``` -Opens an interactive FixLab agent in the selected repository, retaining context -for onboarding, diagnosis, scoped changes, validation, and authorized PR work. +This is the shortest Agency-like entry point. FixLab opens its interactive +agent in the selected repository and retains conversational context while it +inspects or helps onboard the repository, accepts a bug or enhancement, +diagnoses the affected behavior, implements the smallest fix, validates it, +and prepares the evidence-backed pull-request outcome. Use `fixlab run -- ` when the complete request is already known and should execute without first entering the interactive conversation. @@ -376,33 +379,6 @@ reported browser evidence fails validation; it cannot silently broaden the selected mode. Interception is implemented by repository-owned browser automation, not a blanket network proxy installed by the dashboard. -### Verified read-only POST searches - -The default browser workflow treats a verified, approved POST query as a read, -independently of **Allow POST requests**. Use genuine returned records when the -profile permits backend reads, including when actual writes are intercepted. -Verification must cover the query implementation, request shape, exact -environment/origin/path, redirects, and the repository guard's existing allowlist. -A name such as "search" is not sufficient. - -For writes, the separate **Allow POST writes for this job** checkbox is off by -default. Selecting it authorizes configured DEV/SIT/UAT endpoint/test-record -writes without repeat confirmation in a native Copilot dashboard job. Unchecked -or recovered jobs still confirm each action. Approval is job-, environment- and -revision-bound; it expires on stop, hold, completion or restart. No browser guard -is disabled and no production, API-only or explicit read-only restriction is -waived. See [POST configuration and approval](security.md#individually-confirmed-non-production-post). - -A missing allowlist entry is a policy blocker, not an unavailable backend. -Request a narrow configuration review; do not disable the guard, allow all POSTs, -use another client to bypass it, or silently substitute synthetic records. -Read-only jobs cannot edit configuration; approved changes need a fresh reviewed -context/job. Save, Submit, Approve and autosave retain their write controls. -Synthetic-only, API-only, unit-only and production restrictions are unchanged. - -This is workflow guidance, not a global network permission: the repository-owned -guard still enforces the approved scope. - ### Background runtime permissions Direct `--runtime copilot` dashboard jobs display structured runtime requests diff --git a/docs/installation.md b/docs/installation.md index 5fe01cc..80752bf 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -1,22 +1,14 @@ # Installation -New users: start with [First installation](../installation.md) for prerequisites -and an explanation of what is added to the machine. This guide covers detailed -setup for Agency Copilot and direct GitHub Copilot CLI execution. +FixLab supports Agency Copilot and direct GitHub Copilot CLI execution. Install +and authenticate the runtime selected for the local runner before installing +FixLab. For known CLI, package-manager, private-feed, Playwright, authentication, and runtime failures, see [Troubleshooting](troubleshooting.md). -The [Word installation quickstart](FixLab-Installation-Guide-Quickstart.docx) -is a downloadable convenience copy for fresh Windows setup and common -installation problems. These Markdown instructions remain the maintained -source of truth for updates. - Choose the setup path you need: -- [Fresh Windows dev box](#fresh-windows-dev-box) -- [What installation changes](#what-installation-changes) -- [Enterprise-managed GitHub accounts](#enterprise-managed-github-accounts) - [Guided CLI setup](#recommended-path-install-then-run) - [Copy-paste Copilot installation prompt](#copy-paste-installation-prompt) - [Visual Studio Code custom agent](#install-and-use-in-visual-studio-code) @@ -28,25 +20,10 @@ After setup, use [Getting started](getting-started.md) for your first job. ## Recommended path: install, then run -Once your supported runtime is installed and signed in: +Once your supported runtime is installed and signed in, use just: ```powershell npm install --global github:MonaDevAI/FixLab -fixlab onboard C:\source\application --runtime copilot -``` - -If a new example profile is created, configure the actual application paths and -commands, then rerun `onboard` to inspect its setup plans. After reviewing and -approving those plans: - -```powershell -fixlab onboard C:\source\application --runtime copilot --yes -fixlab doctor C:\source\application --runtime copilot -``` - -Resolve every Doctor blocker, then start chat: - -```powershell fixlab chat C:\source\application --runtime copilot ``` @@ -57,8 +34,23 @@ scoped restore/browser setup, and complete any required feed sign-in and application MFA yourself. FixLab checks readiness before starting the dashboard. If setup fails, it reports the prerequisite rather than starting a broken job. -The remaining sections are alternatives, not a checklist. A PR-only feature -needs its approved commit installed until it reaches the default branch. +The commands below are manual setup and troubleshooting alternatives, not a +checklist that every user must run. A PR-only feature needs its approved commit +installed until the change reaches the default branch. + +## Use the dashboard + +Describe your request and select **Start job**. Attach images when useful; +**Run options** holds mode, environment and evidence settings. + +The sidebar opens saved work, run details, connections and settings on demand. +On a small screen, select **Menu** first. **Back to chat** hides those tools +without clearing your draft or running a job. A repository that needs setup +shows **Set up a repository**; Azure DevOps intake is optional and separate. + +Approval requests and errors remain inline. Review the actual command, path +or diff before approving; enter sign-in details only in the normal browser. +Required checks and managed approvals are unchanged. ## Prerequisites @@ -90,139 +82,11 @@ For direct mode, install and authenticate GitHub Copilot CLI, then verify `copilot --help`. Select direct mode with `--runtime copilot` or `FIXLAB_RUNTIME=copilot`. -FixLab does not install Agency or the standalone Copilot CLI, or manage sign-in. -Run `fixlab chat` after installation for guided onboarding and a dashboard -connection once the profile is ready. - -### Fresh Windows dev box - -Install only missing tools using your organization's approved software source. -First check any repository-pinned Node.js version; do not replace a working -NVM-managed runtime with another installation. When WinGet is permitted, these -commands install common prerequisites: - -```powershell -winget install --id Git.Git --exact -winget install --id OpenJS.NodeJS.LTS --exact -winget install --id Microsoft.PowerShell --exact -winget install --id GitHub.Copilot --exact -``` - -Node.js includes npm; a separate npm installation is not normally needed. -Choose a Node release compatible with FixLab and the application rather than -assuming that the newest LTS meets the application's version pin. Follow -installer elevation prompts and organization policy; an Administrator terminal -is not a universal prerequisite for these commands. - -Close and reopen the terminal after installation so it sees the updated `PATH`. -Open PowerShell 7 (`pwsh`), then verify the tools in that same shell: - -```powershell -git --version -node --version -npm --version -pwsh --version -copilot --version -``` - -For direct Copilot mode, run `copilot` and complete the normal login flow with -the identity authorized by your organization. Copilot access and CLI enablement -must already be permitted by your enterprise. Follow the -[official Copilot CLI installation guide](https://docs.github.com/en/copilot/how-tos/copilot-cli/set-up-copilot-cli/install-copilot-cli) -for current supported installers and authentication steps. For Agency mode, -use the organization's Agency setup instead of installing an unnecessary -second runtime. - -Install the [.NET SDK](https://learn.microsoft.com/en-us/dotnet/core/install/windows) -required by the application, not just the .NET runtime. Check `global.json` -and the projects' `TargetFramework` or `TargetFrameworks`; .NET 9 is not a -universal FixLab requirement. Verify SDK resolution from the application root: - -```powershell -Set-Location C:\source\application -dotnet --list-sdks -dotnet --version -``` - -After installing FixLab, configure the real checkout layout before restoring -dependencies or installing Playwright. `fixlab init` copies an example profile; -it does not create an application, install its dependencies, or discover its -actual frontend directory. Edit these fields in -`.github\fixlab\repository-profile.json` as applicable: - -| Field | Set it to | -| --- | --- | -| `applications.frontend.workingDirectory` | The existing frontend package directory, relative to the repository root | -| `applications.backend.workingDirectory` | The existing backend project directory | -| `browserAutomation.workingDirectory` | The existing package/test directory that owns the Playwright setup | -| `validation.commands` and `validation.gates[].workingDirectory` | The repository's actual commands and their working directories | -| `browserAutomation.testCommand` | The repository-owned browser or API test command | - -Do not create an empty `frontend` folder or run a generic Playwright project -generator to make an example path pass. If Playwright is missing after the -paths are corrected, review `fixlab setup-playwright` before approving -`fixlab setup-playwright --yes`. This installs into the configured checkout, -not globally. Restore existing dependencies with the reviewed `fixlab prepare` -plan and complete any required private-feed sign-in first. - -Browser-mode tests need the configured Playwright browser or approved channel -(for example, Edge), not an arbitrary extra Chromium installation. API-only -profiles use `browserAutomation.mode: "api"` and do not install or launch a -browser. An explicit unit-test-only profile skips Playwright; do not disable -required live coverage merely to get a green Doctor result. - -For an example-path error, see -[Playwright working directory is missing](troubleshooting.md#playwright-working-directory-is-missing). - -### Enterprise-managed GitHub accounts - -Some enterprise-managed accounts cannot access or work with repositories -outside their enterprise. A Copilot subscription does not override those -source-access or installation policies. Do not switch to a personal account -or bypass an organization restriction to install FixLab. - -Ask the enterprise administrator or an approved maintainer for an authorized -internal mirror or a reviewed FixLab npm tarball produced with `npm pack`. -After verifying its approved source, version and integrity, install the supplied -artifact, for example: - -```powershell -npm install --global "C:\approved-packages\fixlab-cli-.tgz" -fixlab --help -``` - -Use the actual supplied filename in place of the example. FixLab is not -published to the public npm registry; `npm install --global @fixlab/cli` is -not an available distribution path. An internal mirror or registry must be -provisioned and approved by the organization, not assumed to exist. - -A tarball is not a fully offline installer: npm still resolves FixLab's -dependencies unless an approved package source supplies them. Copilot -installation/sign-in, plugin loading, application feeds and Playwright -downloads have their own network and policy requirements. Use the approved -sources and normal authentication for each; do not embed credentials in -installation commands, profiles or redistributed packages. - -## What installation changes - -| Action | Machine or repository changes | -| --- | --- | -| `npm install --global github:MonaDevAI/FixLab` | Installs FixLab, its dashboard/plugin/templates/docs, npm dependencies, and `fixlab` / `fixlab-mcp` launchers in the global npm prefix | -| SDK dependency | Includes the Copilot SDK's bundled dashboard runtime, not a standalone authenticated Copilot CLI or Agency installation | -| `fixlab init` | Adds missing profile, prompts, and custom agent under the application's `.github`; preserves existing files | -| `fixlab prepare --yes` | Executes configured restore commands; review the plan for their actual effects | -| `fixlab setup-playwright --yes` | Adds a missing project-local test dependency and installs the configured browser/channel; API-only mode installs no browser | -| `fixlab chat` / `fixlab dashboard` | Starts or connects to a local dashboard; jobs can create worktrees, evidence, and Git-private state | - -Use `npm prefix --global` and `npm root --global` to locate the installation. -Installing FixLab alone does not install Git, Node.js, PowerShell, .NET, Agency, -the standalone Copilot CLI, application dependencies, or Playwright browsers. -It does not start an operating-system service or dashboard, register MCP, or -change application files. Optional setup commands require their own review. - -Playwright browser binaries normally use a user cache; branded browser/channel -installers can change the machine's browser installation. Installing tools does -not grant authentication, write, deployment, or publication permission. +FixLab does not install either runtime or manage its authentication. +Installing FixLab does not start a server or background service. Run +`fixlab chat` after installation; if no dashboard is running, chat asks for +the repository path, opens interactive onboarding when needed, and starts the +local dashboard in the background after the profile is ready. ## Install directly from GitHub @@ -685,17 +549,106 @@ Edit: .github\fixlab\repository-profile.json ``` -Open the file in your editor: +Open the file in the developer's normal editor. For example: ```powershell code .github\fixlab\repository-profile.json ``` -Use the [current profile template](../templates/repository-profile.json), not a -second copy of its JSON. Replace example component paths, restore/validation -commands, startup commands, ports, environments, PR target, and developer alias. -Paths are repository-relative; commands must run non-interactively. -If browser authentication is required, set +If Visual Studio Code is unavailable: + +```powershell +notepad .github\fixlab\repository-profile.json +``` + +Replace the template values with paths and commands owned by the application +repository. A React and .NET profile normally defines: + +```json +{ + "version": 1, + "name": "Application React and .NET", + "architecture": "React frontend backed by an ASP.NET API.", + "components": [ + { + "name": "React frontend", + "paths": ["frontend/src", "frontend/e2e"], + "testPatterns": [ + "frontend/src/**/*.test.ts", + "frontend/src/**/*.test.tsx", + "frontend/e2e/**/*.spec.ts" + ] + }, + { + "name": ".NET API", + "paths": ["backend/src"], + "testPatterns": ["backend/tests/**/*.cs"] + } + ], + "validation": { + "commands": { + "frontendRestore": "npm ci", + "frontendTest": "npm test -- --runInBand", + "frontendTypeCheck": "npm run type-check", + "frontendBuild": "npm run build", + "backendRestore": "dotnet restore Application.sln", + "backendTest": "dotnet test Application.sln", + "backendBuild": "dotnet build Application.sln" + }, + "productionBuildTimeoutMinutes": 20, + "blockerPolicy": "continue-on-owned-process-timeout" + }, + "applications": { + "frontend": { + "workingDirectory": "frontend", + "command": "npm start", + "port": 3000, + "healthUrl": "http://127.0.0.1:3000" + }, + "backend": { + "workingDirectory": "backend", + "command": "", + "port": 0, + "healthUrl": "", + "requiredForLiveTest": false + } + }, + "browserAutomation": { + "workingDirectory": "frontend", + "package": "@playwright/test", + "browser": "chromium", + "testCommand": "npm run test:e2e", + "testSynthesis": { + "enabled": true, + "defaultDataSource": "synthetic-intercepted", + "mutationMode": "intercepted", + "requireScenarioEvidence": true + }, + "authentication": { + "required": false, + "command": "", + "statusPaths": [] + }, + "dataSafety": { + "policy": "Use local test data and intercept every mutating request.", + "productionAllowed": false + } + }, + "environments": ["local", "dev", "sit"], + "pullRequests": { + "defaultTargetBranch": "main", + "requireConfirmation": true, + "branchNaming": { + "userId": "developer-alias", + "prefixTemplate": "users/{userId}" + } + } +} +``` + +Use repository-relative paths and commands that run non-interactively. Replace +the example solution, directories, startup command, test command, environments, +target branch, and developer alias. If browser authentication is required, set `authentication.required` to `true` and configure the repository-owned login command and status paths as described below. diff --git a/docs/security.md b/docs/security.md index 05f43f4..64d8271 100644 --- a/docs/security.md +++ b/docs/security.md @@ -153,19 +153,12 @@ checks remain authoritative; a missing safe prerequisite still blocks the stage. ### Individually confirmed non-production POST Native Copilot dashboard jobs expose `fixlab_nonproduction_post` for DEV, SIT and -UAT. The **Allow POST writes for this job in the selected environment** checkbox -is off by default. Selecting it and starting the job explicitly authorizes writes -to that environment's configured endpoints and test records without repeated -payload confirmations. This is business-write approval, not just tool access. -Unchecked jobs require separate exact-action confirmation when POST is needed. -Only the native Copilot dashboard supports job-scoped write approval. -Approval is bound to the exact job, environment and profile/helper revision. -It is memory-only and expires on executor end, stop, hold, completion or restart. -Recovered jobs and saved legacy POST preferences never inherit it. Fresh queued -jobs retain only their explicit choice while the current dashboard remains alive. -Changing configuration requires a newly reviewed job. Missing configuration -blocks the action; adding it later does not expand the original approval. -The browser/auth guard is not removed or bypassed. +UAT. The **Allow POST requests in the selected environment** start checkbox is +selected by default for those environments, unless an explicit read-only or +intercepted mode was selected. Unchecking it defers the exact-action request +until POST is needed; it never waives later confirmation. The checkbox is a +capability preference, not business-write approval. Ordinary validation retains +its default read-only/intercepted behavior until an exact action is approved. API-only, explicit read-only/intercepted, unit-test-only, production-backed and other-environment contracts are unchanged. Agency and interactive CLI sessions do not expose this tool and must block such @@ -203,25 +196,20 @@ Add separately approved `sit` and `uat` entries with their own destinations, test records and authentication configuration; no environment inherits another's allowlist. The profile must also declare the selected environment. Legacy `dataSafety.devPost` and `fixlab_dev_post` remain supported for DEV. Old snapshots -do not gain job-scoped write approval automatically; they retain per-action -confirmation. +do not gain POST capability automatically, and their existing approved-write +requirements are preserved. An endpoint being labeled non-production in configuration is an operator assertion, not proof of its deployment identity. Review the real destination and test data. The helper must already exist and use legitimate authentication; the tool never collects credentials or starts interactive sign-in. Credentials stay in memory. Credential-sensitive debug logging must be disabled. -Without current job-scoped approval, every exact URL and complete JSON payload -receives separate human review, including while Allow all is active. These -requests are excluded from batch approval and expire after five minutes. -With checkbox approval, payloads still require the exact configured test-record -field/value, credential screening and size limits. Review the endpoint contract: -it must scope the operation to that test record; the marker alone cannot prove -the business meaning of other payload fields. -Profile/helper revisions are rechecked before dispatch in both modes. -Redirects are not followed and failures are not retried. Any unsuccessful or -ambiguous attempt stops job-scoped write approval; inspect its effects and start -a newly reviewed job before another write. Response bodies +Every exact URL and complete JSON payload receives a separate human review, +including while Allow all is active. These requests are excluded from batch +approval and expire after five minutes. Profile/helper revisions are checked +again before dispatch. One approval permits one attempt: redirects are not +followed, ambiguous outcomes and failures are not retried, and repeating an +action requires fresh confirmation after inspecting its effects. Response bodies and credentials never enter results or job history; only status metadata returns. An HTTP success is not a passed scenario assertion. Existing arbitrary tool execution remains privileged: this constrained helper is not an OS network diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index dab2993..5b3f63e 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -69,74 +69,6 @@ After changing `PATH` or npm's prefix, open a new PowerShell window and run: fixlab --help ``` -## Playwright working directory is missing - -If Doctor reports both `Playwright package` and `Playwright browser` with -`working directory is missing: ...\frontend`, the profile points to a directory -that does not exist. Installing Playwright elsewhere does not repair that path. -`frontend` and `backend` in the generated profile are examples, not required -folder names. - -Open `.github\fixlab\repository-profile.json` in the application checkout. -Set `browserAutomation.workingDirectory` to the actual existing application or -test package directory, relative to the repository root. Also correct -`applications.frontend.workingDirectory`, used as its fallback, and any restore -or gate working directories that still use the example layout. For a frontend -at the repository root, the working directory is `"."`; for a nested app, use -its real relative path. - -Do not create an empty `frontend` folder or generate an unrelated Playwright -project to satisfy the check. If you already created one, do not install into -it just because the original missing-folder error disappeared. Fix the profile, -check for any files or work before deciding what to keep, and use the real test -package. FixLab does not delete the folder for you. - -Then review setup from the application repository root: - -```powershell -fixlab prepare -fixlab setup-playwright -``` - -Approve only the necessary reviewed commands with `--yes`, then rerun -`fixlab doctor --runtime copilot` (or `agency` for that runtime). -If Doctor now reports a missing package, install into the corrected configured -directory, not globally. A browser launch failure is a separate issue: install -the configured browser/channel. API-only profiles need the Playwright package -and request context, but no browser. - -## Repeated command windows flash after launching - -Repeated console windows are not expected. A visible browser for interactive -sign-in or MFA is different and must not be hidden or bypassed. - -If the flashing window is `git.exe` and appears about once a second while -`fixlab chat` is idle, check that the installed build includes the hidden Git -probe fix. Chat polls dashboard status every second; artifact-root and cache -inspection use background Git queries. Older builds did not hide those two -helpers, allowing short-lived Git terminals to steal focus on Windows. -The fix sets `windowsHide: true` for both helpers without disabling polling, -Git checks, or approvals. Reinstalling Node, .NET or Playwright does not fix -this window-creation bug. - -Hold any active job before changing the setup. In the shell used to start -FixLab, collect `where.exe fixlab`, `npm list --global @fixlab/cli --depth=0`, -`node --version`, `npm --version`, `pwsh --version`, and the selected runtime's -version. Check whether an older global installation or another dashboard is -being launched. Do not kill all Node.js, PowerShell, or browser processes. - -When no existing dashboard/job is using the intended port, start one foreground -instance with `fixlab dashboard C:\source\application --no-open` and preserve -the first terminal error. Note whether flashing starts while idle, only when -starting a job, or during authentication. Include the Windows version, terminal -host, FixLab/runtime versions, and a short recording when reporting the issue; -redact tokens, browser state and private application data. - -FixLab's owned Windows executor uses a hidden inherited console. That is not -proof that every tool, terminal host or older installation behaves identically. -Do not repeatedly reinstall prerequisites or disable permission controls as a -workaround for an undiagnosed flashing loop. - ## npm fails with an internal JavaScript error An error such as: diff --git a/e2e/dashboard-readiness.spec.js b/e2e/dashboard-readiness.spec.js index fe205fd..86af2ce 100644 --- a/e2e/dashboard-readiness.spec.js +++ b/e2e/dashboard-readiness.spec.js @@ -246,7 +246,7 @@ test("dashboard submits per-run interception without optional folder preflight", await expect(page.locator("#job-test-mutation-mode")).toHaveValue("none"); }); -test("POST checkbox explicitly approves this job only and keeps unchecked writes on confirmation", async ({ page }) => { +test("POST checkbox supports DEV/SIT/UAT and keeps unchecked requests on confirmation", async ({ page }) => { const posts = []; await mockChatWorkspace(page, { environments: ["dev", "sit", "uat"] }); await page.route("**/api/jobs", async (route) => { @@ -262,8 +262,7 @@ test("POST checkbox explicitly approves this job only and keeps unchecked writes for (const environment of ["dev", "sit", "uat"]) { await page.locator("#target-environment").selectOption(environment); await expect(checkbox).toBeEnabled(); - await expect(checkbox).not.toBeChecked(); - await checkbox.check(); + await expect(checkbox).toBeChecked(); } await checkbox.uncheck(); await page.waitForResponse("**/api/status"); @@ -275,16 +274,6 @@ test("POST checkbox explicitly approves this job only and keeps unchecked writes await expect.poll(() => posts.length).toBe(1); expect(posts[0].targetEnvironment).toBe("uat"); expect(posts[0].allowPostRequests).toBe(false); - expect(posts[0].postWriteApprovalConfirmed).toBe(false); - await expect(checkbox).not.toBeChecked(); - await page.waitForResponse("**/api/status"); - await page.locator("#request").fill("Synthetic explicitly approved POST writes."); - await checkbox.check(); - await page.locator("#start-button").click(); - await expect.poll(() => posts.length).toBe(2); - expect(posts[1].allowPostRequests).toBe(true); - expect(posts[1].postWriteApprovalConfirmed).toBe(true); - await expect(checkbox).not.toBeChecked(); await page.locator("#job-test-mutation-mode").selectOption("none"); await expect(checkbox).toBeDisabled(); await expect(checkbox).not.toBeChecked(); diff --git a/installation.md b/installation.md deleted file mode 100644 index 16bb852..0000000 --- a/installation.md +++ /dev/null @@ -1,87 +0,0 @@ -# Install FixLab - -Start here for a first installation. Use the -[detailed installation guide](docs/installation.md) for manual setup, -enterprise distribution, updates, and troubleshooting. - -## 1. Prerequisites: install separately - -FixLab does not install these tools or provide their licenses or sign-in. - -| Prerequisite | Verify in PowerShell 7 | -| --- | --- | -| Git | `git --version` | -| Node.js 20.19+ (20.x), or 22.12+; npm is included | `node --version` and `npm --version` | -| PowerShell 7 | `pwsh --version` | -| GitHub Copilot CLI or Agency, with authorized access | `copilot --version` or `agency --version`; complete the runtime's normal sign-in | -| .NET SDK, not just the runtime | From the application root: `dotnet --version`; match `global.json` and the target framework | -| Application checkout and access to its approved package feeds/environments | Confirm access before setup | - -Respect the application's Node version pin. Doctor currently checks the .NET -SDK for every profile; .NET 9 is not a universal requirement. -Missing tools? Follow the -[fresh Windows checklist](docs/installation.md#fresh-windows-dev-box), then -reopen the terminal. - -## 2. Install FixLab - -If public GitHub access is restricted, use an -[organization-approved package](docs/installation.md#enterprise-managed-github-accounts) -instead; do not bypass enterprise policy. Otherwise: - -```powershell -npm install --global github:MonaDevAI/FixLab -fixlab --help -``` - -### What this adds to the machine - -- The FixLab CLI, dashboard assets, plugin, templates, and documentation. -- npm dependencies, including the Copilot SDK and its bundled dashboard runtime. -- The `fixlab` and `fixlab-mcp` command launchers in the global npm prefix. - -Check the installation locations with `npm prefix --global` and -`npm root --global`. This does **not** install the standalone Copilot CLI or -Agency, start a service/dashboard, register MCP, or change application files. - -## 3. Set up an application - -```powershell -fixlab onboard C:\source\application --runtime copilot -``` - -Replace the path with the actual checkout; use `--runtime agency` for Agency. -Set any new example profile to the real application paths and commands, then -rerun `onboard` to review the setup plans. After approving those plans: - -```powershell -fixlab onboard C:\source\application --runtime copilot --yes -fixlab doctor C:\source\application --runtime copilot -``` - -Resolve Doctor blockers before starting chat: - -```powershell -fixlab chat C:\source\application --runtime copilot -``` - -| Later action | What it changes | -| --- | --- | -| `fixlab init` | Adds missing profile, agent, and prompts under `.github`; preserves existing files | -| `fixlab prepare --yes` | Runs the profile's restore commands; review their working directories and effects first | -| `fixlab setup-playwright --yes` | Adds a missing test package to the configured project and installs the configured browser/channel when needed | -| Ready `fixlab chat` / `fixlab dashboard` | Starts a local loopback dashboard; running jobs can create worktrees, evidence, and Git-private metadata | - -Playwright browsers normally use a user cache; branded browser installers can -change the machine's browser installation. API-only setup installs no browser; -explicit unit-only validation does not require Playwright. - -Set `.github\fixlab\repository-profile.json` to real application/test paths. -Do not create empty `frontend` or `backend` folders to satisfy example paths. -Run `fixlab doctor --runtime copilot` after configuration and resolve blockers. -Complete sign-in and MFA yourself; installation does not approve data writes, -deployment, or PR publication. - -Continue with [your first job](docs/getting-started.md), use -[troubleshooting](docs/troubleshooting.md), or download the -[Word quickstart](docs/FixLab-Installation-Guide-Quickstart.docx). diff --git a/package.json b/package.json index 840441d..2fc95e5 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,6 @@ "dashboard", "docs", "examples", - "installation.md", "LICENSE", "mcp", "plugin.json", diff --git a/prompts/fixlab.live-test.prompt.md b/prompts/fixlab.live-test.prompt.md index bbee800..e478de9 100644 --- a/prompts/fixlab.live-test.prompt.md +++ b/prompts/fixlab.live-test.prompt.md @@ -10,22 +10,6 @@ Playwright journey on the developer or registered-runner machine. Capture sanitized screenshots, API assertions, logs, cleanup, and the exact observable result. -Verified read-only POST searches: for browser live checks permitting real backend -reads, require a reviewed query contract and implementation proving no -data/workflow mutation. Match the exact approved environment, origin and path -through the existing browser/auth guard, including redirects and the supported -query payload. Route names and the job POST checkbox are not approval. Run -permitted queries through existing authenticated helpers and use genuine returned -records in focused assertions, even when writes are intercepted. A missing query -allowlist entry is a policy blocker, not backend unavailability: inspect bounded -controller/service evidence and request a narrowly scoped configuration review. -Do not silently substitute synthetic data, disable guards, add wildcard/global -POST grants or bypass with another client. Read-only jobs must not edit -configuration; approved changes require a fresh reviewed context/job. Keep Save, -Submit, Approve and autosave behind existing write controls. Do not override -synthetic-only, API-only, unit-only or production policy. Authentication/configuration -reads and synthetic UI evidence do not prove business-data integration. - For `browserAutomation.mode: "api"`, use `APIRequestContext` without a frontend, browser binary, screenshot or video. Enforce the profile's approved origins, read-only methods and redirects before sending requests; browser interception diff --git a/prompts/fixlab.pr.prompt.md b/prompts/fixlab.pr.prompt.md index 466ced9..1a6175b 100644 --- a/prompts/fixlab.pr.prompt.md +++ b/prompts/fixlab.pr.prompt.md @@ -12,23 +12,6 @@ completed and running checks, browser evidence, skipped gates, and remaining risks. Never mark the PR ready or complete the PR stage until every required local gate passes or repository policy records an explicitly accepted outcome. -Reusable PR learning: within the existing pr stage, only when source edits and -PR publication are authorized, inspect task-relevant existing repository skills -for verified, non-obvious, reusable knowledge from this change. Prefer to update -a matching skill; otherwise create `.github/skills//SKILL.md` with YAML name -and description; the name must match the lowercase-hyphenated topic directory. -Capture when to use it, the verified solution, repository file/test references, -exact working directories and validation commands, and pitfalls. Skills are for -future developers and relevant agent tasks, not a diary of the current run. -Include the skill in the same reviewed PR before committing/pushing; self-review -its instructions and run applicable documentation/skill checks. In the concise -PR body, link the added/updated skill or state no new reusable learning when none -qualifies; do not create empty, speculative or duplicate skills. Use no separate -roadmap stage. Read-only jobs must not write skills, commit or publish. Respect -repository instruction governance and existing approval boundaries. Do not add -allowed-tools or broaden permissions. Never include credentials, personal/customer -data, private endpoints or raw transcripts. - Keep the description short, normally at most 120 words unless required template fields or material risks need more. Preserve required headings, a brief problem/change summary, compact actual validation outcomes and material diff --git a/scripts/ci/check-docs.mjs b/scripts/ci/check-docs.mjs index 1637188..2251d4c 100644 --- a/scripts/ci/check-docs.mjs +++ b/scripts/ci/check-docs.mjs @@ -10,7 +10,6 @@ import { fileURLToPath } from "node:url"; const requiredMarkdownFiles = [ "README.md", - "installation.md", "CONTRIBUTING.md", "SECURITY.md", "docs/architecture.md", diff --git a/scripts/ci/format.mjs b/scripts/ci/format.mjs index 5e9bdbd..0879a0a 100644 --- a/scripts/ci/format.mjs +++ b/scripts/ci/format.mjs @@ -31,7 +31,6 @@ const rootFiles = [ "AGENTS.md", "CHANGELOG.md", "CONTRIBUTING.md", - "installation.md", "README.md", "SECURITY.md", "agency.json", diff --git a/templates/fixlab-autofix.agent.md b/templates/fixlab-autofix.agent.md index 14543f7..a870ae8 100644 --- a/templates/fixlab-autofix.agent.md +++ b/templates/fixlab-autofix.agent.md @@ -33,41 +33,21 @@ repackage the rejected operation to evade review. **Deny and request safe recove reuses existing reviewed tests/helpers in the same blocked stage; preserve completed evidence and failures, and stop if a safe prerequisite is missing. -For authorized DEV/SIT/UAT data-changing POST actions, discover +For individually authorized DEV/SIT/UAT POST actions, discover `fixlab_nonproduction_post` in a native Copilot dashboard job. The **Allow POST -writes** checkbox is off by default. Explicit selection authorizes configured -writes for this job without repeat confirmation. Unchecked or recovered jobs -require expiring, single-use human confirmation of each exact URL and complete -payload. Allow all and batch approval never apply. Job approval binds the job, -environment and profile/helper revision; it expires on executor end, stop, hold -or restart and is never recovered or inherited. Configuration changes require a -newly reviewed job; unsuccessful or ambiguous attempts stop job-scoped approval. -`dataSafety.post.dev`, `.sit` and `.uat` hold separate +requests** start checkbox enables requests; unchecked, ask when an action is needed. +It never approves writes. `dataSafety.post.dev`, `.sit` and `.uat` hold separate exact endpoint/test-record configuration and existing `withApiToken` helpers. Legacy DEV `fixlab_dev_post` and `dataSafety.devPost` remain supported. -No credentials or response body enter +Each exact URL and complete +synthetic/approved test payload requires expiring, single-use human confirmation; +Allow all and batch approval never apply. No credentials or response body enter the result. Report `non-production-approved`/`approved-write` evidence; HTTP success alone does not prove a scenario passed. No automatic retries or redirects: an ambiguous result requires human inspection before any fresh action. Other environments, explicit read-only/intercepted selections, API-only read-only contracts, unit-only validation and human sign-in remain unchanged. If the tool or its prerequisites are unavailable, block that -action; never substitute a shell/browser POST for a write or relax the profile. - -Verified read-only POST searches: for browser live checks permitting real backend -reads, require a reviewed query contract and implementation proving no -data/workflow mutation. Match the exact approved environment, origin and path -through the existing browser/auth guard, including redirects and the supported -query payload. Route names and the job POST checkbox are not approval. Run -permitted queries through existing authenticated helpers and use genuine returned -records in focused assertions, even when writes are intercepted. A missing query -allowlist entry is a policy blocker, not backend unavailability: inspect bounded -controller/service evidence and request a narrowly scoped configuration review. -Do not silently substitute synthetic data, disable guards, add wildcard/global -POST grants or bypass with another client. Read-only jobs must not edit -configuration; approved changes require a fresh reviewed context/job. Keep Save, -Submit, Approve and autosave behind existing write controls. Do not override -synthetic-only, API-only, unit-only or production policy. Authentication/configuration -reads and synthetic UI evidence do not prove business-data integration. +action; never substitute a shell/browser POST or relax the profile. When `validation.liveTestsRequired` is `false`, follow unit-test-only validation: run configured local unit tests and required offline checks, without application @@ -212,23 +192,6 @@ push later reviewed checkpoints to the same branch, and never mark the PR ready until every required local gate passes or the developer explicitly accepts the recorded outcome. -Reusable PR learning: within the existing pr stage, only when source edits and -PR publication are authorized, inspect task-relevant existing repository skills -for verified, non-obvious, reusable knowledge from this change. Prefer to update -a matching skill; otherwise create `.github/skills//SKILL.md` with YAML name -and description; the name must match the lowercase-hyphenated topic directory. -Capture when to use it, the verified solution, repository file/test references, -exact working directories and validation commands, and pitfalls. Skills are for -future developers and relevant agent tasks, not a diary of the current run. -Include the skill in the same reviewed PR before committing/pushing; self-review -its instructions and run applicable documentation/skill checks. In the concise -PR body, link the added/updated skill or state no new reusable learning when none -qualifies; do not create empty, speculative or duplicate skills. Use no separate -roadmap stage. Read-only jobs must not write skills, commit or publish. Respect -repository instruction governance and existing approval boundaries. Do not add -allowed-tools or broaden permissions. Never include credentials, personal/customer -data, private endpoints or raw transcripts. - Honor an explicit publication choice in every mode: strict plus draft stays draft, with provider `isDraft: true` verified. Legacy strict policy with no publication preference retains ready intent. Strict required checks never change. @@ -313,4 +276,3 @@ wait for the owned validation to finish before cleanup. access fails or no safe records can exercise the behavior. Preserve the limitation, do not replace an expected empty state, and report that a synthetic pass proves UI behavior only. - Guard/query-allowlist rejections require configuration review, not fallback. diff --git a/test/agent-rules.test.js b/test/agent-rules.test.js index 91d584b..622eb8d 100644 --- a/test/agent-rules.test.js +++ b/test/agent-rules.test.js @@ -13,7 +13,7 @@ import { const testDirectory = dirname(fileURLToPath(import.meta.url)); -test("safe recovery and scoped DEV/SIT/UAT POST guidance stay aligned across execution surfaces", () => { +test("safe recovery and per-action DEV/SIT/UAT POST guidance stay aligned across execution surfaces", () => { const root = join(testDirectory, ".."); for (const entryPoint of ["dashboard/server.js", "bin/fixlab.js"]) { assert.match(readFileSync(join(root, entryPoint), "utf8"), /RUNTIME_RECOVERY_GUIDANCE/u); @@ -27,10 +27,8 @@ test("safe recovery and scoped DEV/SIT/UAT POST guidance stay aligned across exe /oversized inline scripts/u, /repackage the rejected operation to evade review/u, /Deny and request safe recovery/u, /fixlab_dev_post/u, /dataSafety\.devPost/u, /DEV\/SIT\/UAT/u, /fixlab_nonproduction_post/u, /dataSafety\.post/u, - /checkbox/u, /[Uu]nchecked/u, + /checkbox/u, /unchecked/u, /single-use human confirmation/u, /Allow all and batch approval never apply/u, - /without repeat confirmation/u, /profile\/helper revision/u, - /newly reviewed job/u, /stop job-scoped approval/u, /API-only read-only contracts/u, /never substitute a shell\/browser POST/u ]) { assert.match(text, contract, entryPoint); } } diff --git a/test/dashboard-dev-post.test.js b/test/dashboard-dev-post.test.js index dc92437..60a32f3 100644 --- a/test/dashboard-dev-post.test.js +++ b/test/dashboard-dev-post.test.js @@ -3,7 +3,7 @@ import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "nod import { tmpdir } from "node:os"; import { join } from "node:path"; import test from "node:test"; -import { createDevPostTool, createPostWriteApproval } from "../dashboard/dev-post.js"; +import { createDevPostTool } from "../dashboard/dev-post.js"; import { runCopilotSession } from "../dashboard/copilot-session.js"; import { createRuntimeRequestBridge, runtimeRequestId } from "../dashboard/runtime-requests.js"; @@ -38,7 +38,7 @@ function fixture(t, overrides = {}) { tool: () => createDevPostTool(settings) }; } -test("legacy DEV/SIT/UAT POST preferences still require individual confirmation", async (t) => { +test("DEV/SIT/UAT POST checkboxes enable requests or defer confirmation without approving writes", async (t) => { for (const environment of ["DEV", "SIT", "UAT"]) { for (const enabled of [true, false]) { const state = fixture(t); @@ -81,127 +81,6 @@ test("legacy DEV/SIT/UAT POST preferences still require individual confirmation" } }); -test("explicit job-scoped POST approval authorizes configured DEV/SIT/UAT writes without repeat prompts", async (t) => { - for (const environment of ["DEV", "SIT", "UAT"]) { - const state = fixture(t); - const key = environment.toLowerCase(); - const configuration = structuredClone(state.profile.browserAutomation.dataSafety.devPost); - configuration.actions[0].url = `https://api.${key}.example.test/items`; - delete state.profile.browserAutomation.dataSafety.devPost; - state.profile.browserAutomation.dataSafety.post = { [key]: configuration }; - state.profile.environments = ["DEV", "SIT", "UAT"]; - state.save(); - const jobId = "11111111-1111-4111-8111-111111111111"; - Object.assign(state.settings, { - jobId, targetEnvironment: environment, allowPostRequests: true, - postWriteApproval: createPostWriteApproval(state.repository, environment, jobId) - }); - state.args.url = configuration.actions[0].url; - const tool = state.tool(); - for (let action = 0; action < 2; action++) { - const result = await tool.handler(state.args); - assert.equal(result.environment, environment); - assert.equal(result.mutationMode, "approved-write"); - assert.equal(result.validationPassed, false); - } - assert.equal(state.prompts.length, 0); - assert.equal(state.requests.length, 2); - await assert.rejects(tool.handler({ ...state.args, payload: { name: "OTHER_RECORD" } }), /allowlisted/u); - await assert.rejects(tool.handler({ ...state.args, url: "https://api.other.example.test/items" }), /allowlisted/u); - assert.equal(state.requests.length, 2); - } -}); - -test("job-scoped POST approval is bound to the job, environment and reviewed profile/helper", async (t) => { - for (const change of ["job", "environment", "profile", "helper", "unchecked"]) { - const state = fixture(t, { jobId: "11111111-1111-4111-8111-111111111111", allowPostRequests: true }); - state.settings.postWriteApproval = createPostWriteApproval(state.repository, "DEV", state.settings.jobId); - if (change === "job") { state.settings.jobId = "22222222-2222-4222-8222-222222222222"; } - if (change === "environment") { state.settings.targetEnvironment = "SIT"; } - if (change === "unchecked") { state.settings.allowPostRequests = false; } - if (["job", "environment", "unchecked"].includes(change)) { - assert.throws(state.tool, /job-scoped POST approval/u); - } else { - const tool = state.tool(); - if (change === "profile") { state.profile.name = "Changed"; state.save(); } - if (change === "helper") { writeFileSync(state.helper, `${readFileSync(state.helper, "utf8")}\n`); } - await assert.rejects(tool.handler(state.args), /changed.*newly reviewed job/u); - } - assert.equal(state.prompts.length, 0); - assert.equal(state.requests.length, 0); - } -}); - -test("job-scoped POST approval stops further writes after failed or ambiguous attempts", async (t) => { - for (const status of [302, 401, 500, "network", "auth", "empty-auth"]) { - const state = fixture(t, { jobId: "11111111-1111-4111-8111-111111111111", allowPostRequests: true }); - if (status === "auth") { - writeFileSync(state.helper, 'export async function withApiToken() { throw new Error("synthetic-private-value"); }\n'); - } - if (status === "empty-auth") { - writeFileSync(state.helper, 'export async function withApiToken() {}\n'); - } - state.settings.postWriteApproval = createPostWriteApproval(state.repository, "DEV", state.settings.jobId); - state.settings.request = async (url, options) => { - state.requests.push({ url, options }); - if (status === "network") { throw new Error("synthetic-private-value"); } - return new Response(null, { status: typeof status === "number" ? status : 201 }); - }; - const tool = state.tool(); - await assert.rejects(tool.handler(state.args), /Approval is spent|no request was sent|did not complete one request/u); - await assert.rejects(tool.handler(state.args), /job-scoped POST approval.*stopped/u); - assert.equal(state.prompts.length, 0); - assert.equal(state.requests.length, ["auth", "empty-auth"].includes(status) ? 0 : 1); - } -}); - -test("checkbox approval cannot gain endpoint scope added after job creation", async (t) => { - const state = fixture(t, { jobId: "11111111-1111-4111-8111-111111111111", allowPostRequests: true }); - const configuration = state.profile.browserAutomation.dataSafety.devPost; - delete state.profile.browserAutomation.dataSafety.devPost; - state.save(); - state.settings.postWriteApproval = createPostWriteApproval(state.repository, "DEV", state.settings.jobId); - const tool = state.tool(); - await assert.rejects(tool.handler(state.args), /not configured/u); - state.profile.browserAutomation.dataSafety.devPost = configuration; - state.save(); - await assert.rejects(tool.handler(state.args), /changed.*newly reviewed job/u); - assert.equal(state.prompts.length, 0); - assert.equal(state.requests.length, 0); -}); - -test("native SDK consumes job-scoped POST approval without prompting and revokes it on shutdown", async (t) => { - const state = fixture(t); - const originalFetch = globalThis.fetch; - globalThis.fetch = state.settings.request; - t.after(() => { globalThis.fetch = originalFetch; }); - const sessionId = "11111111-1111-4111-8111-111111111111"; - let exposedTool; - await runCopilotSession({ - repository: state.repository, sessionId, targetEnvironment: "DEV", allowPostRequests: true, - postWriteApproval: createPostWriteApproval(state.repository, "DEV", sessionId), - testMutationMode: "approved-write", prompt: "Synthetic approved test-record write." - }, { - output() {}, - async ask() { assert.fail("checked POST must not ask for a second approval"); }, - client: { - async start() {}, - async createSession(options) { - exposedTool = options.tools[0]; - return { - rpc: { managedSettings: { async get() { return { failClosed: false }; } } }, on() {}, - async sendAndWait() { - assert.equal((await exposedTool.handler(state.args)).status, 201); - } - }; - }, - async stop() { return []; } - } - }); - await assert.rejects(exposedTool.handler(state.args), /policy.*shutdown/u); - assert.equal(state.requests.length, 1); -}); - test("deferred POST reports missing setup without asking for unsafe approval", async (t) => { const state = fixture(t, { allowPostRequests: false, mutationMode: "none" }); delete state.profile.browserAutomation.dataSafety.devPost; diff --git a/test/dashboard-server.test.js b/test/dashboard-server.test.js index d88d4a3..afaa377 100644 --- a/test/dashboard-server.test.js +++ b/test/dashboard-server.test.js @@ -1,6 +1,6 @@ import assert from "node:assert/strict"; import { EventEmitter, once } from "node:events"; -import childProcess, { spawnSync } from "node:child_process"; +import { spawnSync } from "node:child_process"; import { randomUUID } from "node:crypto"; import { existsSync, @@ -14,7 +14,6 @@ import { } from "node:fs"; import { createServer as createHttpServer, request as createHttpRequest } from "node:http"; import { createServer } from "node:net"; -import { syncBuiltinESMExports } from "node:module"; import { tmpdir } from "node:os"; import { dirname, join, parse } from "node:path"; import { PassThrough, Readable, Writable } from "node:stream"; @@ -1288,27 +1287,9 @@ test("shared dashboard jobs refresh develop, reuse one checkout and reacquire he return { completion, terminate() { finish({ code: null, signal: "SIGTERM" }); } }; }; let current = await startDashboard(repository, executor); - // Keep fixture connections separate from the dashboard's internal fetch pool. - const requestJson = (path, body) => new Promise((resolve, reject) => { - const payload = body === undefined ? undefined : JSON.stringify(body); - const request = createHttpRequest(`${current.url}${path}`, { - method: payload === undefined ? "GET" : "POST", - agent: false, - headers: payload === undefined ? {} : { - "Content-Type": "application/json", "Content-Length": Buffer.byteLength(payload) - } - }, (response) => { - let content = ""; - response.setEncoding("utf8"); - response.on("data", (chunk) => { content += chunk; }); - response.once("error", reject); - response.once("end", () => resolve({ - response: { status: response.statusCode }, body: content - })); - }); - request.once("error", reject); - request.end(payload); - }).then(({ response, body }) => ({ response, body: JSON.parse(body) })); + const post = (path, body) => jsonRequest(current.url, path, { + method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) + }); const waitUntil = async (condition) => { for (let attempt = 0; attempt < 100 && !condition(); attempt += 1) { await new Promise((resolve) => setTimeout(resolve, 10)); @@ -1320,7 +1301,7 @@ test("shared dashboard jobs refresh develop, reuse one checkout and reacquire he execution.finish({ code: 0 }); }; try { - const first = await requestJson("/api/jobs", { request: "First shared fix.", mode: "fix-and-validate" }); + const first = await post("/api/jobs", { request: "First shared fix.", mode: "fix-and-validate" }); assert.equal(first.response.status, 202); const firstId = first.body.job.id; const firstBranch = first.body.job.sharedWorkspace.branch; @@ -1332,11 +1313,11 @@ test("shared dashboard jobs refresh develop, reuse one checkout and reacquire he const firstCommit = git(repository, ["rev-parse", "HEAD"]); block(executions[0]); await waitUntil(() => !existsSync(join(repository, ".git", "fixlab", "executor-lease"))); - await requestJson("/api/jobs/hold", { jobId: firstId }); + await post("/api/jobs/hold", { jobId: firstId }); writeFileSync(join(source, "upstream.txt"), "fresh develop"); git(source, ["add", "upstream.txt"]); git(source, ["commit", "--quiet", "-m", "advance develop"]); - const second = await requestJson("/api/jobs", { request: "Second independent fix.", mode: "fix-and-validate" }); + const second = await post("/api/jobs", { request: "Second independent fix.", mode: "fix-and-validate" }); assert.equal(second.response.status, 202); assert.notEqual(second.body.job.sharedWorkspace.branch, firstBranch); assert.equal(second.body.job.sharedWorkspace.baseCommit, git(source, ["rev-parse", "HEAD"])); @@ -1345,12 +1326,12 @@ test("shared dashboard jobs refresh develop, reuse one checkout and reacquire he assert.equal(readFileSync(join(repository, "upstream.txt"), "utf8"), "fresh develop"); block(executions[1]); await waitUntil(() => !existsSync(join(repository, ".git", "fixlab", "executor-lease"))); - await requestJson("/api/jobs/hold", { jobId: second.body.job.id }); + await post("/api/jobs/hold", { jobId: second.body.job.id }); await current.dashboard.close(); current = await startDashboard(repository, executor); - const restored = await requestJson("/api/status"); + const restored = await jsonRequest(current.url, "/api/status"); assert.equal(restored.body.held.find((job) => job.id === firstId).sharedWorkspace.branch, firstBranch); - const resumed = await requestJson("/api/jobs/resume", { jobId: firstId }); + const resumed = await post("/api/jobs/resume", { jobId: firstId }); assert.equal(resumed.response.status, 202); assert.equal(executions[2].input.sessionId, firstId); assert.equal(executions[2].input.resume, true); @@ -1359,7 +1340,7 @@ test("shared dashboard jobs refresh develop, reuse one checkout and reacquire he assert.equal(readFileSync(join(repository, "first-fix.txt"), "utf8"), "first fix"); block(executions[2]); await waitUntil(() => !existsSync(join(repository, ".git", "fixlab", "executor-lease"))); - const restarted = await requestJson("/api/jobs/restart", { jobId: firstId }); + const restarted = await post("/api/jobs/restart", { jobId: firstId }); assert.equal(restarted.response.status, 202); assert.notEqual(restarted.body.job.id, firstId); assert.notEqual(restarted.body.job.sharedWorkspace.branch, firstBranch); @@ -1565,38 +1546,6 @@ async function approvePreflightDirectory(url, job, directory) { }); } -test("idle status polling and cache inspection hide their Git windows", async (context) => { - const repository = createGitRepository(); - const { dashboard, url } = await startDashboard(repository, () => { - throw new Error("Idle status polling must not start an executor"); - }); - const invocation = context.mock.method(childProcess, "spawnSync"); - syncBuiltinESMExports(); - try { - for (let index = 0; index < 2; index += 1) { - const status = await jsonRequest(url, "/api/status"); - assert.equal(status.response.status, 200); - assert.equal(status.body.readiness.profileReady, true); - assert.equal(status.body.job, null); - } - assert.ok(createCacheContext(repository)?.key); - const probes = invocation.mock.calls - .map((call) => call.arguments) - .filter(([command]) => command === "git"); - assert.ok(probes.filter(([, args]) => args.includes("--git-common-dir")).length >= 3); - assert.ok(probes.some(([, args]) => args.includes("HEAD"))); - for (const [, args, options] of probes) { - assert.equal(options.windowsHide, true, `Git probe must not open a window: ${args.join(" ")}`); - assert.equal(options.shell, false); - } - } finally { - invocation.mock.restore(); - syncBuiltinESMExports(); - await dashboard.close(); - rmSync(repository, { recursive: true, force: true }); - } -}); - test("job admission rejects invalid default validation profiles without stopping the dashboard", async () => { const repository = createGitRepository(); let executions = 0; @@ -1725,9 +1674,6 @@ test("DEV/SIT/UAT POST preferences reach the executor, queue and recovered job w } assert.equal(launches.length, 1); assert.equal(launches[0].allowPostRequests, true); - assert.equal(launches[0].postWriteApproval, undefined); - assert.match(launches[0].prompt, /Verified read-only POST searches:/u); - assert.match(launches[0].prompt, /query allowlist independently of this checkbox/u); assert.match(launches[0].prompt, /Every data-changing action still requires separate exact destination\/payload confirmation/u); await running.dashboard.close(); running = await startDashboard(repository, executor, undefined, { runtime: "copilot" }); @@ -1740,94 +1686,6 @@ test("DEV/SIT/UAT POST preferences reach the executor, queue and recovered job w } }); -test("POST checkbox write consent is origin-bound, job-scoped and never recovered or inherited", async () => { - const repository = createGitRepository(); - const profile = validProfile(); - profile.environments = ["dev", "sit", "uat"]; - profile.browserAutomation.dataSafety.post = Object.fromEntries(profile.environments.map((environment) => [ - environment, { scope: "api://example-service/access", sessionHelper: "post-session.mjs", - actions: [{ url: `https://api.${environment}.example.test/items`, - testRecord: { field: "name", value: "FIXLAB_TEST_001" } }] } - ])); - writeFileSync(join(repository, "post-session.mjs"), - 'export async function withApiToken(options, run) { await run("synthetic-value"); }\n'); - writeFileSync(join(repository, ".github", "fixlab", "repository-profile.json"), JSON.stringify(profile)); - const launches = []; - const executor = (input) => { - launches.push(input); - return { completion: new Promise(() => {}) }; - }; - let running = await startDashboard(repository, executor, undefined, { runtime: "copilot" }); - try { - const post = (changes = {}, origin = running.url) => jsonRequest(running.url, "/api/jobs", { - method: "POST", - headers: { "Content-Type": "application/json", ...(origin ? { Origin: origin } : {}) }, - body: JSON.stringify({ request: "Synthetic approved test-record writes.", mode: "validate-only", - targetEnvironment: "dev", allowPostRequests: true, postWriteApprovalConfirmed: true, ...changes }) - }); - for (const changes of [ - { postWriteApprovalConfirmed: "yes" }, { postWriteApprovalConfirmed: null }, - { allowPostRequests: false }, - { testMutationMode: "none" }, { testMutationMode: "intercepted" } - ]) { - assert.equal((await post(changes)).response.status, 400); - } - for (const toolApprovalMode of [undefined, "ask", "allow-all"]) { - for (const origin of ["", "https://example.invalid"]) { - assert.equal((await post({ toolApprovalMode, allowAllConfirmed: true }, origin)).response.status, 403); - } - } - const wrongHost = await new Promise((resolveResponse, reject) => { - const request = createHttpRequest(`${running.url}/api/jobs`, { - method: "POST", - headers: { Host: "example.invalid", Origin: running.url, "Content-Type": "application/json" } - }, (response) => { - response.resume(); - response.once("end", () => resolveResponse(response.statusCode)); - }); - request.once("error", reject); - request.end(JSON.stringify({ request: "Synthetic rejected origin.", mode: "validate-only", - targetEnvironment: "dev", allowPostRequests: true, postWriteApprovalConfirmed: true })); - }); - assert.equal(wrongHost, 403); - assert.equal(launches.length, 0); - const created = await post(); - assert.equal(created.response.status, 202, created.body.error); - assert.equal(created.body.job.postWriteApproved, true); - assert.equal(launches[0].postWriteApproval.jobId, created.body.job.id); - assert.equal(launches[0].postWriteApproval.environment, "dev"); - assert.match(launches[0].postWriteApproval.revision, /^[a-f0-9]{64}$/u); - assert.match(launches[0].prompt, /without repeat confirmation/u); - assert.match(launches[0].prompt, /query allowlist independently of this checkbox/u); - assert.equal((await post({ postWriteApprovalConfirmed: false })).body.queue[0].postWriteApproved, false); - const queued = await post({ targetEnvironment: "sit" }); - assert.equal(queued.body.queue[1].postWriteApproved, true); - assert.notEqual(queued.body.queue[1].id, created.body.job.id); - const held = await jsonRequest(running.url, "/api/jobs/hold", { - method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ jobId: queued.body.queue[1].id }) - }); - assert.equal(held.response.status, 202, held.body.error); - assert.equal(held.body.held[0].postWriteApproved, false); - const snapshot = readFileSync(join(dirname(createCacheContext(repository).cachePath), "dashboard-jobs.json"), "utf8"); - assert.doesNotMatch(snapshot, /"postWriteApproval":/u); - await running.dashboard.close(); - running = await startDashboard(repository, executor, undefined, { runtime: "copilot" }); - const recovered = (await jsonRequest(running.url, "/api/status")).body; - assert.equal(recovered.job.postWriteApproved, false); - assert.ok([...recovered.queue, ...recovered.held].every((job) => job.postWriteApproved === false)); - const resumed = await jsonRequest(running.url, "/api/job/input", { - method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ action: "retry", details: "Continue with individual write review." }) - }); - assert.equal(resumed.response.status, 202, resumed.body.error); - assert.equal(launches.at(-1).postWriteApproval, undefined); - } finally { - await running.dashboard.close(); - rmSync(repository, { recursive: true, force: true }); - } -}); - test("reviewed file trust is exact-job bound, persists on resume/restart and is never inherited by new jobs", async () => { const repository = createGitRepository(); let finish; @@ -2087,9 +1945,6 @@ process.stdin.once("data", (chunk) => { const valid = config.inputTokenLimit === 100 && config.fileAccess.mode === "source-read-write" && config.fileAccess.jobId === config.sessionId && - config.postWriteApproval.jobId === config.sessionId && - config.postWriteApproval.environment === "dev" && - config.postWriteApproval.revision === "a".repeat(64) && config.protectedPaths.length === 1 && config.protectedPaths[0].endsWith("session.js"); process.stdout.write(JSON.stringify({ type: "usage", usage: { inputTokens: 110, cachedInputTokens: 90, cacheWriteTokens: 20, outputTokens: 5 } @@ -2107,7 +1962,6 @@ setInterval(() => {}, 1000); try { handle = createCopilotExecutor({ packageRoot: runtime })({ repository, sessionId, prompt: "Synthetic owned budget protocol.", - postWriteApproval: { jobId: sessionId, environment: "dev", revision: "a".repeat(64) }, fileAccess: { mode: "source-read-write", repository: realpathSync.native(repository), jobId: sessionId }, inputTokenLimit: 100, protectedPaths: [join(repository, "session.js")], onUsage(value) { usage.push(value); }, diff --git a/test/dashboard-workflow-policy.test.js b/test/dashboard-workflow-policy.test.js index a5f1d98..a8ad53e 100644 --- a/test/dashboard-workflow-policy.test.js +++ b/test/dashboard-workflow-policy.test.js @@ -3,94 +3,9 @@ import { mkdtempSync, readFileSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import test from "node:test"; -import { API_ONLY_PLAYWRIGHT_GUIDANCE, UNIT_TEST_ONLY_GUIDANCE, PR_LEARNING_GUIDANCE, READ_ONLY_POST_GUIDANCE, configuredWorkflow, efficientWorkflowGuidance } from "../dashboard/workflow-policy.js"; +import { API_ONLY_PLAYWRIGHT_GUIDANCE, UNIT_TEST_ONLY_GUIDANCE, configuredWorkflow, efficientWorkflowGuidance } from "../dashboard/workflow-policy.js"; import { ValidationSession, selectedValidationGates, validationGates } from "../dashboard/validation-gates.js"; -import { buildJobPrompt, FIXLAB_STAGES } from "../dashboard/server.js"; - -test("verified read-only POST searches use the default browser policy independently of write consent", () => { - for (const mode of ["fix-and-validate", "validate-only", "playwright-only"]) { - for (const allowPostRequests of [undefined, false, true]) { - const testEvidence = { - enabled: true, source: "non-production-read-only", mutationMode: "intercepted", - requireScenarioEvidence: true, allowPostRequests - }; - const prompt = buildJobPrompt({ - request: "Validate a permitted search using genuine returned records.", - mode, targetEnvironment: "dev", testEvidence - }); - assert.ok(prompt.includes(READ_ONLY_POST_GUIDANCE)); - for (const phrase of [ - "Verified read-only POST searches:", "exact approved environment, origin and path", - "genuine returned records", "policy blocker, not backend unavailability", - "Do not silently substitute synthetic data", "Read-only jobs must not edit configuration", - "synthetic-only, API-only, unit-only or production policy" - ]) { - assert.ok(prompt.includes(phrase), `${mode}/${allowPostRequests}: ${phrase}`); - } - assert.equal(testEvidence.mutationMode, "intercepted"); - } - } - for (const testEvidence of [{ apiOnly: true }, { unitTestsOnly: true }]) { - assert.ok(!buildJobPrompt({ request: "Keep stricter validation.", mode: "validate-only", - testEvidence }).includes(READ_ONLY_POST_GUIDANCE)); - } -}); - -test("verified query guidance stays aligned without expanding interactive CLI arguments", () => { - for (const path of [ - "agents/fixlab.md", "com.github.copilot/agents/fixlab.agent.md", - "templates/fixlab-autofix.agent.md", ".github/agents/fixlab-autofix.agent.md", - ".github/agents/fixlab.agent.md", "prompts/fixlab.live-test.prompt.md" - ]) { - const source = readFileSync(new URL(`../${path}`, import.meta.url), "utf8") - .replace(/`/gu, "").replace(/\s+/gu, " "); - assert.ok(source.includes(READ_ONLY_POST_GUIDANCE), path); - } - const cli = readFileSync(new URL("../bin/fixlab.js", import.meta.url), "utf8"); - assert.match(cli, /request && profile\.validation\?\.liveTestsRequired !== false &&\s+profile\.browserAutomation\?\.mode !== "api" \? \[READ_ONLY_POST_GUIDANCE\]/u); -}); - -test("reusable learning is captured within the authorized PR stage without a new roadmap stage", () => { - for (const mode of ["efficient", "strict"]) { - const prompt = buildJobPrompt({ - request: "Fix a repeatable defect.", - mode: "fix-and-validate", - workflow: configuredWorkflow({ workflow: { mode } }) - }); - for (const phrase of [ - "Reusable PR learning:", "existing pr stage", ".github/skills//SKILL.md", - "verified, non-obvious, reusable", "update a matching skill", - "YAML name and description", "when to use", "verified solution", - "working directories", "validation commands", "pitfalls", - "same reviewed PR", "no new reusable learning", "no separate roadmap stage", - "Do not add allowed-tools or broaden permissions", - "Read-only jobs must not write skills" - ]) { - assert.ok(prompt.includes(phrase), `${mode}: ${phrase}`); - } - } - assert.deepEqual(FIXLAB_STAGES, [ - "intake", "diagnosis", "reproduce", "fix", "review", "local-stack", "live-test", "pr" - ]); - for (const mode of ["validate-only", "playwright-only"]) { - const prompt = buildJobPrompt({ request: "Inspect without edits.", mode }); - assert.doesNotMatch(prompt, /Reusable PR learning:|\.github\/skills\/\/SKILL\.md/); - } -}); - -test("PR learning stays aligned across CLI, packaged agents, initialized agents and PR prompts", () => { - for (const path of [ - "agents/fixlab.md", "com.github.copilot/agents/fixlab.agent.md", - "templates/fixlab-autofix.agent.md", ".github/agents/fixlab-autofix.agent.md", - ".github/agents/fixlab.agent.md", "prompts/fixlab.pr.prompt.md" - ]) { - const source = readFileSync(new URL(`../${path}`, import.meta.url), "utf8") - .replace(/`/gu, "").replace(/\s+/gu, " "); - assert.ok(source.includes(PR_LEARNING_GUIDANCE), `${path} must preserve the complete PR learning contract`); - } - const cli = readFileSync(new URL("../bin/fixlab.js", import.meta.url), "utf8"); - assert.match(cli, /!readOnly && request \? \[PR_LEARNING_GUIDANCE\]/); -}); +import { buildJobPrompt } from "../dashboard/server.js"; test("production-backed API safety guidance reaches CLI, dashboard and every packaged agent", () => { for (const path of [ diff --git a/test/documentation-policy.test.js b/test/documentation-policy.test.js index b519b2c..28bff03 100644 --- a/test/documentation-policy.test.js +++ b/test/documentation-policy.test.js @@ -1,5 +1,5 @@ import assert from "node:assert/strict"; -import { mkdirSync, readFileSync, writeFileSync } from "node:fs"; +import { mkdirSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { mkdtempSync, rmSync } from "node:fs"; @@ -8,7 +8,6 @@ import { checkDocumentation } from "../scripts/ci/check-docs.mjs"; const requiredFiles = [ "README.md", - "installation.md", "CONTRIBUTING.md", "SECURITY.md", "docs/architecture.md", @@ -35,28 +34,6 @@ function createDocumentationTree() { return root; } -test("first installation is linked, packaged and separates prerequisites from setup changes", () => { - const read = (file) => readFileSync(new URL(`../${file}`, import.meta.url), "utf8"); - const readme = read("README.md"); - assert.match(readme, /\[installation\.md\]\(installation\.md\)/u); - const install = readme.indexOf("npm install --global"); - const onboard = readme.indexOf("fixlab onboard C:\\source\\application --runtime copilot"); - const doctor = readme.indexOf("fixlab doctor C:\\source\\application --runtime copilot"); - const chat = readme.indexOf("fixlab chat C:\\source\\application --runtime copilot"); - assert.ok(install >= 0 && install < onboard && onboard < doctor && doctor < chat, - "show onboarding and Doctor between installation and chat"); - assert.ok(JSON.parse(read("package.json")).files.includes("installation.md")); - const guide = read("installation.md"); - for (const section of [ - "## 1. Prerequisites: install separately", - "## 2. Install FixLab", - "### What this adds to the machine", - "## 3. Set up an application" - ]) { - assert.ok(guide.includes(section), section); - } -}); - test("documentation policy accepts complete internal links", () => { const root = createDocumentationTree(); try { diff --git a/test/fixlab-cli.test.js b/test/fixlab-cli.test.js index 1e6dda4..73da840 100644 --- a/test/fixlab-cli.test.js +++ b/test/fixlab-cli.test.js @@ -501,93 +501,12 @@ test("doctor blocks when Playwright is unavailable", () => { const repository = mkdtempSync(join(tmpdir(), "fixlab-cli-")); try { - const initialized = run(["init", repository], repository); - assert.equal(initialized.status, 0); - assert.match(initialized.stdout, /frontend.*backend.*example paths/i); - assert.match(initialized.stdout, /Do not create empty application folders/); + assert.equal(run(["init", repository], repository).status, 0); const result = run(["doctor", repository], repository); assert.equal(result.status, 1); assert.match(result.stdout, /FAIL Playwright package/); assert.match(result.stdout, /FAIL Playwright browser/); - assert.match(result.stdout, /browserAutomation\.workingDirectory/); - assert.match(result.stdout, /applications\.frontend\.workingDirectory/); - assert.ok(result.stdout.includes(join(".github", "fixlab", "repository-profile.json"))); - assert.match(result.stdout, /Do not create an empty frontend folder/); - const setup = run(["setup-playwright", repository, "--yes"], repository); - assert.equal(setup.status, 1); - assert.match(setup.stderr, /browserAutomation\.workingDirectory/); - assert.match(setup.stderr, /Do not create an empty frontend folder/); - assert.doesNotMatch(setup.stdout, /Planned commands:/); - assert.equal(existsSync(join(repository, "frontend")), false); - assert.equal(existsSync(join(repository, "backend")), false); - } finally { - rmSync(repository, { recursive: true, force: true }); - } -}); - -test("doctor and Playwright setup reject a file used as the working directory", () => { - const repository = mkdtempSync(join(tmpdir(), "fixlab-cli-")); - try { - assert.equal(run(["init", repository], repository).status, 0); - const frontend = join(repository, "frontend"); - writeFileSync(frontend, "preserve this file"); - const result = run(["doctor", repository], repository, createPackageManagerShims(repository)); - assert.equal(result.status, 1); - assert.match(result.stdout, /FAIL Playwright package \(working directory is not a directory:/); - assert.match(result.stdout, /browserAutomation\.workingDirectory/); - assert.doesNotMatch(result.stdout, /FAIL Frontend package manager/); - const setup = run(["setup-playwright", repository, "--yes"], repository); - assert.equal(setup.status, 1); - assert.match(setup.stderr, /working directory is not a directory:/); - assert.doesNotMatch(setup.stdout, /Planned commands:/); - assert.equal(readFileSync(frontend, "utf8"), "preserve this file"); - } finally { - rmSync(repository, { recursive: true, force: true }); - } -}); - -test("doctor explains a missing Playwright package in an existing empty directory", () => { - const repository = mkdtempSync(join(tmpdir(), "fixlab-cli-")); - try { - assert.equal(run(["init", repository], repository).status, 0); - mkdirSync(join(repository, "frontend")); - const result = run(["doctor", repository], repository, createPackageManagerShims(repository)); - assert.equal(result.status, 1); - assert.match(result.stdout, /FAIL Playwright package \(install @playwright\/test/); - assert.match(result.stdout, /browserAutomation\.workingDirectory/); - assert.match(result.stdout, /An empty frontend folder does not configure the application/); - assert.match(result.stdout, /fixlab setup-playwright/); - assert.equal(existsSync(join(repository, "frontend", "node_modules")), false); - } finally { - rmSync(repository, { recursive: true, force: true }); - } -}); - -test("Playwright directory guidance preserves API-only checks and frontend fallback", () => { - const repository = mkdtempSync(join(tmpdir(), "fixlab-cli-")); - try { - assert.equal(run(["init", repository], repository).status, 0); - const path = join(repository, ".github", "fixlab", "repository-profile.json"); - const profile = JSON.parse(readFileSync(path, "utf8")); - profile.browserAutomation.mode = "api"; - profile.browserAutomation.workingDirectory = "api-tests"; - writeFileSync(path, JSON.stringify(profile)); - const result = run(["doctor", repository], repository); - assert.equal(result.status, 1); - assert.match(result.stdout, /FAIL Playwright API \(working directory is missing:/); - assert.ok(result.stdout.includes(join(repository, "api-tests"))); - assert.doesNotMatch(result.stdout, /Playwright browser/); - delete profile.browserAutomation.workingDirectory; - profile.applications.frontend.workingDirectory = "ui-app"; - writeFileSync(path, JSON.stringify(profile)); - const setup = run(["setup-playwright", repository, "--yes"], repository); - assert.equal(setup.status, 1); - assert.ok(setup.stderr.includes(join(repository, "ui-app"))); - assert.match(setup.stderr, /applications\.frontend\.workingDirectory/); - assert.doesNotMatch(setup.stdout, /Planned commands:/); - assert.equal(existsSync(join(repository, "api-tests")), false); - assert.equal(existsSync(join(repository, "ui-app")), false); } finally { rmSync(repository, { recursive: true, force: true }); } @@ -1267,8 +1186,6 @@ test("run launches the Agency-resolved FixLab agent", () => { assert.match(result.stdout, /Use development as the user-selected validation environment/); assert.match(result.stdout, /Do not silently fall back/); assert.match(result.stdout, /primary business-data source/); - assert.match(result.stdout, /Verified read-only POST searches:/u); - assert.match(result.stdout, /policy blocker, not backend unavailability/u); assert.match(result.stdout, /synthetic pass proves the UI behavior only/); assert.match(result.stdout, /FIXLAB_ACTIVITY/); assert.match(result.stdout, /FIXLAB_TEST/); @@ -1587,9 +1504,8 @@ test("run without a request keeps Agency guidance interactive", () => { } ); - assert.equal(result.status, 0, result.stderr); + assert.equal(result.status, 0); assert.match(result.stdout, /--interactive/); - assert.doesNotMatch(result.stdout, /Reusable PR learning:/); assert.doesNotMatch(result.stdout, /--session-id/); assert.doesNotMatch(result.stdout, /--no-ask-user/); } finally { @@ -1634,9 +1550,8 @@ test("run without a request keeps Copilot environment guidance interactive", () "inherited-standard-input\n" ); - assert.equal(result.status, 0, result.stderr); + assert.equal(result.status, 0); assert.match(result.stdout, /--interactive/); - assert.doesNotMatch(result.stdout, /Reusable PR learning:/); assert.match( result.stdout, /Use development as the user-selected validation environment/