@bendyline/gilde 0.1.7 → 0.1.9

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 (116) hide show
  1. package/README.md +19 -0
  2. package/data/chat-models/de/deepseek-v4-flash-284b-mxfp4/manifest.json +119 -0
  3. package/data/chat-models/de/deepseek-v4-flash-284b-mxfp4/versions/1.0.0/manifest.json +25 -0
  4. package/data/chat-models/de/deepseek-v4-flash-284b-q2/manifest.json +58 -12
  5. package/data/chat-models/de/deepseek-v4-flash-284b-q2/versions/1.1.0/manifest.json +25 -0
  6. package/data/chat-models/de/deepseek-v4-flash-284b-q2q4/manifest.json +120 -0
  7. package/data/chat-models/de/deepseek-v4-flash-284b-q2q4/versions/1.0.0/manifest.json +25 -0
  8. package/data/chat-models/de/deepseek-v4-flash-284b-q4/manifest.json +53 -7
  9. package/data/chat-models/de/deepseek-v4-flash-284b-q4/versions/1.1.0/manifest.json +223 -0
  10. package/data/chat-models/ge/gemma4-e4b-q4/manifest.json +7 -1
  11. package/data/chat-models/index.json +1 -1
  12. package/data/project-types/ca/caregiving-binder/versions/1.0.1/about.md +8 -0
  13. package/data/project-types/ca/caregiving-binder/versions/1.0.1/care.json +4 -0
  14. package/data/project-types/ca/caregiving-binder/versions/1.0.1/manifest.json +177 -0
  15. package/data/project-types/ca/caregiving-binder/versions/1.0.1/meds.json +8 -0
  16. package/data/project-types/ca/caregiving-binder/versions/1.0.1/mission.md +8 -0
  17. package/data/project-types/ca/caregiving-binder/versions/1.0.1/pages/dashboard/index.html +167 -0
  18. package/data/project-types/ch/checkers/versions/1.1.2/about.md +9 -0
  19. package/data/project-types/ch/checkers/versions/1.1.2/game.json +77 -0
  20. package/data/project-types/ch/checkers/versions/1.1.2/manifest.json +166 -0
  21. package/data/project-types/ch/checkers/versions/1.1.2/mission.md +9 -0
  22. package/data/project-types/ch/checkers/versions/1.1.2/pages/board/index.html +1091 -0
  23. package/data/project-types/ch/chess/versions/1.0.2/about.md +9 -0
  24. package/data/project-types/ch/chess/versions/1.0.2/game.json +127 -0
  25. package/data/project-types/ch/chess/versions/1.0.2/manifest.json +180 -0
  26. package/data/project-types/ch/chess/versions/1.0.2/mission.md +8 -0
  27. package/data/project-types/ch/chess/versions/1.0.2/pages/board/index.html +627 -0
  28. package/data/project-types/ch/chess/versions/1.0.3/about.md +9 -0
  29. package/data/project-types/ch/chess/versions/1.0.3/game.json +127 -0
  30. package/data/project-types/ch/chess/versions/1.0.3/manifest.json +180 -0
  31. package/data/project-types/ch/chess/versions/1.0.3/mission.md +8 -0
  32. package/data/project-types/ch/chess/versions/1.0.3/pages/board/index.html +769 -0
  33. package/data/project-types/de/design-scheme/versions/1.0.1/about.md +6 -0
  34. package/data/project-types/de/design-scheme/versions/1.0.1/manifest.json +69 -0
  35. package/data/project-types/de/design-scheme/versions/1.0.1/mission.md +3 -0
  36. package/data/project-types/de/design-scheme/versions/1.0.1/pages/gallery/index.html +105 -0
  37. package/data/project-types/de/design-scheme/versions/1.0.1/schemes/index.json +9 -0
  38. package/data/project-types/de/design-scheme/versions/1.0.1/schemes/warm-dusk.json +4 -0
  39. package/data/project-types/ev/event-planner/versions/1.0.1/about.md +7 -0
  40. package/data/project-types/ev/event-planner/versions/1.0.1/budget.json +5 -0
  41. package/data/project-types/ev/event-planner/versions/1.0.1/guests.json +10 -0
  42. package/data/project-types/ev/event-planner/versions/1.0.1/manifest.json +170 -0
  43. package/data/project-types/ev/event-planner/versions/1.0.1/mission.md +6 -0
  44. package/data/project-types/ev/event-planner/versions/1.0.1/pages/dashboard/index.html +186 -0
  45. package/data/project-types/fi/fitness-coach/versions/1.0.1/about.md +7 -0
  46. package/data/project-types/fi/fitness-coach/versions/1.0.1/manifest.json +125 -0
  47. package/data/project-types/fi/fitness-coach/versions/1.0.1/mission.md +7 -0
  48. package/data/project-types/fi/fitness-coach/versions/1.0.1/pages/dashboard/index.html +172 -0
  49. package/data/project-types/fi/fitness-coach/versions/1.0.1/training.json +6 -0
  50. package/data/project-types/fl/flashcards/versions/1.0.1/about.md +8 -0
  51. package/data/project-types/fl/flashcards/versions/1.0.1/deck.json +7 -0
  52. package/data/project-types/fl/flashcards/versions/1.0.1/manifest.json +167 -0
  53. package/data/project-types/fl/flashcards/versions/1.0.1/mission.md +6 -0
  54. package/data/project-types/fl/flashcards/versions/1.0.1/pages/review/index.html +396 -0
  55. package/data/project-types/fr/freelance-office/versions/1.0.1/about.md +6 -0
  56. package/data/project-types/fr/freelance-office/versions/1.0.1/invoices.json +9 -0
  57. package/data/project-types/fr/freelance-office/versions/1.0.1/manifest.json +142 -0
  58. package/data/project-types/fr/freelance-office/versions/1.0.1/mission.md +6 -0
  59. package/data/project-types/fr/freelance-office/versions/1.0.1/pages/dashboard/index.html +157 -0
  60. package/data/project-types/fu/fundraiser-hq/versions/1.0.1/about.md +7 -0
  61. package/data/project-types/fu/fundraiser-hq/versions/1.0.1/donations.json +7 -0
  62. package/data/project-types/fu/fundraiser-hq/versions/1.0.1/manifest.json +126 -0
  63. package/data/project-types/fu/fundraiser-hq/versions/1.0.1/mission.md +6 -0
  64. package/data/project-types/fu/fundraiser-hq/versions/1.0.1/pages/dashboard/index.html +160 -0
  65. package/data/project-types/go/go/versions/1.0.1/about.md +9 -0
  66. package/data/project-types/go/go/versions/1.0.1/game.json +103 -0
  67. package/data/project-types/go/go/versions/1.0.1/manifest.json +166 -0
  68. package/data/project-types/go/go/versions/1.0.1/mission.md +8 -0
  69. package/data/project-types/go/go/versions/1.0.1/pages/board/index.html +778 -0
  70. package/data/project-types/ho/household-budget/versions/1.0.1/about.md +6 -0
  71. package/data/project-types/ho/household-budget/versions/1.0.1/ledger.json +5 -0
  72. package/data/project-types/ho/household-budget/versions/1.0.1/manifest.json +163 -0
  73. package/data/project-types/ho/household-budget/versions/1.0.1/mission.md +6 -0
  74. package/data/project-types/ho/household-budget/versions/1.0.1/pages/dashboard/index.html +233 -0
  75. package/data/project-types/ho/household-manual/versions/1.0.1/about.md +7 -0
  76. package/data/project-types/ho/household-manual/versions/1.0.1/manifest.json +152 -0
  77. package/data/project-types/ho/household-manual/versions/1.0.1/mission.md +8 -0
  78. package/data/project-types/ho/household-manual/versions/1.0.1/pages/dashboard/index.html +157 -0
  79. package/data/project-types/ho/household-manual/versions/1.0.1/systems.json +8 -0
  80. package/data/project-types/index.json +1 -1
  81. package/data/project-types/jo/job-hunt/versions/1.0.1/about.md +10 -0
  82. package/data/project-types/jo/job-hunt/versions/1.0.1/activity.json +4 -0
  83. package/data/project-types/jo/job-hunt/versions/1.0.1/manifest.json +212 -0
  84. package/data/project-types/jo/job-hunt/versions/1.0.1/mission.md +9 -0
  85. package/data/project-types/jo/job-hunt/versions/1.0.1/pages/dashboard/index.html +327 -0
  86. package/data/project-types/jo/job-hunt/versions/1.0.1/pipeline.json +6 -0
  87. package/data/project-types/la/language-trainer/versions/1.0.1/about.md +6 -0
  88. package/data/project-types/la/language-trainer/versions/1.0.1/manifest.json +79 -0
  89. package/data/project-types/la/language-trainer/versions/1.0.1/mission.md +4 -0
  90. package/data/project-types/la/language-trainer/versions/1.0.1/pages/dashboard/index.html +153 -0
  91. package/data/project-types/la/language-trainer/versions/1.0.1/progress.json +7 -0
  92. package/data/project-types/li/life-binder/versions/1.0.1/about.md +7 -0
  93. package/data/project-types/li/life-binder/versions/1.0.1/documents.json +145 -0
  94. package/data/project-types/li/life-binder/versions/1.0.1/manifest.json +137 -0
  95. package/data/project-types/li/life-binder/versions/1.0.1/mission.md +7 -0
  96. package/data/project-types/li/life-binder/versions/1.0.1/pages/dashboard/index.html +147 -0
  97. package/data/project-types/me/meal-planner/versions/1.0.1/about.md +6 -0
  98. package/data/project-types/me/meal-planner/versions/1.0.1/manifest.json +159 -0
  99. package/data/project-types/me/meal-planner/versions/1.0.1/menu.json +5 -0
  100. package/data/project-types/me/meal-planner/versions/1.0.1/mission.md +5 -0
  101. package/data/project-types/me/meal-planner/versions/1.0.1/pages/dashboard/index.html +152 -0
  102. package/data/project-types/me/meal-planner/versions/1.0.1/pantry.json +9 -0
  103. package/data/project-types/no/novel-writing-room/versions/1.0.1/about.md +7 -0
  104. package/data/project-types/no/novel-writing-room/versions/1.0.1/chapters.json +10 -0
  105. package/data/project-types/no/novel-writing-room/versions/1.0.1/manifest.json +157 -0
  106. package/data/project-types/no/novel-writing-room/versions/1.0.1/mission.md +7 -0
  107. package/data/project-types/no/novel-writing-room/versions/1.0.1/pages/dashboard/index.html +165 -0
  108. package/data/project-types/no/novel-writing-room/versions/1.0.1/sessions.json +6 -0
  109. package/data/project-types/tr/trip-planner/versions/1.0.1/about.md +7 -0
  110. package/data/project-types/tr/trip-planner/versions/1.0.1/journal.json +4 -0
  111. package/data/project-types/tr/trip-planner/versions/1.0.1/manifest.json +193 -0
  112. package/data/project-types/tr/trip-planner/versions/1.0.1/mission.md +4 -0
  113. package/data/project-types/tr/trip-planner/versions/1.0.1/packing.json +8 -0
  114. package/data/project-types/tr/trip-planner/versions/1.0.1/pages/dashboard/index.html +215 -0
  115. package/data/project-types/tr/trip-planner/versions/1.0.1/trip.json +7 -0
  116. package/package.json +5 -2
@@ -0,0 +1,10 @@
1
+ A campaign to land a **{{role}}** role.
2
+
3
+ Your crew: a Loopbaancoach who keeps every application on the pipeline — tracked
4
+ the day it happens, chased when it goes quiet — and an Oefen-interviewer who runs
5
+ practice rounds before the real ones, with debriefs worth reading twice.
6
+
7
+ Open the **Output** tab any time: the pipeline board and this week's follow-ups
8
+ live there.
9
+
10
+ Notes for the coach: {{focus}}
@@ -0,0 +1,4 @@
1
+ {
2
+ "version": 1,
3
+ "events": []
4
+ }
@@ -0,0 +1,212 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "version": "1.0.1",
4
+ "releasedAt": "2026-08-01T00:00:00Z",
5
+ "extends": "job-hunt",
6
+ "meesterManaged": false,
7
+ "params": {
8
+ "type": "object",
9
+ "properties": {
10
+ "role": {
11
+ "type": "string",
12
+ "title": "What role are you hunting for?",
13
+ "description": "The role or title you're targeting — e.g. Staff Engineer, Product Designer, Data Analyst.",
14
+ "default": "Software Engineer"
15
+ },
16
+ "focus": {
17
+ "type": "string",
18
+ "title": "Anything about your search?",
19
+ "description": "Optional context for your coach — location constraints, timeline, dream companies.",
20
+ "default": "Just getting started."
21
+ }
22
+ },
23
+ "required": [
24
+ "role",
25
+ "focus"
26
+ ]
27
+ },
28
+ "nameTemplate": "{{role}} Job Hunt",
29
+ "aboutTemplate": "about.md",
30
+ "missionTemplate": "mission.md",
31
+ "gezels": [
32
+ {
33
+ "templateId": "career-coach",
34
+ "voorman": true
35
+ },
36
+ {
37
+ "templateId": "mock-interviewer"
38
+ }
39
+ ],
40
+ "scripts": {
41
+ "application-store": "import { type InferredInput, defineScript, gezel } from '@bendyline/gezel-sdk';\nimport { logStore, rosterStore } from '@bendyline/gezel-sdk/stores';\n\nexport const meta = defineScript({\n name: 'application-store',\n description:\n \"Read and update the job-search pipeline (applications in pipeline.json, activity in activity.json). Call action:'status' at the start of a session to see where the search stands; 'record_application' the moment the user mentions applying somewhere; 'advance_stage' when reality moves; 'log_activity' for follow-ups, scheduled interviews, and thank-yous.\",\n inputs: {\n action: {\n type: 'string',\n description: \"One of 'status', 'record_application', 'advance_stage', or 'log_activity'.\",\n required: true,\n },\n company: { type: 'string', description: 'Company name (identifies the application when id is omitted).' },\n role: { type: 'string', description: 'Role title for a new application.' },\n link: { type: 'string', description: 'Posting or application link.' },\n notes: { type: 'string', description: 'Free-form notes for a new application.' },\n id: { type: 'string', description: \"Application id (e.g. 'acme-staff-engineer') when advancing or logging.\" },\n stage: {\n type: 'string',\n description: 'Target stage for advance_stage: applied, screening, interview, offer, or closed.',\n },\n outcome: { type: 'string', description: \"With stage 'closed': signed, rejected, or withdrawn.\" },\n kind: {\n type: 'string',\n description: \"Activity kind for log_activity: follow-up, interview-scheduled, thank-you, offer-received, or note.\",\n },\n note: { type: 'string', description: 'The activity note for log_activity, or a note on a stage change.' },\n due: { type: 'string', description: 'Due date (ISO) for a follow-up.' },\n },\n outputs: {\n applications: { type: 'number', description: 'Total applications tracked.' },\n summary: { type: 'string', description: 'Human-readable pipeline summary.' },\n stale: { type: 'json', description: 'Active applications untouched for over 7 days.' },\n followUps: { type: 'json', description: 'Follow-ups due within 7 days or overdue.' },\n },\n requires: ['workspace.read', 'workspace.write'],\n} as const);\n\nconst input = gezel.input as InferredInput<typeof meta>;\n\nconst STAGES: [string, ...string[]] = ['applied', 'screening', 'interview', 'offer', 'closed'];\nconst OUTCOMES = ['signed', 'rejected', 'withdrawn'];\nconst WEEK_MS = 7 * 24 * 60 * 60 * 1000;\n\nconst roster = rosterStore(gezel.fs, 'pipeline.json', { stages: STAGES, idPrefix: 'app' });\nconst activity = logStore(gezel.fs, 'activity.json');\n\nfunction slugify(s: string): string {\n return (\n s\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/^-+|-+$/g, '') || 'application'\n );\n}\n\nasync function freshId(company: string, role?: string): Promise<string> {\n const base = slugify(role ? `${company} ${role}` : company);\n let candidate = base;\n for (let n = 2; (await roster.get(candidate)) !== null; n += 1) {\n candidate = `${base}-${n}`;\n }\n return candidate;\n}\n\nasync function resolveId(): Promise<string> {\n if (input.id) {\n if ((await roster.get(input.id)) === null) throw new Error(`No application '${input.id}'.`);\n return input.id;\n }\n if (input.company) {\n const wanted = input.company.toLowerCase();\n const matches = (await roster.list()).filter(\n (r) => String(r.fields.company ?? '').toLowerCase() === wanted,\n );\n if (matches.length === 1) return matches[0]!.id;\n if (matches.length > 1) {\n throw new Error(\n `Multiple applications match company '${input.company}': ${matches.map((m) => m.id).join(', ')}. Pass id.`,\n );\n }\n }\n throw new Error(\"Pass 'id' or a unique 'company' to identify the application.\");\n}\n\nasync function summarize(): Promise<{\n applications: number;\n summary: string;\n stale: unknown[];\n followUps: unknown[];\n}> {\n const records = await roster.list();\n const counts = await roster.counts();\n const now = Date.now();\n const stale = records\n .filter((r) => r.status !== 'closed' && now - Date.parse(r.updatedAt) > WEEK_MS)\n .map((r) => ({\n id: r.id,\n company: r.fields.company ?? r.id,\n stage: r.status,\n daysQuiet: Math.floor((now - Date.parse(r.updatedAt)) / (24 * 60 * 60 * 1000)),\n }));\n const followUps = (await activity.list({ kind: 'follow-up' }))\n .filter((e) => {\n const due = typeof e.data?.dueAt === 'string' ? Date.parse(e.data.dueAt) : null;\n return due !== null && due <= now + WEEK_MS;\n })\n .map((e) => ({\n note: e.note ?? '',\n dueAt: e.data?.dueAt,\n applicationId: e.data?.applicationId,\n overdue: typeof e.data?.dueAt === 'string' && Date.parse(e.data.dueAt) < now,\n }));\n const active = records.filter((r) => r.status !== 'closed').length;\n const stageLine = STAGES.filter((s) => (counts[s] ?? 0) > 0)\n .map((s) => `${s} ${counts[s]}`)\n .join(', ');\n const summary = records.length\n ? `${active} application(s) in play (${stageLine || 'none'}). ${stale.length} stale, ${followUps.length} follow-up(s) due this week.`\n : 'No applications tracked yet.';\n return { applications: records.length, summary, stale, followUps };\n}\n\nif (input.action === 'record_application') {\n if (!input.company) throw new Error(\"record_application needs 'company'.\");\n const id = await freshId(input.company, input.role);\n const record = await roster.add({\n id,\n fields: {\n company: input.company,\n ...(input.role ? { role: input.role } : {}),\n ...(input.link ? { link: input.link } : {}),\n ...(input.notes ? { notes: input.notes } : {}),\n },\n });\n await activity.append({\n kind: 'applied',\n note: `Applied to ${input.company}${input.role ? ` for ${input.role}` : ''}.`,\n data: { applicationId: record.id },\n });\n} else if (input.action === 'advance_stage') {\n if (!input.stage) throw new Error(\"advance_stage needs 'stage'.\");\n if (!STAGES.includes(input.stage)) {\n throw new Error(`Unknown stage '${input.stage}'. Stages: ${STAGES.join(', ')}.`);\n }\n if (input.outcome && input.stage !== 'closed') {\n throw new Error(\"'outcome' only applies when closing (stage 'closed').\");\n }\n if (input.outcome && !OUTCOMES.includes(input.outcome)) {\n throw new Error(`Unknown outcome '${input.outcome}'. Outcomes: ${OUTCOMES.join(', ')}.`);\n }\n const id = await resolveId();\n await roster.transition(id, input.stage, input.note ? { note: input.note } : {});\n if (input.outcome) await roster.update(id, { fields: { outcome: input.outcome } });\n const company = String((await roster.get(id))?.fields.company ?? id);\n await activity.append({\n kind: 'stage',\n note: `${company} moved to ${input.stage}${input.outcome ? ` (${input.outcome})` : ''}.`,\n data: { applicationId: id },\n });\n} else if (input.action === 'log_activity') {\n if (!input.note) throw new Error(\"log_activity needs 'note'.\");\n let applicationId: string | undefined;\n if (input.id || input.company) {\n applicationId = await resolveId();\n // A logged touch counts as attention — it resets the staleness clock.\n await roster.update(applicationId, input.note ? { note: input.note } : {});\n }\n await activity.append({\n kind: input.kind ?? 'note',\n note: input.note,\n data: {\n ...(applicationId ? { applicationId } : {}),\n ...(input.due ? { dueAt: input.due } : {}),\n },\n });\n} else if (input.action !== 'status') {\n throw new Error(\n `Unknown action '${input.action}'. Use status, record_application, advance_stage, or log_activity.`,\n );\n}\n\ngezel.output(await summarize());\n"
42
+ },
43
+ "tools": [
44
+ {
45
+ "name": "record_application",
46
+ "description": "Track a new job application the moment it happens.",
47
+ "script": "application-store",
48
+ "inputs": {
49
+ "type": "object",
50
+ "properties": {
51
+ "company": {
52
+ "type": "string",
53
+ "description": "Company name."
54
+ },
55
+ "role": {
56
+ "type": "string",
57
+ "description": "Role title."
58
+ },
59
+ "link": {
60
+ "type": "string",
61
+ "description": "Posting or application link."
62
+ },
63
+ "notes": {
64
+ "type": "string",
65
+ "description": "Anything worth remembering about this one."
66
+ }
67
+ },
68
+ "required": [
69
+ "company"
70
+ ]
71
+ },
72
+ "bind": {
73
+ "action": "record_application"
74
+ }
75
+ },
76
+ {
77
+ "name": "advance_stage",
78
+ "description": "Move an application to a new pipeline stage (applied, screening, interview, offer, closed).",
79
+ "script": "application-store",
80
+ "inputs": {
81
+ "type": "object",
82
+ "properties": {
83
+ "id": {
84
+ "type": "string",
85
+ "description": "Application id."
86
+ },
87
+ "company": {
88
+ "type": "string",
89
+ "description": "Company name (when id is omitted)."
90
+ },
91
+ "stage": {
92
+ "type": "string",
93
+ "enum": [
94
+ "applied",
95
+ "screening",
96
+ "interview",
97
+ "offer",
98
+ "closed"
99
+ ],
100
+ "description": "The stage reality moved to."
101
+ },
102
+ "note": {
103
+ "type": "string",
104
+ "description": "What happened."
105
+ },
106
+ "outcome": {
107
+ "type": "string",
108
+ "enum": [
109
+ "signed",
110
+ "rejected",
111
+ "withdrawn"
112
+ ],
113
+ "description": "Only when closing: how it ended."
114
+ }
115
+ },
116
+ "required": [
117
+ "stage"
118
+ ]
119
+ },
120
+ "bind": {
121
+ "action": "advance_stage"
122
+ }
123
+ },
124
+ {
125
+ "name": "log_activity",
126
+ "description": "Log activity on the search — a follow-up to send, an interview scheduled, a thank-you sent.",
127
+ "script": "application-store",
128
+ "inputs": {
129
+ "type": "object",
130
+ "properties": {
131
+ "id": {
132
+ "type": "string",
133
+ "description": "Application id this activity belongs to."
134
+ },
135
+ "company": {
136
+ "type": "string",
137
+ "description": "Company name (when id is omitted)."
138
+ },
139
+ "kind": {
140
+ "type": "string",
141
+ "enum": [
142
+ "follow-up",
143
+ "interview-scheduled",
144
+ "thank-you",
145
+ "offer-received",
146
+ "note"
147
+ ],
148
+ "description": "What kind of activity this is."
149
+ },
150
+ "note": {
151
+ "type": "string",
152
+ "description": "The activity itself."
153
+ },
154
+ "due": {
155
+ "type": "string",
156
+ "description": "Due date (ISO) for a follow-up."
157
+ }
158
+ },
159
+ "required": [
160
+ "note"
161
+ ]
162
+ },
163
+ "bind": {
164
+ "action": "log_activity"
165
+ }
166
+ },
167
+ {
168
+ "name": "pipeline_status",
169
+ "description": "See the whole pipeline: counts per stage, stale applications, and follow-ups due this week.",
170
+ "script": "application-store",
171
+ "inputs": {
172
+ "type": "object",
173
+ "properties": {}
174
+ },
175
+ "bind": {
176
+ "action": "status"
177
+ }
178
+ }
179
+ ],
180
+ "craftbooks": [
181
+ "tailor-resume",
182
+ "company-research-brief",
183
+ "mock-interview",
184
+ "offer-compare",
185
+ "weekly-pipeline-review"
186
+ ],
187
+ "schedules": [
188
+ {
189
+ "cron": "0 17 * * 5",
190
+ "craftbook": "weekly-pipeline-review",
191
+ "consent": "ask",
192
+ "overlap": "skip"
193
+ }
194
+ ],
195
+ "pages": {
196
+ "entry": "dashboard/index.html",
197
+ "reads": [
198
+ {
199
+ "source": "workspace",
200
+ "path": "pipeline.json"
201
+ },
202
+ {
203
+ "source": "workspace",
204
+ "path": "activity.json"
205
+ }
206
+ ]
207
+ },
208
+ "workspaceSeed": [
209
+ "pipeline.json",
210
+ "activity.json"
211
+ ]
212
+ }
@@ -0,0 +1,9 @@
1
+ - Land a signed offer for a {{role}} role — that is done.
2
+ - Every application is tracked the day it happens; stages move when reality moves.
3
+ - Nothing goes stale silently — anything untouched for a week gets chased, parked
4
+ with a reason, or closed.
5
+ - Materials are tailored per serious application: one resume variant, one company
6
+ brief.
7
+ - Before any real interview: at least one mock round, with its debrief read.
8
+ - Every Friday: the weekly pipeline review — what moved, what stalled, the next
9
+ three actions.
@@ -0,0 +1,327 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <meta name="gezel-demo" content="standalone" />
7
+ <title>Job hunt pipeline</title>
8
+ <style>
9
+ :root {
10
+ color-scheme: light dark;
11
+ --bg: #faf7f2;
12
+ --card: #fff;
13
+ --ink: #2b2620;
14
+ --muted: #7a7166;
15
+ --accent: #b5651d;
16
+ --track: #ece5da;
17
+ --good: #4a7c59;
18
+ --warn: #a4552e;
19
+ }
20
+ @media (prefers-color-scheme: dark) {
21
+ :root {
22
+ --bg: #1c1a17;
23
+ --card: #262320;
24
+ --ink: #efe9e0;
25
+ --muted: #a59a8c;
26
+ --accent: #e0894a;
27
+ --track: #3a352f;
28
+ --good: #7fae8d;
29
+ --warn: #d98a63;
30
+ }
31
+ }
32
+ * { box-sizing: border-box; }
33
+ body {
34
+ margin: 0;
35
+ font: 15px/1.5 system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
36
+ background: var(--bg);
37
+ color: var(--ink);
38
+ padding: 1.5rem 1.25rem 2rem;
39
+ }
40
+ main { max-width: 960px; margin: 0 auto; }
41
+ h1 {
42
+ font-family: Georgia, 'PT Serif', 'Times New Roman', serif;
43
+ font-size: 1.4rem;
44
+ font-weight: 600;
45
+ margin: 0 0 0.15rem;
46
+ }
47
+ .sub { color: var(--muted); margin: 0 0 1.25rem; font-size: 0.9rem; }
48
+ .tiles { display: grid; grid-template-columns: repeat(4, 1fr); gap: 0.6rem; margin-bottom: 1.25rem; }
49
+ .tile {
50
+ background: var(--card);
51
+ border: 1px solid var(--track);
52
+ border-radius: 10px;
53
+ padding: 0.7rem 0.85rem;
54
+ }
55
+ .tile .n { font-size: 1.5rem; font-weight: 700; color: var(--accent); line-height: 1.2; }
56
+ .tile .l { font-size: 0.72rem; color: var(--muted); text-transform: uppercase; letter-spacing: 0.05em; }
57
+ .board { display: grid; grid-template-columns: repeat(5, 1fr); gap: 0.6rem; margin-bottom: 1.25rem; }
58
+ .col { min-width: 0; }
59
+ .col h2 {
60
+ font-size: 0.72rem;
61
+ text-transform: uppercase;
62
+ letter-spacing: 0.05em;
63
+ color: var(--muted);
64
+ margin: 0 0 0.4rem;
65
+ font-weight: 600;
66
+ display: flex;
67
+ justify-content: space-between;
68
+ align-items: baseline;
69
+ }
70
+ .col h2 .count { color: var(--ink); }
71
+ .card {
72
+ background: var(--card);
73
+ border: 1px solid var(--track);
74
+ border-radius: 6px;
75
+ padding: 0.5rem 0.6rem;
76
+ margin-bottom: 0.45rem;
77
+ font-size: 0.85rem;
78
+ }
79
+ .card .co { font-weight: 600; overflow-wrap: anywhere; }
80
+ .card .ro { color: var(--muted); font-size: 0.8rem; overflow-wrap: anywhere; }
81
+ .card .ft { margin-top: 0.3rem; display: flex; gap: 0.35rem; flex-wrap: wrap; align-items: center; color: var(--muted); font-size: 0.72rem; }
82
+ .chip {
83
+ display: inline-block;
84
+ border: 1px solid var(--track);
85
+ border-radius: 4px;
86
+ padding: 0 0.3rem;
87
+ font-size: 0.7rem;
88
+ line-height: 1.4;
89
+ }
90
+ .chip.stale { color: var(--warn); border-color: var(--warn); }
91
+ .chip.signed { color: var(--good); border-color: var(--good); }
92
+ .panels { display: grid; grid-template-columns: 1fr 1fr; gap: 0.6rem; }
93
+ .panel {
94
+ background: var(--card);
95
+ border: 1px solid var(--track);
96
+ border-radius: 10px;
97
+ padding: 0.85rem 1rem;
98
+ }
99
+ .panel h2 { font-size: 0.9rem; margin: 0 0 0.5rem; }
100
+ .panel ul { margin: 0; padding-left: 1.05rem; font-size: 0.85rem; }
101
+ .panel li { margin: 0.25rem 0; }
102
+ .panel .when { color: var(--muted); font-size: 0.78rem; }
103
+ .overdue { color: var(--warn); font-weight: 600; }
104
+ .empty { color: var(--muted); font-style: italic; }
105
+ .board-empty {
106
+ background: var(--card);
107
+ border: 1px dashed var(--track);
108
+ border-radius: 10px;
109
+ padding: 0.85rem 1.25rem;
110
+ text-align: center;
111
+ color: var(--muted);
112
+ margin-bottom: 0.75rem;
113
+ }
114
+ @media (max-width: 720px) {
115
+ .tiles { grid-template-columns: repeat(2, 1fr); }
116
+ .board { grid-template-columns: 1fr; }
117
+ .panels { grid-template-columns: 1fr; }
118
+ }
119
+ + .demo-banner {
120
+ display: flex;
121
+ align-items: center;
122
+ justify-content: space-between;
123
+ gap: 0.75rem;
124
+ margin: 0 0 0.85rem;
125
+ padding: 0.5rem 0.65rem;
126
+ border: 1px solid var(--accent, #8a5a2b);
127
+ border-radius: 6px;
128
+ background: color-mix(in srgb, var(--card, var(--surface, #fffdf8)) 88%, var(--accent, #8a5a2b));
129
+ color: var(--ink, #2b2620);
130
+ font-size: 0.8rem;
131
+ }
132
+ .demo-banner[hidden] { display: none; }
133
+ .demo-banner strong { font-weight: 700; }
134
+ .demo-reset {
135
+ flex: none;
136
+ padding: 0.25rem 0.5rem;
137
+ border: 1px solid var(--accent, #8a5a2b);
138
+ border-radius: 6px;
139
+ background: var(--card, var(--surface, #fffdf8));
140
+ color: var(--ink, #2b2620);
141
+ cursor: pointer;
142
+ font: inherit;
143
+ }
144
+ .demo-reset:hover { border-color: var(--ink, #2b2620); }
145
+ </style>
146
+ </head>
147
+ <body>
148
+ <main>
149
+ + <div class="demo-banner" data-gezel-demo-banner hidden>
150
+ <span><strong>Demo mode</strong> — sample data stays in this page.</span>
151
+ <button type="button" class="demo-reset" onclick="location.reload()">Reset demo</button>
152
+ </div>
153
+ <h1 id="title">Job hunt</h1>
154
+ <p class="sub" id="sub">Loading the pipeline…</p>
155
+ <div class="tiles">
156
+ <div class="tile"><div class="n" id="t-play">—</div><div class="l">In play</div></div>
157
+ <div class="tile"><div class="n" id="t-interview">—</div><div class="l">Interviewing</div></div>
158
+ <div class="tile"><div class="n" id="t-offer">—</div><div class="l">Offers</div></div>
159
+ <div class="tile"><div class="n" id="t-follow">—</div><div class="l">Follow-ups this week</div></div>
160
+ </div>
161
+ <div class="board-empty" id="board-empty" hidden>
162
+ No applications yet — tell your Loopbaancoach about the first one.
163
+ </div>
164
+ <div class="board" id="board"></div>
165
+ <div class="panels">
166
+ <div class="panel">
167
+ <h2>This week</h2>
168
+ <ul id="week"><li class="empty">Nothing due — nice and current.</li></ul>
169
+ </div>
170
+ <div class="panel">
171
+ <h2>Recent activity</h2>
172
+ <ul id="activity"><li class="empty">No activity yet.</li></ul>
173
+ </div>
174
+ </div>
175
+ </main>
176
+ <script>
177
+ // Served at /preview/<capability>/type/<projectId>/dashboard/index.html.
178
+ // Pull capability + projectId from our own path, then read the pipeline
179
+ // and activity records from the workspace preview (same origin; both
180
+ // paths are declared in the manifest's pages.reads).
181
+ (function () {
182
+ var parts = location.pathname.split('/').filter(Boolean);
183
+ var i = parts.indexOf('type');
184
+ var capability = i > 0 ? parts[i - 1] : null;
185
+ var projectId = i >= 0 ? parts[i + 1] : null;
186
+ var WEEK = 7 * 24 * 60 * 60 * 1000;
187
+ var DAY = 24 * 60 * 60 * 1000;
188
+ var FALLBACK_STAGES = ['applied', 'screening', 'interview', 'offer', 'closed'];
189
+ var demoMode = !capability || !projectId;
190
+ var demoPipeline = {
191
+ role: 'Product designer',
192
+ stages: FALLBACK_STAGES,
193
+ records: [
194
+ { id: 'app-12', status: 'applied', updatedAt: new Date(Date.now() - 3 * DAY).toISOString(), fields: { company: 'Canal Labs', role: 'Senior Product Designer' }, history: [{ at: new Date(Date.now() - 3 * DAY).toISOString() }] },
195
+ { id: 'app-11', status: 'screening', updatedAt: new Date(Date.now() - 9 * DAY).toISOString(), fields: { company: 'Northwind', role: 'Product Designer' }, history: [{ at: new Date(Date.now() - 9 * DAY).toISOString() }] },
196
+ { id: 'app-10', status: 'interview', updatedAt: new Date(Date.now() - DAY).toISOString(), fields: { company: 'Veld Works', role: 'Staff Product Designer' }, history: [{ at: new Date(Date.now() - 5 * DAY).toISOString() }] },
197
+ { id: 'app-09', status: 'offer', updatedAt: new Date().toISOString(), fields: { company: 'Juniper Press', role: 'Design Lead' }, history: [{ at: new Date().toISOString() }] },
198
+ { id: 'app-08', status: 'closed', updatedAt: new Date(Date.now() - 12 * DAY).toISOString(), fields: { company: 'Old Mill', role: 'Product Designer', outcome: 'withdrew' }, history: [{ at: new Date(Date.now() - 12 * DAY).toISOString() }] },
199
+ ],
200
+ };
201
+ var demoActivity = { events: [
202
+ { kind: 'note', at: new Date(Date.now() - 2 * DAY).toISOString(), note: 'Prepared portfolio stories for Veld Works.' },
203
+ { kind: 'follow-up', at: new Date(Date.now() - DAY).toISOString(), note: 'Send Northwind a short follow-up', data: { dueAt: new Date(Date.now() + DAY).toISOString() } },
204
+ { kind: 'interview', at: new Date().toISOString(), note: 'Juniper Press final conversation completed.' },
205
+ ] };
206
+
207
+ function esc(s) {
208
+ return String(s).replace(/[&<>"]/g, function (c) {
209
+ return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c];
210
+ });
211
+ }
212
+ function cap(s) { return s.charAt(0).toUpperCase() + s.slice(1); }
213
+ function daysAgo(iso) { return Math.floor((Date.now() - Date.parse(iso)) / DAY); }
214
+
215
+ function fetchJson(file) {
216
+ return fetch(
217
+ '/preview/' + encodeURIComponent(capability) + '/workspace/' + encodeURIComponent(projectId) + '/' + file,
218
+ { cache: 'no-store' },
219
+ ).then(function (r) {
220
+ if (!r.ok) throw new Error(file + ' unavailable');
221
+ return r.json();
222
+ });
223
+ }
224
+
225
+ function render(pipeline, act) {
226
+ var stages = Array.isArray(pipeline.stages) && pipeline.stages.length ? pipeline.stages : FALLBACK_STAGES;
227
+ var records = Array.isArray(pipeline.records) ? pipeline.records : [];
228
+ var events = Array.isArray(act.events) ? act.events : [];
229
+ var now = Date.now();
230
+
231
+ if (pipeline.role) document.getElementById('title').textContent = 'Job hunt: ' + pipeline.role;
232
+
233
+ var inPlay = records.filter(function (r) { return r.status !== 'closed'; });
234
+ var followUps = events.filter(function (e) {
235
+ var due = e.data && typeof e.data.dueAt === 'string' ? Date.parse(e.data.dueAt) : null;
236
+ return e.kind === 'follow-up' && due !== null && due <= now + WEEK;
237
+ });
238
+ document.getElementById('t-play').textContent = inPlay.length;
239
+ document.getElementById('t-interview').textContent = records.filter(function (r) { return r.status === 'interview'; }).length;
240
+ document.getElementById('t-offer').textContent = records.filter(function (r) { return r.status === 'offer'; }).length;
241
+ document.getElementById('t-follow').textContent = followUps.length;
242
+ document.getElementById('sub').textContent =
243
+ inPlay.length + ' in play · ' + followUps.length + ' follow-up' + (followUps.length === 1 ? '' : 's') + ' this week';
244
+
245
+ // The stage columns always render — even empty, the board teaches
246
+ // the shape of the campaign. The empty-state line sits above it.
247
+ document.getElementById('board-empty').hidden = records.length > 0;
248
+ var board = document.getElementById('board');
249
+ board.innerHTML = stages
250
+ .map(function (stage) {
251
+ var here = records.filter(function (r) { return r.status === stage; });
252
+ var cards = here
253
+ .map(function (r) {
254
+ var f = r.fields || {};
255
+ var chips = '';
256
+ var last = (r.history && r.history[r.history.length - 1]) || null;
257
+ var inStage = last ? daysAgo(last.at) : null;
258
+ if (stage !== 'closed' && daysAgo(r.updatedAt) > 7) {
259
+ chips += '<span class="chip stale">stale</span>';
260
+ }
261
+ if (stage === 'closed' && f.outcome) {
262
+ chips += '<span class="chip ' + (f.outcome === 'signed' ? 'signed' : '') + '">' + esc(f.outcome) + '</span>';
263
+ }
264
+ return (
265
+ '<div class="card"><div class="co">' + esc(f.company || r.id) + '</div>' +
266
+ (f.role ? '<div class="ro">' + esc(f.role) + '</div>' : '') +
267
+ '<div class="ft">' + (inStage !== null ? '<span>' + inStage + 'd in stage</span>' : '') + chips + '</div></div>'
268
+ );
269
+ })
270
+ .join('');
271
+ return (
272
+ '<div class="col"><h2><span>' + esc(cap(stage)) + '</span><span class="count">' + here.length + '</span></h2>' + cards + '</div>'
273
+ );
274
+ })
275
+ .join('');
276
+
277
+ var week = document.getElementById('week');
278
+ var weekItems = [];
279
+ followUps
280
+ .slice()
281
+ .sort(function (a, b) { return Date.parse(a.data.dueAt) - Date.parse(b.data.dueAt); })
282
+ .forEach(function (e) {
283
+ var overdue = Date.parse(e.data.dueAt) < now;
284
+ weekItems.push(
285
+ '<li>' + (overdue ? '<span class="overdue">Overdue:</span> ' : '') + esc(e.note || 'Follow up') +
286
+ ' <span class="when">(' + esc(String(e.data.dueAt).slice(0, 10)) + ')</span></li>',
287
+ );
288
+ });
289
+ inPlay.forEach(function (r) {
290
+ var quiet = daysAgo(r.updatedAt);
291
+ if (quiet > 7) {
292
+ weekItems.push(
293
+ '<li>' + esc((r.fields && r.fields.company) || r.id) + ' <span class="when">— no touch in ' + quiet + ' days</span></li>',
294
+ );
295
+ }
296
+ });
297
+ if (weekItems.length) week.innerHTML = weekItems.join('');
298
+
299
+ var activityList = document.getElementById('activity');
300
+ var recent = events.slice(-6).reverse();
301
+ if (recent.length) {
302
+ activityList.innerHTML = recent
303
+ .map(function (e) {
304
+ return '<li>' + esc(e.note || e.kind) + ' <span class="when">(' + esc(String(e.at).slice(0, 10)) + ')</span></li>';
305
+ })
306
+ .join('');
307
+ }
308
+ }
309
+
310
+ if (demoMode) {
311
+ document.querySelector('[data-gezel-demo-banner]').hidden = false;
312
+ render(demoPipeline, demoActivity);
313
+ return;
314
+ }
315
+ Promise.all([
316
+ fetchJson('pipeline.json'),
317
+ fetchJson('activity.json').catch(function () { return { events: [] }; }),
318
+ ])
319
+ .then(function (results) { render(results[0], results[1]); })
320
+ .catch(function () {
321
+ document.getElementById('sub').textContent =
322
+ 'No pipeline yet — tell your Loopbaancoach about your first application.';
323
+ });
324
+ })();
325
+ </script>
326
+ </body>
327
+ </html>
@@ -0,0 +1,6 @@
1
+ {
2
+ "version": 1,
3
+ "role": "{{role}}",
4
+ "stages": ["applied", "screening", "interview", "offer", "closed"],
5
+ "records": []
6
+ }
@@ -0,0 +1,6 @@
1
+ A project for learning **{{language}}** by actually speaking it. Your tutor chats with
2
+ you in {{language}}, meets you at your level, corrects mistakes gently, and keeps track of
3
+ how far you've come.
4
+
5
+ Open the **Output** tab any time to see your progress dashboard — your current level,
6
+ practice sessions, and streak. The more you show up and talk, the further it moves.