@gentbajko/slopify 3.16.0 → 3.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (143) hide show
  1. package/dist/edge/http/actions.js +2 -2
  2. package/dist/edge/http/actor.js +10 -0
  3. package/dist/edge/http/app.js +21 -0
  4. package/dist/edge/http/checkpoints.js +14 -6
  5. package/dist/edge/http/external-reviews.js +95 -0
  6. package/dist/edge/http/projects.js +36 -18
  7. package/dist/edge/http/reviews.js +53 -18
  8. package/dist/edge/http/revisions.js +5 -1
  9. package/dist/edge/http/settings.js +9 -1
  10. package/dist/edge/http/starts.js +41 -0
  11. package/dist/edge/http/studio.js +5 -130
  12. package/dist/edge/mcp/attribution.js +38 -0
  13. package/dist/edge/mcp/call-route.js +203 -0
  14. package/dist/edge/mcp/excluded-routes.js +95 -0
  15. package/dist/edge/mcp/file-tools.js +141 -0
  16. package/dist/edge/mcp/http.js +79 -0
  17. package/dist/edge/mcp/instructions.js +17 -0
  18. package/dist/edge/mcp/library-tools.js +143 -0
  19. package/dist/edge/mcp/remake-tools.js +90 -0
  20. package/dist/edge/mcp/review-tools.js +64 -0
  21. package/dist/edge/mcp/route-tool.js +61 -0
  22. package/dist/edge/mcp/server.js +48 -0
  23. package/dist/edge/mcp/state-shapes.js +60 -0
  24. package/dist/edge/mcp/tools-channels.js +153 -0
  25. package/dist/edge/mcp/tools-library.js +151 -0
  26. package/dist/edge/mcp/tools-planning.js +137 -0
  27. package/dist/edge/mcp/tools-projects.js +167 -0
  28. package/dist/edge/mcp/tools-publishing.js +148 -0
  29. package/dist/edge/mcp/tools-runs.js +94 -0
  30. package/dist/edge/mcp/tools-schedules.js +143 -0
  31. package/dist/edge/mcp/tools-setup.js +234 -0
  32. package/dist/edge/mcp/tools-storage.js +99 -0
  33. package/dist/edge/mcp/tools.js +21 -0
  34. package/dist/extension/slopify-studio-chrome.zip +0 -0
  35. package/dist/extension/slopify-studio-firefox.zip +0 -0
  36. package/dist/kernel/db/migrations/0055-external-reviews.sql +56 -0
  37. package/dist/kernel/db/migrations/0056-agent-actions.sql +20 -0
  38. package/dist/kernel/db/migrations/0057-agent-action-once.sql +5 -0
  39. package/dist/kernel/db/migrations/0058-revision-work-admission.sql +3 -0
  40. package/dist/kernel/db/migrations/0059-external-review-canceled.sql +29 -0
  41. package/dist/kernel/db/migrations/0060-reservation-work.sql +4 -0
  42. package/dist/kernel/db/migrations/0061-project-control-counts.sql +4 -0
  43. package/dist/kernel/db/migrations/0062-told-failures.sql +8 -0
  44. package/dist/kernel/pipeline.js +6 -0
  45. package/dist/kernel/runner/graph.js +37 -2
  46. package/dist/kernel/runner/index.js +6 -3
  47. package/dist/main.js +29 -13
  48. package/dist/patch-notes/3.17.0.md +10 -0
  49. package/dist/patch-notes/3.18.0.md +13 -0
  50. package/dist/patch-notes/index.json +12 -0
  51. package/dist/slices/admission/admitted-work.js +53 -0
  52. package/dist/slices/admission/heads.js +5 -0
  53. package/dist/slices/admission/repo.js +47 -29
  54. package/dist/slices/admission/schema.js +1 -0
  55. package/dist/slices/admission/stage-writes.js +21 -0
  56. package/dist/slices/admission/start.js +2 -1
  57. package/dist/slices/admission/state-words.js +110 -0
  58. package/dist/slices/agents/attention-work.js +233 -0
  59. package/dist/slices/agents/attention.js +168 -0
  60. package/dist/slices/agents/connection.js +15 -0
  61. package/dist/slices/agents/model.js +29 -0
  62. package/dist/slices/agents/repo.js +66 -0
  63. package/dist/slices/cancel/index.js +4 -1
  64. package/dist/slices/control/index.js +2 -1
  65. package/dist/slices/episodes/summarize.js +2 -1
  66. package/dist/slices/estimate/index.js +6 -2
  67. package/dist/slices/images/run.js +2 -1
  68. package/dist/slices/library/transfer.js +69 -0
  69. package/dist/slices/narration/run.js +2 -1
  70. package/dist/slices/notifications/address.js +40 -0
  71. package/dist/slices/notifications/notifier.js +43 -10
  72. package/dist/slices/notifications/rules.js +111 -45
  73. package/dist/slices/notifications/server.js +14 -0
  74. package/dist/slices/notifications/settings.js +1 -1
  75. package/dist/slices/notifications/subject.js +30 -0
  76. package/dist/slices/notifications/told.js +33 -0
  77. package/dist/slices/play-drafts/readiness.js +9 -2
  78. package/dist/slices/play-drafts/schema.js +2 -1
  79. package/dist/slices/rebuild/narration-words.js +76 -0
  80. package/dist/slices/rebuild/recipe-model.js +1 -0
  81. package/dist/slices/rebuild/recipe-reviews.js +44 -23
  82. package/dist/slices/rebuild/recovery-repo.js +0 -20
  83. package/dist/slices/rebuild/recovery-selection.js +14 -3
  84. package/dist/slices/rebuild/recovery.js +5 -2
  85. package/dist/slices/rebuild/remake-outdated.js +27 -0
  86. package/dist/slices/rebuild/repo.js +7 -2
  87. package/dist/slices/rebuild/review-redo.js +12 -0
  88. package/dist/slices/rebuild/runtime-agent-review.js +121 -0
  89. package/dist/slices/rebuild/runtime-export-inputs.js +26 -11
  90. package/dist/slices/rebuild/runtime-review.js +12 -2
  91. package/dist/slices/rebuild/runtime-run.js +11 -2
  92. package/dist/slices/rebuild/runtime-store.js +32 -5
  93. package/dist/slices/reruns/index.js +2 -1
  94. package/dist/slices/research/run.js +2 -1
  95. package/dist/slices/reviews/external-model.js +21 -0
  96. package/dist/slices/reviews/external-release.js +38 -0
  97. package/dist/slices/reviews/external-repo.js +109 -0
  98. package/dist/slices/reviews/external-rows.js +68 -0
  99. package/dist/slices/reviews/external-waiting.js +75 -0
  100. package/dist/slices/reviews/holds.js +144 -0
  101. package/dist/slices/reviews/model.js +14 -0
  102. package/dist/slices/reviews/repo.js +27 -10
  103. package/dist/slices/reviews/rules.js +5 -3
  104. package/dist/slices/reviews/verdict.js +18 -8
  105. package/dist/slices/revisions/repo.js +3 -0
  106. package/dist/slices/revisions/schema.js +1 -0
  107. package/dist/slices/schedules/agenda.js +47 -17
  108. package/dist/slices/schedules/prepare.js +6 -0
  109. package/dist/slices/schedules/schema.js +13 -1
  110. package/dist/slices/storage/consequences.js +27 -0
  111. package/dist/slices/storage/portable.js +3 -0
  112. package/dist/slices/storage/project-files.js +66 -0
  113. package/dist/slices/studio/settings.js +0 -35
  114. package/dist/slices/studio/stats.js +2 -34
  115. package/dist/slices/subtitles/prepare.js +2 -1
  116. package/dist/slices/uploads/repo.js +12 -6
  117. package/dist/slices/uploads/rules.js +5 -0
  118. package/dist/slices/video/write-export.js +1 -1
  119. package/dist/slices/video/write-subtitles.js +1 -1
  120. package/dist/tutorials/Agents-MCP.md +168 -0
  121. package/dist/tutorials/Calendar.md +2 -1
  122. package/dist/tutorials/First-Launch-and-Welcome.md +1 -0
  123. package/dist/tutorials/Home-and-Projects.md +16 -9
  124. package/dist/tutorials/Home.md +1 -0
  125. package/dist/tutorials/Notifications.md +25 -6
  126. package/dist/tutorials/Project-Page.md +8 -2
  127. package/dist/tutorials/Publishing-to-YouTube.md +1 -0
  128. package/dist/tutorials/Recovery-and-Retries.md +17 -3
  129. package/dist/tutorials/Reviews-and-Checkpoints.md +22 -5
  130. package/dist/tutorials/Schedules.md +1 -1
  131. package/dist/tutorials/Settings-Reference.md +15 -0
  132. package/dist/tutorials/Troubleshooting.md +3 -1
  133. package/dist/tutorials/Your-First-Short.md +1 -0
  134. package/dist/tutorials/_Sidebar.md +1 -0
  135. package/dist/web/assets/{index-VbpUYLAp.css → index-BKbZIxAq.css} +1 -1
  136. package/dist/web/assets/index-Drth_MfQ.js +325 -0
  137. package/dist/web/assets/{pdf-C6KhxeKQ.js → pdf-CQFUyijy.js} +1 -1
  138. package/dist/web/index.html +2 -2
  139. package/package.json +1 -1
  140. package/dist/slices/studio/performance.js +0 -83
  141. package/dist/slices/studio/report-stats.js +0 -39
  142. package/dist/slices/studio/report.js +0 -172
  143. package/dist/web/assets/index-Dvz8bsiD.js +0 -325
@@ -1,5 +1,6 @@
1
1
  import { z } from "zod";
2
- import { stageKinds, stageStates } from "../../kernel/pipeline.js";
2
+ import { failureId, stageKinds, stageStates } from "../../kernel/pipeline.js";
3
+ import { admittedInItsState, parkedOnReview } from "./admitted-work.js";
3
4
  import { formats, stageSources } from "./model.js";
4
5
  import { runConfigSchema } from "./schema.js";
5
6
  export { runConfigSchema, runDraftSchema } from "./schema.js";
@@ -18,6 +19,9 @@ const standingRow = z.object({
18
19
  state: z.enum(stageStates),
19
20
  progress_current: z.number().nullable(),
20
21
  progress_total: z.number().nullable(),
22
+ source: z.enum(stageSources),
23
+ admitted: z.number(),
24
+ parked: z.number(),
21
25
  });
22
26
  const stageRow = z.object({
23
27
  id: z.string(),
@@ -33,21 +37,26 @@ const stageRow = z.object({
33
37
  finished_at: z.string().nullable(),
34
38
  failure_kind: z.string().nullable().optional(),
35
39
  retry_at: z.string().nullable().optional(),
40
+ admitted: z.number(),
41
+ parked: z.number(),
36
42
  });
37
43
  export function insertProject(db, project) {
38
44
  db.prepare("INSERT INTO projects (id, title, format, config, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)").run(project.id, project.title, project.format, JSON.stringify(project.config), project.createdAt, project.updatedAt);
39
45
  if (project.paused === true)
40
46
  setProjectPaused(db, project.id, true, project.updatedAt);
41
47
  }
42
- export function insertStage(db, stage) {
43
- db.prepare("INSERT INTO stages (id, project_id, kind, source, state, attempt_count) VALUES (?, ?, ?, ?, ?, ?)").run(stage.id, stage.projectId, stage.kind, stage.source, stage.state, stage.attemptCount);
44
- }
45
48
  // A project in the trash (`project_trash`, Settings → Trash) is not found by any of these
46
49
  // reads: it is listed nowhere, opens nowhere and takes no action until it is restored.
47
50
  // `slices/trash` and `slices/storage/delete-project.ts` read the table directly.
48
51
  export function liveProject(alias = "projects", column = "id") {
49
52
  return `NOT EXISTS (SELECT 1 FROM project_trash WHERE project_trash.project_id = ${alias}.${column})`;
50
53
  }
54
+ // The clause and its parameter that limit a query to one project, when `projectId` is given.
55
+ export function oneProject(alias, projectId) {
56
+ return projectId === undefined
57
+ ? { sql: "", params: [] }
58
+ : { sql: ` AND ${alias}.project_id=?`, params: [projectId] };
59
+ }
51
60
  export function projectTrashed(db, id) {
52
61
  return db.prepare("SELECT 1 FROM project_trash WHERE project_id = ?").get(id) !== undefined;
53
62
  }
@@ -76,12 +85,12 @@ export function listProjects(db) {
76
85
  // What 07 Projects needs of every project's stages and no more: the state each one is in
77
86
  // and how far through itself a running one is. One statement for the whole list rather
78
87
  // than `stagesOf` per row, and five columns rather than the whole stage table, because
79
- // the screen shows a status word and a meter and reads nothing else from a stage.
80
- export function stageStandingsByProject(db) {
88
+ // the screen shows a status word and a meter and reads nothing else from a stage. One project's
89
+ // alone when `projectId` is given.
90
+ export function stageStandingsByProject(db, projectId) {
81
91
  const grouped = new Map();
82
- const rows = db
83
- .prepare("SELECT project_id, kind, state, progress_current, progress_total FROM stages ORDER BY rowid")
84
- .all();
92
+ const select = `SELECT project_id, kind, state, progress_current, progress_total, source, ${admittedInItsState("stages")} AS admitted, ${parkedOnReview("stages")} AS parked FROM stages${projectId === undefined ? "" : " WHERE project_id=?"} ORDER BY rowid`;
93
+ const rows = projectId === undefined ? db.prepare(select).all() : db.prepare(select).all(projectId);
85
94
  for (const row of rows) {
86
95
  const parsed = standingRow.parse(row);
87
96
  const standing = {
@@ -89,6 +98,9 @@ export function stageStandingsByProject(db) {
89
98
  state: parsed.state,
90
99
  progressCurrent: parsed.progress_current,
91
100
  progressTotal: parsed.progress_total,
101
+ source: parsed.source,
102
+ ...(parsed.admitted === 1 ? { admitted: true } : {}),
103
+ ...(parsed.parked === 1 ? { parked: true } : {}),
92
104
  };
93
105
  const seen = grouped.get(parsed.project_id);
94
106
  if (seen === undefined) {
@@ -100,30 +112,32 @@ export function stageStandingsByProject(db) {
100
112
  }
101
113
  return grouped;
102
114
  }
115
+ const failedRow = z.object({
116
+ project_id: z.string(),
117
+ kind: z.enum(stageKinds),
118
+ finished_at: z.string().nullable(),
119
+ });
120
+ // Each project's failed steps by their identity (`kernel/pipeline.ts` failureId), in the
121
+ // run's order, so a failure is told apart from one before it.
122
+ export function stepFailuresByProject(db) {
123
+ const failures = new Map();
124
+ for (const row of db
125
+ .prepare("SELECT project_id, kind, finished_at FROM stages WHERE state='failed' ORDER BY rowid")
126
+ .all()) {
127
+ const failed = failedRow.parse(row);
128
+ failures.set(failed.project_id, [
129
+ ...(failures.get(failed.project_id) ?? []),
130
+ failureId(failed.kind, failed.finished_at),
131
+ ]);
132
+ }
133
+ return failures;
134
+ }
103
135
  export function stagesOf(db, projectId) {
104
136
  return db
105
- .prepare("SELECT * FROM stages WHERE project_id = ? ORDER BY rowid")
137
+ .prepare(`SELECT *, ${admittedInItsState("stages")} AS admitted, ${parkedOnReview("stages")} AS parked FROM stages WHERE project_id = ? ORDER BY rowid`)
106
138
  .all(projectId)
107
139
  .map((row) => toStage(stageRow.parse(row)));
108
140
  }
109
- // The runner's claim: one statement, so two callers cannot both find the stage pending.
110
- export function claimStage(db, stageId, at) {
111
- const result = db
112
- .prepare("UPDATE stages SET state = 'running', started_at = ?, finished_at = NULL, failure_reason = NULL WHERE id = ? AND state = 'pending' AND NOT EXISTS (SELECT 1 FROM project_controls WHERE project_id = stages.project_id AND paused = 1)")
113
- .run(at, stageId);
114
- return Number(result.changes) === 1;
115
- }
116
- export function finishStage(db, stageId, state, failureReason, at) {
117
- db.prepare("UPDATE stages SET state = ?, failure_reason = ?, retry_at = NULL, finished_at = ? WHERE id = ?").run(state, failureReason, state === "pending" ? null : at, stageId);
118
- }
119
- // A stage put back to `pending` by a re-run, a cascade or a retry starts again from a clean row
120
- // - no error text, no progress from the run before it, and a fresh attempt budget.
121
- export function resetStage(db, stageId) {
122
- db.prepare("UPDATE stages SET state = 'pending', failure_reason = NULL, failure_kind = NULL, retry_at = NULL, attempt_count = 0, progress_current = NULL, progress_total = NULL, started_at = NULL, finished_at = NULL WHERE id = ?").run(stageId);
123
- }
124
- export function setStageProgress(db, stageId, current, total) {
125
- db.prepare("UPDATE stages SET progress_current = ?, progress_total = ? WHERE id = ?").run(current, total, stageId);
126
- }
127
141
  function toProject(row) {
128
142
  return {
129
143
  id: row.id,
@@ -141,7 +155,9 @@ export function projectPaused(db, projectId) {
141
155
  ?.paused === 1);
142
156
  }
143
157
  export function setProjectPaused(db, projectId, paused, at) {
144
- db.prepare("INSERT INTO project_controls (project_id, paused) VALUES (?, ?) ON CONFLICT(project_id) DO UPDATE SET paused = excluded.paused").run(projectId, paused ? 1 : 0);
158
+ db.prepare(`INSERT INTO project_controls (project_id, paused, pauses) VALUES (?1, ?2, ?2)
159
+ ON CONFLICT(project_id) DO UPDATE SET pauses = pauses + (excluded.paused = 1 AND paused = 0),
160
+ paused = excluded.paused`).run(projectId, paused ? 1 : 0);
145
161
  db.prepare("UPDATE projects SET updated_at = ? WHERE id = ?").run(at, projectId);
146
162
  }
147
163
  export function updateProjectConfig(db, projectId, config, at) {
@@ -171,5 +187,7 @@ function toStage(row) {
171
187
  ? {}
172
188
  : { failureKind: row.failure_kind }),
173
189
  ...(row.retry_at === null || row.retry_at === undefined ? {} : { retryAt: row.retry_at }),
190
+ ...(row.admitted === 1 ? { admitted: true } : {}),
191
+ ...(row.parked === 1 ? { parked: true } : {}),
174
192
  };
175
193
  }
@@ -172,6 +172,7 @@ export const runDraftSchema = z.object({
172
172
  provider: z.string(),
173
173
  model: z.string(),
174
174
  thinking: z.enum(thinkingModes).optional(),
175
+ reviewer: z.literal("agent").optional(),
175
176
  retries: z.number().optional(),
176
177
  stages: z.partialRecord(z.enum(reviewStages), z.object({ mode: z.enum(reviewModes), prompt: z.string().optional() })),
177
178
  })
@@ -0,0 +1,21 @@
1
+ export function insertStage(db, stage) {
2
+ db.prepare("INSERT INTO stages (id, project_id, kind, source, state, attempt_count) VALUES (?, ?, ?, ?, ?, ?)").run(stage.id, stage.projectId, stage.kind, stage.source, stage.state, stage.attemptCount);
3
+ }
4
+ // The runner's claim: one statement, so two callers cannot both find the stage pending.
5
+ export function claimStage(db, stageId, at) {
6
+ const result = db
7
+ .prepare("UPDATE stages SET state = 'running', started_at = ?, finished_at = NULL, failure_reason = NULL WHERE id = ? AND state = 'pending' AND NOT EXISTS (SELECT 1 FROM project_controls WHERE project_id = stages.project_id AND paused = 1)")
8
+ .run(at, stageId);
9
+ return Number(result.changes) === 1;
10
+ }
11
+ export function finishStage(db, stageId, state, failureReason, at) {
12
+ db.prepare("UPDATE stages SET state = ?, failure_reason = ?, retry_at = NULL, finished_at = ? WHERE id = ?").run(state, failureReason, state === "pending" ? null : at, stageId);
13
+ }
14
+ // A stage put back to `pending` by a re-run, a cascade or a retry starts again from a clean row
15
+ // - no error text, no progress from the run before it, and a fresh attempt budget.
16
+ export function resetStage(db, stageId) {
17
+ db.prepare("UPDATE stages SET state = 'pending', failure_reason = NULL, failure_kind = NULL, retry_at = NULL, attempt_count = 0, progress_current = NULL, progress_total = NULL, started_at = NULL, finished_at = NULL WHERE id = ?").run(stageId);
18
+ }
19
+ export function setStageProgress(db, stageId, current, total) {
20
+ db.prepare("UPDATE stages SET progress_current = ?, progress_total = ? WHERE id = ?").run(current, total, stageId);
21
+ }
@@ -15,8 +15,9 @@ import { attachStagedFile, storeText } from "../storage/staging.js";
15
15
  import { releaseStagedFile } from "../storage/staging-refs.js";
16
16
  import { usesAmbientBed } from "../video/ambient-bed.js";
17
17
  import { sourceOf, } from "./model.js";
18
- import { insertProject, insertStage } from "./repo.js";
18
+ import { insertProject } from "./repo.js";
19
19
  import { usesPronunciationGlossary, usesReference } from "./rules.js";
20
+ import { insertStage } from "./stage-writes.js";
20
21
  import { detectSlots, render } from "./substitute.js";
21
22
  // Provide → `provided` with its output attached; Off → `skipped`;
22
23
  // everything else → `pending`.
@@ -0,0 +1,110 @@
1
+ import { waitsForAnswer } from "../../kernel/runner/graph.js";
2
+ // One word per state, everywhere a state is said: the MCP tools, and the web's lists and pages
3
+ // (`web/src/lib/state-words.ts` projectStateOf, Home's Needs you, the project's next action).
4
+ // Browser-safe.
5
+ // A project that has not started yet is Queued: it waits its turn, not for the person.
6
+ export const projectStateWords = {
7
+ running: "Running",
8
+ paused: "Paused",
9
+ pending: "Queued",
10
+ failed: "Failed",
11
+ partial: "Done with problems",
12
+ done: "Done",
13
+ canceled: "Canceled",
14
+ };
15
+ // A step of a run. A skipped step is one the run's settings turned off.
16
+ export const stageStateWords = {
17
+ pending: "Not started",
18
+ running: "Running",
19
+ done: "Done",
20
+ provided: "Provided",
21
+ failed: "Failed",
22
+ canceled: "Canceled",
23
+ skipped: "Off",
24
+ };
25
+ // What a held run waits for, on a step or on a project: the person's review at a checkpoint,
26
+ // an External agent's review, Continue the run once a restart stopped it, its scheduled day,
27
+ // or nobody once its leftover steps were set aside on purpose.
28
+ export const heldWord = "Waiting for your review";
29
+ export const waitingForAgentWord = "Waiting for the agent";
30
+ export const stoppedWord = "Stopped";
31
+ export const preparedWord = "Prepared";
32
+ export const keptAsIsWord = "Kept as is";
33
+ export const heldStateWords = [
34
+ heldWord,
35
+ waitingForAgentWord,
36
+ stoppedWord,
37
+ preparedWord,
38
+ keptAsIsWord,
39
+ ];
40
+ // A run that ran and is held: a waiting project with some of its run done (`progress` is the
41
+ // run's share done, `progressOf`) or a step failed, or one stopped or parked on an agent's review
42
+ // before it got going. One with none done that waits for neither is Queued, waiting its turn.
43
+ export function heldRun(project) {
44
+ return (project.status === "pending" &&
45
+ (project.progress > 0 ||
46
+ project.stepFailed === true ||
47
+ project.stopped === true ||
48
+ project.waitingForAgent === true));
49
+ }
50
+ // Whether the person is the one it waits for (to approve, or to Continue the run): not when it
51
+ // is Kept as is, waits for an agent or for its scheduled day.
52
+ function heldForPerson(project) {
53
+ return project.setAside !== true && project.waitingForAgent !== true && project.prepared !== true;
54
+ }
55
+ export function heldStateWord(project) {
56
+ if (project.setAside === true)
57
+ return keptAsIsWord;
58
+ if (project.waitingForAgent === true)
59
+ return waitingForAgentWord;
60
+ if (project.stopped === true)
61
+ return stoppedWord;
62
+ if (project.prepared === true)
63
+ return preparedWord;
64
+ return heldWord;
65
+ }
66
+ // A waiting run parked on an External agent's review, which goes on by itself once the agent
67
+ // answers. Kept as is wins, as in `heldStateWord`.
68
+ export function parkedOnAgent(project) {
69
+ return (project.status === "pending" && project.waitingForAgent === true && project.setAside !== true);
70
+ }
71
+ // Going on by itself: running, or parked on an agent's review. Home's Running now and the
72
+ // Projects Running filter.
73
+ export function runningNow(project) {
74
+ return project.status === "running" || parkedOnAgent(project);
75
+ }
76
+ // Started and waiting its turn: pending with none of its run done, not stopped and not parked
77
+ // on an agent's review. Projects' Queued filter and Home's queued line under Running now.
78
+ export function queuedRun(project) {
79
+ return project.status === "pending" && !heldRun(project);
80
+ }
81
+ // An uploaded project already reads "done" from the projects route (`uploadedAndSettled`).
82
+ export function projectStateWord(project) {
83
+ return heldRun(project) ? heldStateWord(project) : projectStateWords[project.status];
84
+ }
85
+ // A run that ran and now waits for the person, held or paused: Home's Needs you, the Projects
86
+ // "Needs you" filter and its waiting-first sort.
87
+ export function waitsForPerson(project) {
88
+ return project.status === "paused" || (heldRun(project) && heldForPerson(project));
89
+ }
90
+ // A step failed and the run reads Pending by the wait beside it, nothing else running
91
+ // (`derive`): what `stepFailed` says on the Projects list, a project's page and the calendar.
92
+ // Unlike `stepFailedWhileWaiting`, false while a third step still runs.
93
+ export function readsByWaitBesideFailure(status, stages) {
94
+ return status === "pending" && stages.some((stage) => stage.state === "failed");
95
+ }
96
+ // A step failed while another waits for the agent, the person or its day (`waitsForAnswer`),
97
+ // whether or not a third still runs: what "Step failed" is told for (`notifications/rules.ts`
98
+ // runNotices) and While you were away counts. A failed step in a run with nothing waiting is
99
+ // told by the run's own end notice. Unlike `readsByWaitBesideFailure`, true while a third step
100
+ // still runs.
101
+ export function stepFailedWhileWaiting(status, stages) {
102
+ return ((status === "running" || status === "pending") &&
103
+ stages.some((stage) => stage.state === "failed") &&
104
+ waitsForAnswer(stages));
105
+ }
106
+ // A run listed as failed, on Home's Needs you and under the Projects Failed filter: it failed, or
107
+ // a step failed while the run still waits for the agent or the person (`stepFailed`).
108
+ export function failedRun(project) {
109
+ return project.status === "failed" || project.stepFailed === true;
110
+ }
@@ -0,0 +1,233 @@
1
+ import { z } from "zod";
2
+ import { liveProject } from "../admission/repo.js";
3
+ import { releaseCalendar } from "../studio/calendar.js";
4
+ export function rowsOf(rows, source, skip, entries) {
5
+ return rows.flatMap((value) => {
6
+ try {
7
+ return entries(value);
8
+ }
9
+ catch (error) {
10
+ skip(source, value, error);
11
+ return [];
12
+ }
13
+ });
14
+ }
15
+ const stageRow = z.object({
16
+ project_id: z.string(),
17
+ title: z.string(),
18
+ kind: z.string(),
19
+ failure_reason: z.string().nullable(),
20
+ finished_at: z.string().nullable(),
21
+ head: z.string().nullable(),
22
+ retrying: z.number(),
23
+ });
24
+ // A narration chunk that stopped matching its text is recorded again by Slopify itself
25
+ // (`rebuild/narration-retry.ts`): pending (also while it waits for a busy project), or started
26
+ // after the failure it answers. A short whose pick stopped fails the video stage the same way.
27
+ export function failedSteps(db, skip) {
28
+ const rows = db
29
+ .prepare(`SELECT s.project_id, p.title, s.kind, s.failure_reason, s.finished_at, h.revision_id AS head,
30
+ (s.kind IN ('audio','video') AND EXISTS (SELECT 1 FROM narration_retries n WHERE n.project_id=s.project_id
31
+ AND (n.state='pending' OR (n.state='started' AND n.updated_at>=COALESCE(s.finished_at,''))))) AS retrying
32
+ FROM stages s
33
+ JOIN projects p ON p.id=s.project_id
34
+ LEFT JOIN project_heads h ON h.project_id=s.project_id
35
+ WHERE s.state='failed' AND ${liveProject("s", "project_id")}`)
36
+ .all();
37
+ return rowsOf(rows, "failed steps", skip, (value) => {
38
+ const row = stageRow.parse(value);
39
+ const retrying = row.retrying === 1;
40
+ return [
41
+ {
42
+ kind: "failed-step",
43
+ since: row.finished_at ?? "",
44
+ projectId: row.project_id,
45
+ title: row.title,
46
+ stage: row.kind,
47
+ itemKey: null,
48
+ reason: row.failure_reason ??
49
+ "The step failed without saying why. Try it again with retry_stage.",
50
+ autoRetryInFlight: retrying,
51
+ agentCanAct: !retrying,
52
+ ids: {
53
+ id: row.project_id,
54
+ kind: row.kind,
55
+ ...(row.head === null ? {} : { baseRevisionId: row.head }),
56
+ },
57
+ tools: [
58
+ "get_project",
59
+ "read_narration_chunks",
60
+ "get_revision",
61
+ "save_revision",
62
+ "preview_rebuild",
63
+ "start_rebuild",
64
+ "resume_project",
65
+ "retry_stage",
66
+ "soften_stage",
67
+ ],
68
+ },
69
+ ];
70
+ });
71
+ }
72
+ const runRow = z.object({
73
+ id: z.string(),
74
+ title: z.string(),
75
+ updated_at: z.string(),
76
+ head: z.string().nullable(),
77
+ });
78
+ // A run nothing moves on but Continue the run (resume_project), which the agent may give once
79
+ // nothing in the project still needs fixing: the runs `which` picks among the live projects.
80
+ function runsToContinue(db, skip, run) {
81
+ const rows = db
82
+ .prepare(`SELECT p.id, p.title, p.updated_at, h.revision_id AS head FROM projects p
83
+ LEFT JOIN project_heads h ON h.project_id=p.id
84
+ WHERE ${liveProject("p")} AND ${run.which}`)
85
+ .all(...run.params);
86
+ return rowsOf(rows, run.source, skip, (value) => {
87
+ const row = runRow.parse(value);
88
+ return [
89
+ {
90
+ kind: run.kind,
91
+ since: row.updated_at,
92
+ projectId: row.id,
93
+ title: row.title,
94
+ stage: null,
95
+ itemKey: null,
96
+ reason: run.reason,
97
+ autoRetryInFlight: false,
98
+ agentCanAct: true,
99
+ ids: { id: row.id, ...(row.head === null ? {} : { baseRevisionId: row.head }) },
100
+ tools: ["get_project", "resume_project"],
101
+ },
102
+ ];
103
+ });
104
+ }
105
+ // No pause reason is stored: any paused run is the agent's to continue, whoever paused it.
106
+ export function pausedRuns(db, skip) {
107
+ return runsToContinue(db, skip, {
108
+ kind: "paused-run",
109
+ source: "paused runs",
110
+ which: "EXISTS (SELECT 1 FROM project_controls c WHERE c.project_id=p.id AND c.paused=1)",
111
+ params: [],
112
+ reason: "Paused: nothing runs until Continue the run (resume_project). Read get_project first: continue it once nothing it shows still needs fixing.",
113
+ });
114
+ }
115
+ // A run a restart stopped before it finished (`reviews/holds.ts`): like a paused one, nothing
116
+ // goes on until Continue the run, and the agent may give it.
117
+ export function stoppedRuns(db, stopped, skip) {
118
+ return runsToContinue(db, skip, {
119
+ kind: "stopped-run",
120
+ source: "stopped runs",
121
+ which: "p.id IN (SELECT value FROM json_each(?))",
122
+ params: [JSON.stringify([...stopped])],
123
+ reason: "Stopped before it finished, as a restart leaves a run: nothing goes on until Continue the run (resume_project). Read get_project first: continue it once nothing it shows still needs fixing.",
124
+ });
125
+ }
126
+ const topicsRow = z.object({
127
+ id: z.string(),
128
+ name: z.string(),
129
+ held: z.number(),
130
+ since: z.string(),
131
+ });
132
+ export function heldTopics(db, skip) {
133
+ const rows = db
134
+ .prepare(`SELECT s.id, s.name, count(t.id) AS held, min(t.created_at) AS since FROM schedules s
135
+ JOIN schedule_topics t ON t.schedule_id=s.id AND t.state='held'
136
+ WHERE s.deleted_at IS NULL GROUP BY s.id, s.name`)
137
+ .all();
138
+ return rowsOf(rows, "held topics", skip, (value) => {
139
+ const row = topicsRow.parse(value);
140
+ return [
141
+ {
142
+ kind: "held-topics",
143
+ since: row.since,
144
+ projectId: null,
145
+ title: row.name,
146
+ stage: null,
147
+ itemKey: null,
148
+ reason: `${String(row.held)} suggested ${row.held === 1 ? "topic waits" : "topics wait"} for approval.`,
149
+ autoRetryInFlight: false,
150
+ agentCanAct: true,
151
+ ids: { id: row.id },
152
+ tools: ["list_held_topics", "approve_topics", "reject_topics", "approve_all_topics"],
153
+ },
154
+ ];
155
+ });
156
+ }
157
+ // Calendar → Releases' "Late · upload now": the agent reports it; uploading is the person's.
158
+ export function lateUploads(deps, now) {
159
+ return releaseCalendar(deps, { now, weeks: 2, candidates: [] }).entries.flatMap((entry) => entry.project === null
160
+ ? []
161
+ : entry.items
162
+ .filter((item) => item.state === "late")
163
+ .map((item) => {
164
+ const project = entry.project;
165
+ return {
166
+ kind: "late-upload",
167
+ since: item.uploadBy ?? item.at ?? entry.at,
168
+ projectId: project?.id ?? null,
169
+ title: project?.title ?? "",
170
+ stage: "video",
171
+ itemKey: item.short === 0 ? "video" : `short:${String(item.short)}`,
172
+ reason: `Late · upload now: "${item.title}" was due to be uploaded by ${item.uploadBy ?? "now"} for its release at ${item.at ?? entry.at}. Uploading is the person's: report it.`,
173
+ autoRetryInFlight: false,
174
+ agentCanAct: false,
175
+ ids: { id: project?.id ?? "", short: String(item.short) },
176
+ tools: ["get_upload_pack", "get_project_releases"],
177
+ };
178
+ }));
179
+ }
180
+ const videoRow = z.object({
181
+ project_id: z.string(),
182
+ title: z.string(),
183
+ short: z.number(),
184
+ recorded_at: z.string(),
185
+ finish_state: z.string(),
186
+ finish_message: z.string().nullable(),
187
+ comment_state: z.string(),
188
+ comment_message: z.string().nullable(),
189
+ ab_state: z.string(),
190
+ ab_message: z.string().nullable(),
191
+ ab_at: z.string().nullable(),
192
+ });
193
+ // What the Studio extension could not finish on YouTube's side: the agent reports it.
194
+ export function extensionFailures(db, skip) {
195
+ const rows = db
196
+ .prepare(`SELECT v.project_id, p.title, v.short, v.recorded_at, v.finish_state, v.finish_message,
197
+ v.comment_state, v.comment_message, v.ab_state, v.ab_message, v.ab_at
198
+ FROM youtube_videos v JOIN projects p ON p.id=v.project_id
199
+ WHERE (v.finish_state='failed' OR v.comment_state='failed' OR v.ab_state='failed')
200
+ AND ${liveProject("v", "project_id")}`)
201
+ .all();
202
+ return rowsOf(rows, "extension failures", skip, (value) => {
203
+ const row = videoRow.parse(value);
204
+ const touches = [
205
+ row.finish_state === "failed"
206
+ ? {
207
+ what: "The finishing touches in YouTube Studio (end screen, captions, related video)",
208
+ message: row.finish_message,
209
+ at: row.recorded_at,
210
+ }
211
+ : undefined,
212
+ row.comment_state === "failed"
213
+ ? { what: "The pinned comment", message: row.comment_message, at: row.recorded_at }
214
+ : undefined,
215
+ row.ab_state === "failed"
216
+ ? { what: "The A/B test", message: row.ab_message, at: row.ab_at ?? row.recorded_at }
217
+ : undefined,
218
+ ].filter((touch) => touch !== undefined);
219
+ return touches.map((touch) => ({
220
+ kind: "extension-failure",
221
+ since: touch.at,
222
+ projectId: row.project_id,
223
+ title: row.title,
224
+ stage: "video",
225
+ itemKey: row.short === 0 ? "video" : `short:${String(row.short)}`,
226
+ reason: `${touch.what} failed: ${touch.message ?? "the extension gave no reason."} Fixing it is the extension's or the person's: report it.`,
227
+ autoRetryInFlight: false,
228
+ agentCanAct: false,
229
+ ids: { id: row.project_id, short: String(row.short) },
230
+ tools: ["get_youtube_videos"],
231
+ }));
232
+ });
233
+ }