diff --git a/src/resource-loader.ts b/src/resource-loader.ts index 62952818c..27c9c3e29 100644 --- a/src/resource-loader.ts +++ b/src/resource-loader.ts @@ -263,6 +263,10 @@ function makeTreeWritable(dirPath: string): void { const stats = lstatSync(dirPath); if (stats.isSymbolicLink()) return; + // Skip node_modules entirely — it's too large to crawl and should already + // have correct permissions from the package manager or cpSync. + if (basename(dirPath) === "node_modules") return; + const isDir = stats.isDirectory(); const currentMode = stats.mode & 0o777; @@ -724,17 +728,16 @@ export function initResources(agentDir: string): void { // env var is not set (e.g. fork/dev builds, alternative entry points). const workflowSrc = join(resourcesDir, "SF-WORKFLOW.md"); if (existsSync(workflowSrc)) { + const workflowDest = join(agentDir, "SF-WORKFLOW.md"); try { - copyFileSync(workflowSrc, join(agentDir, "SF-WORKFLOW.md")); + copyFileSync(workflowSrc, workflowDest); + // Ensure it's writable for the next upgrade cycle + makeTreeWritable(workflowDest); } catch { /* non-fatal */ } } - // Ensure all newly copied files are owner-writable so the next run can - // overwrite them (covers extensions, agents, and skills in one walk). - makeTreeWritable(agentDir); - writeManagedResourceManifest(agentDir); ensureRegistryEntries(join(agentDir, "extensions")); } diff --git a/src/resources/extensions/sf/bootstrap/register-hooks.ts b/src/resources/extensions/sf/bootstrap/register-hooks.ts index f3141bf8f..98ce21794 100644 --- a/src/resources/extensions/sf/bootstrap/register-hooks.ts +++ b/src/resources/extensions/sf/bootstrap/register-hooks.ts @@ -232,6 +232,19 @@ export function registerHooks( "warning", ); } + // Forge-only: surface high/critical entries as inline-fix candidates so + // the operator (or a follow-up dispatcher) can drain self-reported bugs + // without leaving the session. Read-only signal for now — no auto-dispatch. + const highBlocked = triage.stillBlocked.filter( + (e) => e.severity === "high" || e.severity === "critical", + ); + if (highBlocked.length > 0) { + const ids = highBlocked.map((e) => `${e.id} (${e.kind})`).join(", "); + ctx.ui?.notify?.( + `${highBlocked.length} inline-fix candidate${highBlocked.length === 1 ? "" : "s"} pending in .sf/BACKLOG.md: ${ids}`, + "warning", + ); + } } catch { /* non-fatal — self-feedback drain must never block session start */ } diff --git a/src/tests/integration/pack-install.test.ts b/src/tests/integration/pack-install.test.ts index 699ef4b54..09b43419e 100644 --- a/src/tests/integration/pack-install.test.ts +++ b/src/tests/integration/pack-install.test.ts @@ -322,7 +322,7 @@ test("sf launches and loads extensions without errors", async () => { // No extension load errors assert.ok( - !output.includes("[sf] Extension load error"), + !output.includes("[forge] Extension load error"), `no extension load errors on stderr (got: ${output.slice(0, 500)})`, ); diff --git a/src/tests/integration/web-mode-onboarding.test.ts b/src/tests/integration/web-mode-onboarding.test.ts index 74fa31678..af78e4b0c 100644 --- a/src/tests/integration/web-mode-onboarding.test.ts +++ b/src/tests/integration/web-mode-onboarding.test.ts @@ -492,7 +492,7 @@ test("refresh failures keep the workspace locked and expose the failed bridge-re failedBootPayload.onboarding.bridgeAuthRefresh.error, /could not attach/i, ); -}); +}, 120_000); test("fresh sf --web browser onboarding stays locked on failed validation and unlocks after a successful retry", async (t) => { if (process.platform === "win32") { diff --git a/src/tests/integration/web-mode-runtime-harness.ts b/src/tests/integration/web-mode-runtime-harness.ts index 0f8e4f684..2ff82a3e8 100644 --- a/src/tests/integration/web-mode-runtime-harness.ts +++ b/src/tests/integration/web-mode-runtime-harness.ts @@ -198,7 +198,7 @@ export function ensureRuntimeArtifacts(): void { export function parseStartedUrl(stderr: string): string { const match = stderr.match( - /\[sf\] Web mode startup: status=started[^\n]*url=(http:\/\/[^\s]+)/, + /\[forge\] Web mode startup: status=started[^\n]*url=(http:\/\/[^\s]+)/, ); if (!match) { throw new Error( @@ -210,7 +210,7 @@ export function parseStartedUrl(stderr: string): string { function parseReadyAuthToken(stderr: string): string | null { const match = stderr.match( - /\[sf\] Ready → http:\/\/[^\s]+\/#token=([a-f0-9]{64})/, + /\[forge\] Ready → http:\/\/[^\s]+\/#token=([a-f0-9]{64})/, ); return match?.[1] ?? null; }