Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 41 additions & 1 deletion client/src/components/fableloom/LoomEditorialAutomation.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ function FindingLink({ loom, finding }) {
export default function LoomEditorialAutomation({ loom, dirty, onLoomUpdate }) {
const [route, setRoute] = useState({ providerId: '', model: '', effort: '' });
const [maxRounds, setMaxRounds] = useState(3);
const [selfImprove, setSelfImprove] = useState(false);
const [result, setResult] = useState(null);
const [autopilotRun, setAutopilotRun] = useState(null);
const handledTerminalRunRef = useRef(null);
Expand Down Expand Up @@ -165,6 +166,7 @@ export default function LoomEditorialAutomation({ loom, dirty, onLoomUpdate }) {
const run = await startLoomEditorialAutopilot(loom.id, {
...routeBody,
maxRounds,
...(selfImprove ? { selfImprove: true } : {}),
}, { silent: true });
handledTerminalRunRef.current = null;
setAutopilotRun(run);
Expand Down Expand Up @@ -194,7 +196,7 @@ export default function LoomEditorialAutomation({ loom, dirty, onLoomUpdate }) {
const findings = result?.type === 'autopilot' || shownRun?.status
? shownRun?.residualFindings || []
: review?.findings?.length ? review.findings : evaluation?.findings || [];
const summary = review?.summary || evaluation?.summary || shownRun?.message;
const summary = shownRun?.message || review?.summary || evaluation?.summary;
const passed = result?.type === 'playtest'
? response?.passed
: shownRun?.status === 'completed' || diagnostics?.passed;
Expand Down Expand Up @@ -262,6 +264,25 @@ export default function LoomEditorialAutomation({ loom, dirty, onLoomUpdate }) {
</p>
) : null}

<div className="rounded border border-port-border bg-port-bg/30 px-3 py-2">
<label htmlFor="fableloom-editorial-self-improve" className="flex items-start gap-2 text-xs text-port-text">
<input
id="fableloom-editorial-self-improve"
type="checkbox"
checked={selfImprove}
onChange={(event) => setSelfImprove(event.target.checked)}
disabled={busy}
className="mt-0.5"
/>
<span>
<span className="font-medium">Improve FableLoom itself when autopilot stalls or fails</span>
<span className="mt-1 block text-[11px] text-port-text-muted">
Runs one content-free, budget-gated post-mortem to distinguish story work from a broken or inefficient editor/reviewer workflow. A confident PortOS defect queues a deduplicated worktree + PR CoS task in the approval queue; it never starts by itself. Healthy and canceled runs spend nothing.
</span>
</span>
</label>
</div>

<div className="grid gap-2 sm:grid-cols-3">
<button
type="button"
Expand Down Expand Up @@ -377,6 +398,25 @@ export default function LoomEditorialAutomation({ loom, dirty, onLoomUpdate }) {
{shownRun.rounds.length} bounded editorial round{shownRun.rounds.length === 1 ? '' : 's'} recorded. The run stops on success, plateau, cancellation, provider failure, or the selected round limit.
</p>
) : null}
{shownRun?.selfImprove?.verdict === 'pipeline' ? (
<div className="rounded border border-port-accent/30 bg-port-accent/5 px-3 py-2 text-xs text-port-accent">
<p>
{shownRun.selfImprove.duplicate
? `FableLoom improvement already tracked (${shownRun.selfImprove.area}): ${shownRun.selfImprove.title}`
: shownRun.selfImprove.filed
? `Queued a FableLoom improvement (${shownRun.selfImprove.area}): ${shownRun.selfImprove.title}`
: `Diagnosed a FableLoom workflow defect (${shownRun.selfImprove.area}), but task filing failed: ${shownRun.selfImprove.title}`}
</p>
{shownRun.selfImprove.taskId ? (
<Link
to={`/cos/tasks?task=${encodeURIComponent(shownRun.selfImprove.taskId)}&source=internal`}
className="mt-1 inline-block font-medium hover:underline"
>
Review CoS task
</Link>
) : null}
</div>
) : null}
</div>
) : null}
</section>
Expand Down
57 changes: 57 additions & 0 deletions client/src/components/fableloom/LoomEditorialAutomation.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,63 @@ describe('LoomEditorialAutomation', () => {
expect(screen.getAllByText(/Round 1: evaluating and remediating/).length).toBeGreaterThan(0);
});

it('opts a run into approval-gated FableLoom workflow diagnosis', async () => {
const user = userEvent.setup();
api.startLoomEditorialAutopilot.mockResolvedValue({
id: 'editorial-run-1', loomId: 'loom-1', status: 'running', round: 0, maxRounds: 3,
message: 'Starting FableLoom editorial autopilot…', rounds: [], residualFindings: [],
});
renderPanel();

const toggle = await screen.findByLabelText(/improve fableloom itself/i);
expect(toggle).not.toBeChecked();
await user.click(toggle);
expect(screen.getByText(/queues a deduplicated worktree \+ PR CoS task in the approval queue/i)).toBeInTheDocument();
await user.click(screen.getByRole('button', { name: 'Start editor autopilot' }));

await waitFor(() => expect(api.startLoomEditorialAutopilot).toHaveBeenCalledWith(
'loom-1', { maxRounds: 3, selfImprove: true }, { silent: true },
));
});

it('links a filed workflow diagnosis to its CoS approval task', async () => {
api.getLoom.mockResolvedValue(loom);
api.getLoomEditorialAutopilotStatus.mockResolvedValue({
run: {
id: 'editorial-run-1', loomId: 'loom-1', status: 'paused', round: 2, maxRounds: 3,
pauseReason: 'plateau', message: 'Editorial autopilot paused.', rounds: [],
residualFindings: [],
selfImprove: {
verdict: 'pipeline', area: 'prompt', title: 'Tighten the remediation contract',
taskId: 'sys-example', filed: true, duplicate: false,
},
},
});
renderPanel();

expect(await screen.findByText(/Queued a FableLoom improvement \(prompt\)/)).toBeInTheDocument();
expect(screen.getByRole('link', { name: 'Review CoS task' })).toHaveAttribute(
'href',
'/cos/tasks?task=sys-example&source=internal',
);
});

it('keeps the decided pause explanation visible when cancellation lands during diagnosis', async () => {
api.getLoomEditorialAutopilotStatus.mockResolvedValue({
run: {
id: 'editorial-run-1', loomId: 'loom-1', status: 'canceled', round: 1, maxRounds: 1,
pauseReason: 'round-limit',
message: 'Editorial autopilot reached its 1-round limit with review findings still open.',
rounds: [], residualFindings: [],
lastReview: { summary: 'The story review still has open findings.', findings: [] },
},
});
renderPanel();

expect(await screen.findByText(/reached its 1-round limit with review findings still open/i))
.toBeInTheDocument();
});

it('blocks every mutating AI action while the series plan has unsaved edits', async () => {
renderPanel({ dirty: true });

Expand Down
2 changes: 1 addition & 1 deletion client/src/services/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ toasts on throw). **Custom catch ⇒ `silent: true`** — otherwise toasts fire
| `apiMediaJobs.js` | Media generation job tracking + `refineMediaPrompt` / `promptFromMedia` (vision reverse-prompt). |
| `apiCreativeDirector.js` | Creative Director (video production). |
| `apiCreativeCommission.js` | Creative Commissions (Autonomous Creation Engine — standing recurring briefs). |
| `apiFableLoom.js` | FableLoom branching narratives — loom/episode/scene-node/transition CRUD, deterministic graph validation, and the AI lanes (weave, branch, review, play turns, per-episode scene reformat). |
| `apiFableLoom.js` | FableLoom branching narratives — loom/episode/scene-node/transition CRUD, deterministic graph validation, AI authoring lanes, and the bounded editorial/playthrough autopilot lifecycle. |
| `apiGames.js` | Game studio records, managed-app binding, reusable sprite/music bindings, deterministic asset-bundle compilation/integrity preflight, and AI feedback history. |
| `apiMusicVideo.js` | Music Video projects + scene board + audio analysis. |
| `apiSprites.js` | Sprite Manager records, asset library, production-set import (#2895), reference workflow: create/generate/lock (#2896), directional walk and per-track generation/approval, animation-type definition CRUD (#3153), trim/postprocess, and per-run source-frame listing for the Loop Trimmer's re-derive (#2980), and animation render-provider readiness (#4876). |
Expand Down
4 changes: 2 additions & 2 deletions client/src/services/apiFableLoom.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,9 @@ describe('apiFableLoom', () => {
method: 'POST', body: JSON.stringify({ aiReview: true }),
});

await api.startLoomEditorialAutopilot('loom-1', { maxRounds: 3 });
await api.startLoomEditorialAutopilot('loom-1', { maxRounds: 3, selfImprove: true });
expect(request).toHaveBeenCalledWith('/fableloom/loom-1/editorial/autopilot/start', {
method: 'POST', body: JSON.stringify({ maxRounds: 3 }),
method: 'POST', body: JSON.stringify({ maxRounds: 3, selfImprove: true }),
});

await api.getLoomEditorialAutopilotStatus('loom-1', { silent: true });
Expand Down
7 changes: 7 additions & 0 deletions data.reference/prompts/stage-config.json
Original file line number Diff line number Diff line change
Expand Up @@ -1080,6 +1080,13 @@
"returnsJson": true,
"variables": []
},
"fableloom-editorial-self-improve": {
"name": "FableLoom — Editorial Autopilot Self-Improvement",
"description": "Opt-in, content-free post-mortem for a paused or failed FableLoom editor/reviewer loop. Distinguishes unfinished story work from an inefficient, broken, or incomplete PortOS workflow and returns a bounded software diagnosis; a confident PortOS verdict queues a deduplicated, approval-gated CoS task.",
"model": "heavy",
"returnsJson": true,
"variables": []
},
"fableloom-review-playthroughs": {
"name": "FableLoom — Review Playthrough Variations",
"description": "Reviews deterministically enumerated playthrough variations for mechanics, causal branch coherence, audience agency, continuity, pacing, ending payoff, canon, and ship-quality storytelling.",
Expand Down
83 changes: 83 additions & 0 deletions data.reference/prompts/stages/fableloom-editorial-self-improve.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# FableLoom Editorial Autopilot — Self-Improvement Diagnosis

You are a **systems engineer** doing a post-mortem on PortOS's automated FableLoom editor/reviewer loop. Decide whether the run's trouble came from the **story** (the interactive series still needs editorial work, while the automation behaved correctly) or from **PortOS** (its workflow, prompt, validation, runner, configuration, or UI is broken, wasteful, or unable to apply the repair it claims to perform).

You are NOT reviewing the story. You receive no story prose, titles, record ids, or finding text. Do not invent or request them. Your output may become a CoS coding-task brief and a public pull request, so it must describe **software behavior only**.

## The workflow

Each bounded round performs these steps in order:

1. `evaluate-remediate` — inspect the complete existing series and return a safe sparse patch. It may repair series-plan fields, episode outlines, existing scene metadata, and existing transition labels/targets, but it must preserve episode, scene, and transition membership and ids. PortOS rejects invalid output and graph/outline/playthrough/continuity regressions before saving.
2. `playthrough-review` — deterministically enumerate bounded path variations, run structural and continuity diagnostics, and ask an independent story editor for a quality verdict.
3. If findings remain, translate them into guidance for the next remediation pass. Stop when the story passes, the same findings survive a no-change pass (`plateau`), the selected round limit is reached, the user cancels, or a step throws.

The post-mortem is opt-in and runs only after a pause or failure. A confident `pipeline` verdict (PortOS is at fault) queues a deduplicated, worktree-isolated, PR-opening CoS task. The task waits for human approval before an agent starts. Reporting `content` or `none` is useful and files nothing.

## The run

- **Outcome:** `{{outcome}}`
- **Reason:** `{{outcomeReason}}`
- **Step active when the run stopped:** `{{currentStep}}`
- **Error code:** `{{errorCode}}`
- **Round:** {{round}} / {{maxRounds}}
- **Playthrough path cap:** {{maxPaths}}

Content-free round telemetry:

```json
{{telemetryJson}}
```

Each round reports only counters and booleans:

- `remediation.changed` / `changeCount` — whether a sparse patch actually changed the loom and how many change notes the stage returned.
- `before` / `after` — deterministic blocker counts around the accepted patch.
- `evaluationFindingCount` — how many issues the remediation stage itself identified.
- `diagnosticsPassed` / `diagnosticFindingCount` — whether non-narrative editorial diagnostics cleared.
- `deterministicPassed` / `deterministicComplete` / `stats` — path enumeration health and coverage.
- `reviewPassed` / `qualityScore` / finding counts by category and severity — the independent quality verdict, without any story text.

## What counts as a PORTOS problem

Return `pipeline` only when the counters, terminal reason, or error code support a concrete software change. Strong cases include:

1. **An ineffective loop step.** Remediation repeatedly reports no applicable change while its own findings remain, accepted edits do not reduce the relevant deterministic blockers, or next-round guidance cannot target what the review produced.
2. **A broken output contract or validator.** A stage returns unusable data, claims edits that do not apply, produces a shape its consumer cannot read, or a safe intended repair is rejected for a contract PortOS should support.
3. **A missing or late gate.** Expensive full-series editing discovers a structural condition that a cheaper outline/graph preflight could have surfaced before expansion.
4. **A runner or lifecycle defect.** The wrong provider route is used, cancellation/status handling is inconsistent, a failure is swallowed, or a terminal run is left active.
5. **A bad bound or missing control.** A round/path limit makes the workflow unable to do its job, or the editor needs a specific user-facing option that does not exist.
6. **A wasteful ordering.** The workflow performs costly review before required deterministic evidence is available, or repeats work that an earlier retained result should have made unnecessary.

## What is NOT a PortOS problem

- A story genuinely needs another authored choice, consequence, scene, or ending, and the loop correctly pauses instead of changing graph membership → `content`.
- The round limit is reached while distinct story findings continue to improve from round to round → `content`.
- A plateau alone, without counters showing an automation mismatch → `content` or `none`; do not assume every hard editorial problem is a software bug.
- A one-off provider or network failure with no software evidence → `none`.
- Any theory that requires story text or source inspection not present here → `none` with low confidence.

## Output contract

Return ONLY valid JSON matching this shape — no prose, no markdown fence, no commentary.

```json
{
"verdict": "pipeline",
"confidence": 0.0,
"area": "pipeline-step",
"title": "string — imperative, one line, names the software defect",
"problem": "string — what is wrong in PortOS and which counters/status values show it",
"evidence": ["string — a specific counter, status, reason, or error code from the telemetry above"],
"proposedChange": "string — the smallest concrete source/prompt/check/config/UI change that fixes it",
"risks": "string — behavior that must keep working"
}
```

- `verdict` must be `pipeline` (PortOS is at fault), `content` (the story needs work), or `none`.
- `confidence` is 0–1. Diagnoses below 0.6 are discarded; a guess should be discarded.
- `area` must be `editorial-check`, `pipeline-step`, `prompt`, `runner`, `config`, or `ui`.
- `evidence` has at most 8 entries and may cite only the supplied content-free telemetry.
- `proposedChange` must be actionable without access to this run or its story.

When the story needs work or the evidence is inconclusive, return the same shape with `verdict` set to `content` or `none` and empty change fields.
7 changes: 7 additions & 0 deletions docs/features/fableloom.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,13 @@ scene-local clothes cannot silently reappear between episodes.
are client-side state (restart is free; nothing persists server-side).
- **Story settings drawer** — audience role and communication medium, scene
format (plus the rewrite pass), and the narrator's provider/model/effort pin.
- **AI editor, reviewer & playtest** — a whole-series remediation pass and a
bounded autopilot that alternates safe edits with deterministic + narrative
path review. Its optional **Improve FableLoom itself** post-mortem runs only
after a pause or failure, sends content-free counters (never story records)
through one budget-gated diagnosis, and queues a deduplicated PortOS CoS task
when the workflow rather than the story is at fault. The task remains in the
approval queue; the per-run checkbox does not grant unattended source edits.
- **Series detail page** (`/pipeline/series/:seriesId`) — a "Branching
narratives" card lists the looms linked to that series (counts + a link into
the editor) and spawns a new one pre-linked to the series and its universe.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/** Seed the FableLoom editorial-autopilot self-improvement stage. */

import { makeSeedMigration } from './_seedStageHelpers.js';

export default makeSeedMigration('fableloom-editorial-self-improve');
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { describe } from 'vitest';

import migration from './322-fableloom-editorial-self-improve-stage.js';
import { runSeedStageMigrationTests } from './_seedStageTestHelpers.js';

describe('migration 322 — seed the FableLoom editorial self-improvement stage', () => {
runSeedStageMigrationTests({
migration,
stages: ['fableloom-editorial-self-improve'],
prefix: 'migration-322-',
});
});
4 changes: 4 additions & 0 deletions server/lib/fableLoomValidation.js
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,10 @@ export const playthroughReviewSchema = z.object({
export const editorialAutopilotStartSchema = z.object({
maxRounds: z.number().int().min(1).max(LOOM_LIMITS.EDITORIAL_AUTOPILOT_ROUNDS_MAX).optional(),
maxPaths: z.number().int().min(1).max(FABLELOOM_PLAYTEST_LIMITS.MAX_PATHS).optional(),
// Opt-in post-mortem over content-free run counters. A confident PortOS
// verdict queues an approval-gated CoS task; healthy/canceled runs spend
// nothing, and the story itself never crosses into the task brief.
selfImprove: z.boolean().optional(),
...llmPickFields,
});

Expand Down
3 changes: 3 additions & 0 deletions server/lib/promptStageCallSites.generated.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@
"fableloom-editorial-remediate": [
"server/services/fableLoom/editorial.js"
],
"fableloom-editorial-self-improve": [
"server/services/fableLoom/editorialSelfImprove.js"
],
"fableloom-feedback-episode": [
"server/services/fableLoom/weave.js"
],
Expand Down
12 changes: 6 additions & 6 deletions server/lib/socketEventCatalog.generated.json
Original file line number Diff line number Diff line change
Expand Up @@ -2261,7 +2261,7 @@
{
"direction": "server-to-client",
"source": "client/src/components/fableloom/LoomPlayPanel.jsx",
"line": 114
"line": 125
}
]
},
Expand Down Expand Up @@ -2300,7 +2300,7 @@
{
"direction": "server-to-client",
"source": "client/src/components/fableloom/LoomPlayPanel.jsx",
"line": 145
"line": 156
}
]
},
Expand Down Expand Up @@ -2339,7 +2339,7 @@
{
"direction": "server-to-client",
"source": "client/src/components/fableloom/LoomPlayPanel.jsx",
"line": 137
"line": 148
}
]
},
Expand All @@ -2365,7 +2365,7 @@
{
"direction": "server-to-client",
"source": "client/src/components/fableloom/LoomPlayPanel.jsx",
"line": 118
"line": 129
}
]
},
Expand Down Expand Up @@ -2396,7 +2396,7 @@
{
"direction": "server-to-client",
"source": "client/src/components/fableloom/LoomPlayPanel.jsx",
"line": 122
"line": 133
}
]
},
Expand All @@ -2409,7 +2409,7 @@
{
"direction": "server-to-client",
"source": "client/src/components/fableloom/LoomPlayPanel.jsx",
"line": 126
"line": 137
}
]
},
Expand Down
Loading