@bendyline/gilde 0.1.22 → 0.1.24

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 (58) hide show
  1. package/data/connector-types/ai/airtable-records/versions/1.0.1/manifest.json +81 -0
  2. package/data/connector-types/bl/bluesky-posts/versions/1.0.0/manifest.json +7 -1
  3. package/data/connector-types/bl/bluesky-posts/versions/1.0.1/manifest.json +93 -0
  4. package/data/connector-types/ca/calendar-google/versions/1.0.0/manifest.json +14 -0
  5. package/data/connector-types/gi/github-issues/versions/1.0.2/manifest.json +82 -0
  6. package/data/connector-types/gi/github-pulls/versions/1.0.1/manifest.json +52 -0
  7. package/data/connector-types/gi/github-releases/versions/1.0.1/manifest.json +86 -0
  8. package/data/connector-types/gi/github-wiki/versions/1.0.1/manifest.json +56 -0
  9. package/data/connector-types/in/instagram-media/manifest.json +1 -1
  10. package/data/connector-types/in/instagram-media/versions/1.0.0/manifest.json +29 -3
  11. package/data/connector-types/index.json +1 -1
  12. package/data/connector-types/li/linear-issues/versions/1.0.1/manifest.json +70 -0
  13. package/data/connector-types/li/linkedin-posts/manifest.json +16 -0
  14. package/data/connector-types/li/linkedin-posts/versions/1.0.0/manifest.json +54 -0
  15. package/data/connector-types/ma/mail-gmail/versions/1.0.0/manifest.json +14 -0
  16. package/data/connector-types/ma/mail-imap/versions/1.0.1/manifest.json +71 -0
  17. package/data/connector-types/ma/mail-microsoft365/versions/1.0.0/manifest.json +14 -1
  18. package/data/connector-types/ma/mail-outlook/versions/1.0.0/manifest.json +14 -1
  19. package/data/connector-types/x-/x-posts/manifest.json +1 -1
  20. package/data/connector-types/x-/x-posts/versions/1.0.0/manifest.json +30 -4
  21. package/data/craftbook-templates/an/anniversary-cut/manifest.json +3 -1
  22. package/data/craftbook-templates/an/anniversary-cut/versions/1.0.2/craftbook.json +212 -0
  23. package/data/craftbook-templates/an/anniversary-cut/versions/1.0.2/test.json +157 -0
  24. package/data/craftbook-templates/au/audiobook-master-pack/manifest.json +3 -1
  25. package/data/craftbook-templates/au/audiobook-master-pack/versions/1.0.2/craftbook.json +197 -0
  26. package/data/craftbook-templates/au/audiobook-master-pack/versions/1.0.2/test.json +221 -0
  27. package/data/craftbook-templates/dr/draft-social-post/logo.webp +0 -0
  28. package/data/craftbook-templates/dr/draft-social-post/manifest.json +3 -1
  29. package/data/craftbook-templates/dr/draft-social-post/versions/1.0.0/craftbook.json +8 -6
  30. package/data/craftbook-templates/index.json +1 -1
  31. package/data/craftbook-templates/re/reception-report/logo.webp +0 -0
  32. package/data/craftbook-templates/re/reception-report/manifest.json +1 -0
  33. package/data/craftbook-templates/so/social-digest/logo.webp +0 -0
  34. package/data/craftbook-templates/so/social-digest/manifest.json +1 -0
  35. package/data/project-types/ca/caregiving-binder/versions/1.1.0/about.md +8 -0
  36. package/data/project-types/ca/caregiving-binder/versions/1.1.0/care.json +4 -0
  37. package/data/project-types/ca/caregiving-binder/versions/1.1.0/manifest.json +178 -0
  38. package/data/project-types/ca/caregiving-binder/versions/1.1.0/meds.json +8 -0
  39. package/data/project-types/ca/caregiving-binder/versions/1.1.0/mission.md +8 -0
  40. package/data/project-types/ca/caregiving-binder/versions/1.1.0/pages/dashboard/index.html +265 -0
  41. package/data/project-types/ch/checkers/versions/1.2.0/about.md +9 -0
  42. package/data/project-types/ch/checkers/versions/1.2.0/game.json +77 -0
  43. package/data/project-types/ch/checkers/versions/1.2.0/manifest.json +167 -0
  44. package/data/project-types/ch/checkers/versions/1.2.0/mission.md +9 -0
  45. package/data/project-types/ch/checkers/versions/1.2.0/pages/board/index.html +1157 -0
  46. package/data/project-types/fl/flashcards/versions/1.1.0/about.md +8 -0
  47. package/data/project-types/fl/flashcards/versions/1.1.0/deck.json +7 -0
  48. package/data/project-types/fl/flashcards/versions/1.1.0/manifest.json +168 -0
  49. package/data/project-types/fl/flashcards/versions/1.1.0/mission.md +6 -0
  50. package/data/project-types/fl/flashcards/versions/1.1.0/pages/review/index.html +476 -0
  51. package/data/project-types/im/image-feed/versions/1.0.0/pages/gallery/index.html +2 -1
  52. package/data/project-types/index.json +1 -1
  53. package/data/project-types/so/social-feed/manifest.json +1 -0
  54. package/data/project-types/so/social-feed/versions/1.0.0/about.md +2 -2
  55. package/data/project-types/so/social-feed/versions/1.0.0/manifest.json +6 -0
  56. package/data/project-types/so/social-feed/versions/1.0.0/pages/dashboard/index.html +4 -2
  57. package/package.json +1 -1
  58. package/schemas/connector-type-version.schema.json +31 -0
@@ -14,6 +14,11 @@
14
14
  "title": "Sync mentions of the account (costs extra paid reads)",
15
15
  "default": false
16
16
  },
17
+ "allowPublish": {
18
+ "type": "boolean",
19
+ "title": "Allow this project's gezels to queue posts for publishing (each publish still needs your consent)",
20
+ "default": false
21
+ },
17
22
  "syncIntervalHours": {
18
23
  "type": "number",
19
24
  "title": "Hours between post syncs (X reads are paid — keep this generous)",
@@ -35,6 +40,11 @@
35
40
  "minimum": 0.25,
36
41
  "maximum": 720
37
42
  },
43
+ "metricsHistory": {
44
+ "type": "boolean",
45
+ "title": "Record daily engagement history records",
46
+ "default": true
47
+ },
38
48
  "statsIntervalHours": {
39
49
  "type": "number",
40
50
  "title": "Hours between account-stats snapshots",
@@ -48,9 +58,25 @@
48
58
  "kind": "oauth2",
49
59
  "authorizeUrl": "https://x.com/i/oauth2/authorize",
50
60
  "tokenUrl": "https://api.x.com/2/oauth2/token",
51
- "scopes": "tweet.read users.read offline.access",
61
+ "scopes": "tweet.read users.read offline.access tweet.write",
52
62
  "clientIdEnv": "GEZEL_X_CLIENT_ID",
53
- "clientSecretEnv": "GEZEL_X_CLIENT_SECRET"
63
+ "clientSecretEnv": "GEZEL_X_CLIENT_SECRET",
64
+ "clientSetup": {
65
+ "providerLabel": "X (Twitter)",
66
+ "docsUrl": "https://developer.x.com/en/portal/dashboard",
67
+ "secretRequired": false,
68
+ "appTypeNote": "Create a free developer app and set its type to a Native/Public client — PKCE needs no client secret. Your API tier and read limits are your own.",
69
+ "redirectPort": 6241,
70
+ "redirectNote": "X matches callback URLs exactly. Register the URI shown here character-for-character.",
71
+ "steps": [
72
+ "Sign in at developer.x.com and create a project with an app — the free plan works for publishing.",
73
+ "In the app's settings, open 'User authentication settings' and click Set up.",
74
+ "Choose the 'Native App' (public client) type and enable OAuth 2.0.",
75
+ "Paste the redirect URI shown below into Callback URI / Redirect URL, character-for-character.",
76
+ "Fill the required website URL field with any page of yours and save.",
77
+ "Copy the OAuth 2.0 Client ID from the app's Keys and tokens page and paste it below — native apps use PKCE, so there is no secret to copy."
78
+ ]
79
+ }
54
80
  },
55
81
  "normalize": {
56
82
  "kind": "native"
@@ -58,9 +84,9 @@
58
84
  "actions": [
59
85
  {
60
86
  "name": "publish",
61
- "consentScope": "manual-export"
87
+ "consentScope": "social-publish"
62
88
  }
63
89
  ],
64
90
  "completeness": "window",
65
- "notes": "Syncs three things from the linked account: the account's own posts (text with links expanded, language, reply threading, plus like/repost/reply/quote/bookmark/view counts), mentions of the account when enabled, and a once-per-day account-stats record (followers, following, total posts). Be blunt about cost: X charges for API reads on every tier, and the free tier allows roughly 100 reads per month — that fits only the default generous intervals (posts every 4 hours, stats every 6, engagement refresh daily); for real day-to-day use you need the paid Basic tier or higher. Every read scope is interval-gated (syncIntervalHours for posts and mentions, statsIntervalHours for stats) so background syncing can never burn reads faster than configured, and mentions are OFF by default to conserve reads. Engagement counts on posts from the last metricsWindowDays are re-observed roughly every metricsRefreshHours as one extra page per refresh, updating existing files in place. The first sync captures up to 500 recent posts; older history is not backfilled. Publishing is manual-export only in this version: gezels can draft posts for review, but Gezel never posts to X on your behalf you copy a draft and publish it yourself. Requires your own X developer app: set GEZEL_X_CLIENT_ID (and GEZEL_X_CLIENT_SECRET for confidential apps) to enable linking."
91
+ "notes": "Syncs three things from the linked account: the account's own posts (text with links expanded, language, reply threading, plus like/repost/reply/quote/bookmark/view counts), mentions of the account when enabled, and a once-per-day account-stats record (followers, following, total posts). Be blunt about cost: X charges for API reads on every tier, and the free tier allows roughly 100 reads per month — that fits only the default generous intervals (posts every 4 hours, stats every 6, engagement refresh daily); for real day-to-day use you need the paid Basic tier or higher. Every read scope is interval-gated (syncIntervalHours for posts and mentions, statsIntervalHours for stats) so background syncing can never burn reads faster than configured, and mentions are OFF by default to conserve reads. Engagement counts on posts from the last metricsWindowDays are re-observed roughly every metricsRefreshHours as one extra page per refresh, updating existing files in place; each observation also appends a per-day metrics-history record per post (the engagement curve over time) unless metricsHistory is turned off. The first sync captures up to 500 recent posts; older history is not backfilled. Publishing works even on the free tier — X budgets writes separately at roughly 500 posts per month — and is off by default: a gezel can only draft posts until allowPublish is enabled on the binding, and every publish is individually consented through the daemon (night shift defers publishes to the outbox for daytime approval). Accounts linked before publishing existed must reconnect from the connector's settings to grant the new tweet.write scope. Requires your own X developer app: set GEZEL_X_CLIENT_ID (and GEZEL_X_CLIENT_SECRET for confidential apps) to enable linking."
66
92
  }
@@ -19,6 +19,8 @@
19
19
  },
20
20
  "logo": "logo.webp",
21
21
  "license": "MIT",
22
- "yankedVersions": [],
22
+ "yankedVersions": [
23
+ "1.0.1"
24
+ ],
23
25
  "workflow": "build-loop"
24
26
  }
@@ -0,0 +1,212 @@
1
+ {
2
+ "id": "anniversary-cut",
3
+ "name": "Anniversary Cut",
4
+ "description": "Build the highlight reel for the occasion: the archive queried for the person and the years that matter, clips chosen with stated reasons, assembled through the project's media pipeline with a run log, and delivered with a chapter list and a dedication — the reel that makes the birthday. Locks the occasion and the selection criteria FIRST (era coverage, the famous moments in, a length budget), then selects from the archive with no invented footage, then assembles, then writes the presentation notes. Use for birthday reels, anniversary videos, memorial montages, 'make a highlight reel', and milestone tributes.\n\nA gallery craftbook generated from an archetype spec. It runs\n`phase → (per-phase gate) → … → evaluate → (loop) → finish`. Each build\nphase that produces a checkable artifact is followed by a **runtime\ngate-checkpoint** — the runtime verifies the artifact and routes with no\nmodel turn, looping back to redo the phase on a miss. The final `evaluate`\nstep holds a static deliverable gate plus a reviewer QA pass. What it adds\nover the generic `build-loop`: a specialist role per phase, a\ndomain-correct ordering, and a concrete per-phase quality bar.\n\nPhases:\n\n1. Scope the occasion (filmarchivaris) — occasion, person, selection criteria and length budget locked → gated on `notes/scope.md` (markdown-notes)\n2. Select the clips (curator) — the cut list: archive clips chosen with reasons, in reel order → gated on `cutlist.json` (json)\n3. Assemble the reel (developer) — the reel rendered through the media pipeline, log on disk → gated on `renders/assembly-log.json` (json)\n4. Chapters and dedication (copywriter) — the chapter list, the dedication, how to play it on the day → gated on `report.md` (markdown-report)\n\nThe gates never advance with an unmet criterion, and loop back to the\nowning phase to fix named gaps.\n",
5
+ "entryStepId": "scope",
6
+ "triggers": [
7
+ "make a highlight reel",
8
+ "anniversary video",
9
+ "birthday montage",
10
+ "put together a tribute",
11
+ "clips of grandma over the years"
12
+ ],
13
+ "steps": [
14
+ {
15
+ "id": "scope",
16
+ "name": "Scope the occasion",
17
+ "description": "occasion, person, selection criteria and length budget locked",
18
+ "prompt": "Define the reel before pulling a single clip. Step 1: name the occasion and the person it honors, and read family.md so names and relationships are right. Step 2: read clips.json — the archive's indexed moments — and survey what exists for this person: which tapes, which years, which of the family-famous moments are on file. Step 3: lock the selection criteria — the reel covers the person's eras rather than clustering in one year, the moments the family always retells make the cut, every selection carries a stated reason, only clips that exist in the archive may be used (no invented footage, ever), and the total sticks to a length budget you set now from the occasion. Step 4: write an acceptance-criteria checklist ('every selected clip exists in clips.json', 'every selection has a reason', 'eras covered per the criteria', 'total within the length budget', 'assembled through the project's pipeline script via run_installed_script with a status-ok log', 'chapter list and dedication delivered'). Call write_task_note with the occasion + archive counts + checklist and write notes/scope.md.",
19
+ "suggestedRole": "filmarchivaris",
20
+ "advanceWhen": {
21
+ "file": "notes/scope.md",
22
+ "minBytes": 1,
23
+ "sniff": "nonempty"
24
+ },
25
+ "gate": {
26
+ "at": "completion",
27
+ "checks": [
28
+ {
29
+ "kind": "minBytes",
30
+ "file": "notes/scope.md",
31
+ "bytes": 120
32
+ },
33
+ {
34
+ "kind": "sniff",
35
+ "file": "notes/scope.md",
36
+ "sniff": "nonempty"
37
+ }
38
+ ],
39
+ "onReject": "scope",
40
+ "maxAttempts": 3
41
+ },
42
+ "next": "select"
43
+ },
44
+ {
45
+ "id": "select",
46
+ "name": "Select the clips",
47
+ "description": "the cut list: archive clips chosen with reasons, in reel order",
48
+ "prompt": "Choose the reel's clips from the archive against the locked criteria. Step 1: sweep clips.json for every candidate moment featuring the person, across all tapes and years. Step 2: select — the famous moments first, then the quieter ones that show an era the famous ones miss; cut candidates that repeat a beat already covered. Every clip id must come from clips.json exactly as indexed; if a moment the family remembers is not in the archive, note it as missing footage rather than inventing a reference. Step 3: order the selections into the reel's emotional arc — chronology is the default, but a strong opening and a strong close outrank strict order. Step 4: write cutlist.json as a JSON array, each record with the clip id from the archive and the reason it made the cut; keep the running total inside the length budget. Call write_task_note with the selection count, the era coverage in one line, and anything remembered-but-missing.",
49
+ "suggestedRole": "curator",
50
+ "advanceWhen": {
51
+ "file": "cutlist.json",
52
+ "minBytes": 1,
53
+ "sniff": "json-valid"
54
+ },
55
+ "gate": {
56
+ "at": "completion",
57
+ "checks": [
58
+ {
59
+ "kind": "minBytes",
60
+ "file": "cutlist.json",
61
+ "bytes": 2
62
+ },
63
+ {
64
+ "kind": "recordSchema",
65
+ "file": "cutlist.json",
66
+ "fields": [
67
+ {
68
+ "name": "clip",
69
+ "type": "nonempty"
70
+ },
71
+ {
72
+ "name": "reason",
73
+ "type": "nonempty"
74
+ }
75
+ ],
76
+ "minRows": 5
77
+ },
78
+ {
79
+ "kind": "valuesSubsetOf",
80
+ "file": "cutlist.json",
81
+ "sourceFiles": [
82
+ "clips.json"
83
+ ],
84
+ "pattern": "\"clip\"\\s*:\\s*\"([^\"]+)\""
85
+ }
86
+ ],
87
+ "scripts": [
88
+ {
89
+ "name": "checkJsonValid",
90
+ "scope": "standard",
91
+ "inputs": {
92
+ "file": "cutlist.json"
93
+ }
94
+ }
95
+ ],
96
+ "onReject": "select",
97
+ "maxAttempts": 3
98
+ },
99
+ "next": "assemble"
100
+ },
101
+ {
102
+ "id": "assemble",
103
+ "name": "Assemble the reel",
104
+ "description": "the reel rendered through the media pipeline, log on disk",
105
+ "prompt": "Assemble the reel from the cut list. Step 1: the assembly pipeline is a project script — call list_scripts to find it, then run it with run_installed_script({ name: \"<script name>\" }); never run it with run_nodejs_script and never reimplement it. It reads cutlist.json and the archive index, refuses any clip it cannot find, writes the render into renders/, and writes its run log to renders/assembly-log.json with a status field. Step 2: if run_installed_script reports an error, read the error text, fix what it names, and run it again — never write renders/assembly-log.json yourself; a fabricated log on a family keepsake is still a fabricated log. Step 3: once it succeeds, read the log and confirm status ok, the clip list matching the cut list, and the computed duration inside the length budget. Call write_task_note with the run outcome and the reel's duration.",
106
+ "suggestedRole": "developer",
107
+ "advanceWhen": {
108
+ "file": "renders/assembly-log.json",
109
+ "minBytes": 1,
110
+ "sniff": "json-valid"
111
+ },
112
+ "gate": {
113
+ "at": "completion",
114
+ "checks": [
115
+ {
116
+ "kind": "minBytes",
117
+ "file": "renders/assembly-log.json",
118
+ "bytes": 2
119
+ },
120
+ {
121
+ "kind": "fileCount",
122
+ "ext": [
123
+ "mp4"
124
+ ],
125
+ "min": 1,
126
+ "dir": "renders"
127
+ },
128
+ {
129
+ "kind": "jsonPathEquals",
130
+ "file": "renders/assembly-log.json",
131
+ "path": "status",
132
+ "value": "ok",
133
+ "label": "the assembly log reports status ok"
134
+ }
135
+ ],
136
+ "scripts": [
137
+ {
138
+ "name": "checkJsonValid",
139
+ "scope": "standard",
140
+ "inputs": {
141
+ "file": "renders/assembly-log.json"
142
+ }
143
+ }
144
+ ],
145
+ "onReject": "assemble",
146
+ "maxAttempts": 3
147
+ },
148
+ "next": "report"
149
+ },
150
+ {
151
+ "id": "report",
152
+ "name": "Chapters and dedication",
153
+ "description": "the chapter list, the dedication, how to play it on the day",
154
+ "prompt": "Write the presentation notes that turn a render into a gift. Step 1: write the chapter list — each selected moment in reel order with its rough position, who is in it, and one warm line about why it is there (the reasons from the cut list, written for the family rather than the archive). Step 2: write the dedication — a short paragraph to open or close the reel, in the family's voice, naming the occasion and the person. Step 3: add the practical notes — the reel's duration, where the render lives, and anything remembered-but-missing from the archive so nobody wonders why a famous moment is absent. Write report.md and call write_task_note with the report path and the chapter count.",
155
+ "suggestedRole": "copywriter",
156
+ "advanceWhen": {
157
+ "file": "report.md",
158
+ "minBytes": 1,
159
+ "sniff": "nonempty"
160
+ },
161
+ "gate": {
162
+ "at": "completion",
163
+ "checks": [
164
+ {
165
+ "kind": "minBytes",
166
+ "file": "report.md",
167
+ "bytes": 800
168
+ },
169
+ {
170
+ "kind": "contains",
171
+ "file": "report.md",
172
+ "pattern": "chapter",
173
+ "flags": "i",
174
+ "label": "a chapter list for the reel"
175
+ }
176
+ ],
177
+ "scripts": [
178
+ {
179
+ "name": "checkContains",
180
+ "scope": "standard",
181
+ "inputs": {
182
+ "file": "report.md",
183
+ "pattern": "(?:^|\\n)#{1,3}\\s+\\S",
184
+ "flags": "i"
185
+ }
186
+ }
187
+ ],
188
+ "onReject": "report",
189
+ "maxAttempts": 4
190
+ },
191
+ "next": "evaluate"
192
+ },
193
+ {
194
+ "id": "evaluate",
195
+ "name": "Evaluate",
196
+ "description": "Grade the deliverable against every acceptance criterion. All pass → finish; any fail → loop back and fix the gap.",
197
+ "prompt": "Open report.md, cutlist.json, renders/assembly-log.json, and clips.json and verify every criterion from notes/scope.md: every selected clip id exists in the archive, every selection carries a real reason, the eras are covered per the criteria, the log reports status ok with a duration inside the length budget, the chapter list matches the assembled order, and the dedication names the occasion and the person warmly and correctly. Write PASS/FAIL per criterion; on any failure, name the gap and loop back to report — or to select if the selection itself is the problem.\n\nThen route — this is the whole point of the loop:\n\n- **Every criterion PASSES →** call `advance_task_step({ ref, stepId: \"evaluate\", next: \"finish\" })`.\n- **Any criterion FAILS →** write the specific gaps to notes, then call `advance_task_step({ ref, stepId: \"evaluate\", next: \"report\" })` to loop back. The builder fixes exactly those gaps.\n\nNever route to `finish` while any criterion is unmet. The build phase's completion gate already blocked a grossly-incomplete deliverable; your job is the judgment an automated check cannot make (does it actually work, read well, look right). After ~3 unproductive loops, stop and report DONE_WITH_CONCERNS so the user can step in.",
198
+ "suggestedRole": "reviewer",
199
+ "next": "report"
200
+ },
201
+ {
202
+ "id": "finish",
203
+ "name": "Finish",
204
+ "description": "All acceptance criteria met. Stamp a short summary and report DONE.",
205
+ "prompt": "Every acceptance criterion passed. Write a one-paragraph DONE summary to task notes via `write_task_note`: what was built, the deliverable path(s), and a one-line confirmation that each criterion is met. Then report DONE.",
206
+ "suggestedRole": "developer",
207
+ "terminal": true
208
+ }
209
+ ],
210
+ "version": "1.0.2",
211
+ "releasedAt": "2026-08-12T00:00:00Z"
212
+ }
@@ -0,0 +1,157 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "title": "Anniversary Cut smoke eval",
4
+ "objective": "Task-specific eval for the Anniversary Cut craftbook: a 30-record family archive is curated into a birthday reel for Ria's 70th — clips selected with reasons and era coverage, assembled through the fake pipeline, delivered with chapters and a dedication.",
5
+ "tags": [
6
+ "external"
7
+ ],
8
+ "prompt": "moms 70th is next month — can you put together a highlight reel from the archive with the fake pipeline? the clips index has everythign tagged",
9
+ "setup": {
10
+ "projectName": "Home Video Archive — Anniversary Reel Eval",
11
+ "about": "Self-contained eval project for anniversary-cut. The archive index is clips.json (30 indexed moments across tapes 01-09, 1986-2024) and the family roster is family.md — mam is Ria, the reel is for her 70th. The selection goes to cutlist.json, the fake media-assembly pipeline is the ready-made mock-probe project script (writes renders/anniversary-cut.mp4 plus renders/assembly-log.json), and the chapter list with the dedication goes to report.md.",
12
+ "missionObjectives": "Use the Anniversary Cut craftbook/template, select the reel from clips.json with a stated reason per clip and coverage across the eras, assemble through the provided mock-probe script, and deliver report.md with a chapter list and dedication. No invented footage, no real media tools — only the wired fake pipeline.",
13
+ "files": [
14
+ {
15
+ "path": "clips.json",
16
+ "content": "[\n {\n \"id\": \"t01-c01\",\n \"tape\": \"tape-01\",\n \"year\": 1986,\n \"timecode\": \"00:04:12\",\n \"durationSeconds\": 41,\n \"people\": [\n \"Ria\",\n \"Opa Cor\"\n ],\n \"description\": \"Ria and her father at the harbor, gulls everywhere\"\n },\n {\n \"id\": \"t01-c02\",\n \"tape\": \"tape-01\",\n \"year\": 1986,\n \"timecode\": \"00:17:40\",\n \"durationSeconds\": 28,\n \"people\": [\n \"Ria\"\n ],\n \"description\": \"Ria winning the village quiz, arms up\"\n },\n {\n \"id\": \"t01-c03\",\n \"tape\": \"tape-01\",\n \"year\": 1987,\n \"timecode\": \"00:29:05\",\n \"durationSeconds\": 35,\n \"people\": [\n \"Ria\",\n \"Henk\"\n ],\n \"description\": \"the first appearance of Henk, very nervous at the door\"\n },\n {\n \"id\": \"t02-c01\",\n \"tape\": \"tape-02\",\n \"year\": 1988,\n \"timecode\": \"00:03:55\",\n \"durationSeconds\": 74,\n \"people\": [\n \"Ria\",\n \"Henk\"\n ],\n \"description\": \"wedding day - the vows, rain starting right on cue\"\n },\n {\n \"id\": \"t02-c02\",\n \"tape\": \"tape-02\",\n \"year\": 1988,\n \"timecode\": \"00:21:18\",\n \"durationSeconds\": 52,\n \"people\": [\n \"Ria\",\n \"Henk\",\n \"Opa Cor\"\n ],\n \"description\": \"wedding toast by Opa Cor, the whole tent laughing\"\n },\n {\n \"id\": \"t02-c03\",\n \"tape\": \"tape-02\",\n \"year\": 1988,\n \"timecode\": \"00:40:02\",\n \"durationSeconds\": 33,\n \"people\": [\n \"Ria\"\n ],\n \"description\": \"Ria dancing in the wedding dress, shoes off\"\n },\n {\n \"id\": \"t03-c01\",\n \"tape\": \"tape-03\",\n \"year\": 1990,\n \"timecode\": \"00:02:33\",\n \"durationSeconds\": 47,\n \"people\": [\n \"Ria\",\n \"Daan\"\n ],\n \"description\": \"bringing Daan home, Ria narrating the whole drive\"\n },\n {\n \"id\": \"t03-c02\",\n \"tape\": \"tape-03\",\n \"year\": 1991,\n \"timecode\": \"00:15:20\",\n \"durationSeconds\": 26,\n \"people\": [\n \"Ria\",\n \"Daan\"\n ],\n \"description\": \"first birthday cake, most of it on Daan\"\n },\n {\n \"id\": \"t03-c03\",\n \"tape\": \"tape-03\",\n \"year\": 1992,\n \"timecode\": \"00:31:44\",\n \"durationSeconds\": 39,\n \"people\": [\n \"Henk\",\n \"Daan\"\n ],\n \"description\": \"Henk and Daan asleep in the same chair\"\n },\n {\n \"id\": \"t04-c01\",\n \"tape\": \"tape-04\",\n \"year\": 1993,\n \"timecode\": \"00:05:10\",\n \"durationSeconds\": 44,\n \"people\": [\n \"Ria\",\n \"Fleur\"\n ],\n \"description\": \"Fleur arrives - Daan inspecting his sister very seriously\"\n },\n {\n \"id\": \"t04-c02\",\n \"tape\": \"tape-04\",\n \"year\": 1994,\n \"timecode\": \"00:19:27\",\n \"durationSeconds\": 31,\n \"people\": [\n \"Ria\",\n \"Daan\",\n \"Fleur\"\n ],\n \"description\": \"the paddling pool afternoon, hose chaos\"\n },\n {\n \"id\": \"t04-c03\",\n \"tape\": \"tape-04\",\n \"year\": 1995,\n \"timecode\": \"00:36:51\",\n \"durationSeconds\": 58,\n \"people\": [\n \"Ria\"\n ],\n \"description\": \"Ria singing at Oma's birthday, surprising everyone\"\n },\n {\n \"id\": \"t05-c01\",\n \"tape\": \"tape-05\",\n \"year\": 1997,\n \"timecode\": \"00:08:14\",\n \"durationSeconds\": 42,\n \"people\": [\n \"Ria\",\n \"Henk\",\n \"Daan\",\n \"Fleur\"\n ],\n \"description\": \"the flooded-kitchen incident, all four bailing with pans\"\n },\n {\n \"id\": \"t05-c02\",\n \"tape\": \"tape-05\",\n \"year\": 1998,\n \"timecode\": \"00:22:39\",\n \"durationSeconds\": 27,\n \"people\": [\n \"Daan\",\n \"Fleur\"\n ],\n \"description\": \"school play - Fleur as the sun, Daan as a reluctant tree\"\n },\n {\n \"id\": \"t05-c03\",\n \"tape\": \"tape-05\",\n \"year\": 1999,\n \"timecode\": \"00:41:03\",\n \"durationSeconds\": 36,\n \"people\": [\n \"Ria\"\n ],\n \"description\": \"Ria finishing her first half marathon, red-faced and delighted\"\n },\n {\n \"id\": \"t06-c01\",\n \"tape\": \"tape-06\",\n \"year\": 2002,\n \"timecode\": \"00:06:30\",\n \"durationSeconds\": 49,\n \"people\": [\n \"Ria\",\n \"Fleur\"\n ],\n \"description\": \"camping in the dunes, tent collapsing at dawn\"\n },\n {\n \"id\": \"t06-c02\",\n \"tape\": \"tape-06\",\n \"year\": 2004,\n \"timecode\": \"00:18:22\",\n \"durationSeconds\": 38,\n \"people\": [\n \"Ria\",\n \"Henk\"\n ],\n \"description\": \"silver-grey Vespa arrives, Ria refuses to let Henk drive it\"\n },\n {\n \"id\": \"t06-c03\",\n \"tape\": \"tape-06\",\n \"year\": 2006,\n \"timecode\": \"00:33:47\",\n \"durationSeconds\": 30,\n \"people\": [\n \"Daan\"\n ],\n \"description\": \"Daan's graduation, Ria filming and crying at the same time\"\n },\n {\n \"id\": \"t07-c01\",\n \"tape\": \"tape-07\",\n \"year\": 2009,\n \"timecode\": \"00:04:58\",\n \"durationSeconds\": 45,\n \"people\": [\n \"Ria\",\n \"Fleur\"\n ],\n \"description\": \"Fleur's graduation, the hat that would not stay on\"\n },\n {\n \"id\": \"t07-c02\",\n \"tape\": \"tape-07\",\n \"year\": 2011,\n \"timecode\": \"00:20:15\",\n \"durationSeconds\": 34,\n \"people\": [\n \"Ria\",\n \"Henk\"\n ],\n \"description\": \"25th anniversary dance in the kitchen, radio on\"\n },\n {\n \"id\": \"t07-c03\",\n \"tape\": \"tape-07\",\n \"year\": 2013,\n \"timecode\": \"00:38:29\",\n \"durationSeconds\": 29,\n \"people\": [\n \"Ria\"\n ],\n \"description\": \"Ria teaching the neighbourhood kids to bake, chaos and flour\"\n },\n {\n \"id\": \"t08-c01\",\n \"tape\": \"tape-08\",\n \"year\": 2015,\n \"timecode\": \"00:07:21\",\n \"durationSeconds\": 40,\n \"people\": [\n \"Ria\",\n \"Daan\"\n ],\n \"description\": \"Daan's wedding - Ria's speech, short and devastatingly funny\"\n },\n {\n \"id\": \"t08-c02\",\n \"tape\": \"tape-08\",\n \"year\": 2017,\n \"timecode\": \"00:23:44\",\n \"durationSeconds\": 32,\n \"people\": [\n \"Ria\",\n \"Henk\"\n ],\n \"description\": \"the allotment garden's first real harvest, absurd zucchini\"\n },\n {\n \"id\": \"t08-c03\",\n \"tape\": \"tape-08\",\n \"year\": 2019,\n \"timecode\": \"00:39:56\",\n \"durationSeconds\": 51,\n \"people\": [\n \"Ria\",\n \"Fleur\",\n \"Noor\"\n ],\n \"description\": \"meeting Noor - Ria holding her granddaughter for the first time\"\n },\n {\n \"id\": \"t09-c01\",\n \"tape\": \"tape-09\",\n \"year\": 2020,\n \"timecode\": \"00:03:40\",\n \"durationSeconds\": 25,\n \"people\": [\n \"Ria\",\n \"Noor\"\n ],\n \"description\": \"window visit, Noor waving both arms\"\n },\n {\n \"id\": \"t09-c02\",\n \"tape\": \"tape-09\",\n \"year\": 2021,\n \"timecode\": \"00:16:12\",\n \"durationSeconds\": 37,\n \"people\": [\n \"Ria\",\n \"Noor\"\n ],\n \"description\": \"first baking lesson for Noor, history repeating in flour\"\n },\n {\n \"id\": \"t09-c03\",\n \"tape\": \"tape-09\",\n \"year\": 2022,\n \"timecode\": \"00:29:33\",\n \"durationSeconds\": 43,\n \"people\": [\n \"Ria\",\n \"Henk\",\n \"Daan\",\n \"Fleur\",\n \"Noor\"\n ],\n \"description\": \"the whole family at the coast, the five-person kite disaster\"\n },\n {\n \"id\": \"t09-c04\",\n \"tape\": \"tape-09\",\n \"year\": 2023,\n \"timecode\": \"00:44:18\",\n \"durationSeconds\": 28,\n \"people\": [\n \"Ria\"\n ],\n \"description\": \"Ria back on the Vespa for her 68th, victory lap of the block\"\n },\n {\n \"id\": \"t09-c05\",\n \"tape\": \"tape-09\",\n \"year\": 2024,\n \"timecode\": \"00:52:02\",\n \"durationSeconds\": 33,\n \"people\": [\n \"Ria\",\n \"Noor\"\n ],\n \"description\": \"Noor reading her first book aloud to Ria on the bench\"\n },\n {\n \"id\": \"t09-c06\",\n \"tape\": \"tape-09\",\n \"year\": 2024,\n \"timecode\": \"00:58:41\",\n \"durationSeconds\": 30,\n \"people\": [\n \"Henk\",\n \"Ria\"\n ],\n \"description\": \"Henk failing to surprise Ria with breakfast, caught mid-plan\"\n }\n]\n"
17
+ },
18
+ {
19
+ "path": "family.md",
20
+ "content": "# Family roster\n\n- Ria - mam, turns 70 next month, the reel is for her\n- Henk - pap, married Ria in 1988\n- Daan - eldest, born 1990\n- Fleur - daughter, born 1993\n- Noor - Fleur's daughter, born 2019\n- Opa Cor - Ria's father, on the earliest tapes\n"
21
+ }
22
+ ],
23
+ "worker": {
24
+ "name": "Noor",
25
+ "role": "Curator"
26
+ }
27
+ },
28
+ "mocks": [
29
+ {
30
+ "kind": "cli",
31
+ "id": "probe",
32
+ "description": "Provenance-trusted project script `mock-probe` that executes the fake media-assembly pipeline: validates cutlist.json against the clips.json archive and writes the stub reel plus its run log.",
33
+ "shim": {
34
+ "path": "scripts/mock-probe.ts",
35
+ "content": "import { defineScript, gezel } from '@bendyline/gezel-sdk';\n\nexport const meta = defineScript({\n name: 'mock-probe',\n description:\n 'Fake media-assembly pipeline for this eval: validates cutlist.json against the clips.json archive index, refuses any clip that is not indexed, then writes the stub reel to renders/anniversary-cut.mp4 and its run log to renders/assembly-log.json.',\n inputs: {},\n outputs: {\n log: { type: 'string', description: 'The assembly run log JSON written to renders/assembly-log.json.' },\n },\n requires: ['workspace.read', 'workspace.write'],\n});\n\nconst cutlist = JSON.parse(await gezel.fs.read('cutlist.json'));\nconst archive = JSON.parse(await gezel.fs.read('clips.json'));\nif (!Array.isArray(cutlist) || cutlist.length === 0) {\n throw new Error('cutlist.json must be a non-empty JSON array');\n}\nconst byId = new Map(archive.map((clip) => [clip.id, clip]));\nlet total = 0;\nconst assembled = [];\nfor (const row of cutlist) {\n const clip = byId.get(row.clip);\n if (!clip) throw new Error(`unknown clip in cutlist: ${row.clip} is not indexed in clips.json`);\n total += typeof clip.durationSeconds === 'number' ? clip.durationSeconds : 0;\n assembled.push(row.clip);\n}\nconst log = {\n status: 'ok',\n engine: 'mock-assemble',\n segments: assembled.length,\n clips: assembled,\n totalDurationSeconds: Math.round(total * 100) / 100,\n};\nawait gezel.fs.write('renders/anniversary-cut.mp4', `MOCKMP4 anniversary reel stub\\n${'0'.repeat(2048)}\\n`);\nawait gezel.fs.write('renders/assembly-log.json', `${JSON.stringify(log, null, 2)}\\n`);\ngezel.output({ log: JSON.stringify(log) });\n"
36
+ }
37
+ }
38
+ ],
39
+ "success": {
40
+ "summary": "cutlist.json selects real archive clips with reasons, the fake pipeline produced the reel and a status-ok log, and report.md carries a chapter list and a dedication for Ria.",
41
+ "deliverables": [
42
+ {
43
+ "path": "cutlist.json",
44
+ "kind": "json",
45
+ "checks": [
46
+ {
47
+ "kind": "recordSchema",
48
+ "file": "cutlist.json",
49
+ "fields": [
50
+ {
51
+ "name": "clip",
52
+ "type": "nonempty"
53
+ },
54
+ {
55
+ "name": "reason",
56
+ "type": "nonempty"
57
+ }
58
+ ],
59
+ "minRows": 5
60
+ },
61
+ {
62
+ "kind": "valuesSubsetOf",
63
+ "file": "cutlist.json",
64
+ "sourceFiles": [
65
+ "clips.json"
66
+ ],
67
+ "pattern": "\"clip\"\\s*:\\s*\"([^\"]+)\"",
68
+ "label": "every selected clip exists in the archive — no invented footage"
69
+ }
70
+ ]
71
+ },
72
+ {
73
+ "path": "renders/anniversary-cut.mp4",
74
+ "kind": "generic-file",
75
+ "minBytes": 200
76
+ },
77
+ {
78
+ "path": "renders/assembly-log.json",
79
+ "kind": "json",
80
+ "checks": [
81
+ {
82
+ "kind": "jsonPathEquals",
83
+ "file": "renders/assembly-log.json",
84
+ "path": "status",
85
+ "value": "ok",
86
+ "label": "the assembly log reports status ok"
87
+ }
88
+ ]
89
+ },
90
+ {
91
+ "path": "report.md",
92
+ "kind": "markdown-report",
93
+ "minBytes": 700,
94
+ "checks": [
95
+ {
96
+ "kind": "contains",
97
+ "file": "report.md",
98
+ "pattern": "(?:^|\\n)#{1,3}\\s+\\S",
99
+ "flags": "i"
100
+ },
101
+ {
102
+ "kind": "contains",
103
+ "file": "report.md",
104
+ "pattern": "chapter",
105
+ "flags": "i",
106
+ "label": "a chapter list for the reel"
107
+ },
108
+ {
109
+ "kind": "contains",
110
+ "file": "report.md",
111
+ "pattern": "Ria",
112
+ "label": "the dedication names Ria"
113
+ },
114
+ {
115
+ "kind": "judge",
116
+ "file": "report.md",
117
+ "rubric": "The selection covers Ria across her eras (1980s through the 2020s) rather than clustering in one period, the family-famous moments (the wedding, meeting Noor) are in, each chapter's line gives a warm reason, and the dedication fits a 70th birthday.",
118
+ "sourceFiles": [
119
+ "clips.json",
120
+ "family.md"
121
+ ],
122
+ "label": "era coverage and warmth"
123
+ }
124
+ ]
125
+ }
126
+ ]
127
+ },
128
+ "rubric": {
129
+ "artifact": {
130
+ "path": "report.md",
131
+ "kind": "markdown"
132
+ },
133
+ "axes": [
134
+ {
135
+ "name": "emotional-arc",
136
+ "description": "The reel opens and closes strong and moves through Ria's life with intent."
137
+ },
138
+ {
139
+ "name": "selection-reasons",
140
+ "description": "Every clip earns its place with a stated reason, not a shrug."
141
+ },
142
+ {
143
+ "name": "era-coverage",
144
+ "description": "Decades are represented; the reel is a life, not one summer."
145
+ },
146
+ {
147
+ "name": "presentation",
148
+ "description": "Chapters and dedication turn a render into a gift."
149
+ }
150
+ ]
151
+ },
152
+ "qualityFocus": [
153
+ "no invented footage",
154
+ "era coverage",
155
+ "dedication warmth"
156
+ ]
157
+ }
@@ -19,6 +19,8 @@
19
19
  },
20
20
  "logo": "logo.webp",
21
21
  "license": "MIT",
22
- "yankedVersions": [],
22
+ "yankedVersions": [
23
+ "1.0.1"
24
+ ],
23
25
  "workflow": "build-loop"
24
26
  }