@elalfner/relevo 0.0.0-stage → 0.3.1

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 (142) hide show
  1. package/.claude-plugin/plugin.json +19 -0
  2. package/LICENSE +21 -0
  3. package/NOTICE +51 -0
  4. package/README.es.md +90 -0
  5. package/README.md +89 -2
  6. package/agents/dev.md +101 -0
  7. package/agents/qa.md +112 -0
  8. package/package.json +42 -4
  9. package/scripts/add-corrections.mjs +21 -0
  10. package/scripts/app-id.mjs +19 -0
  11. package/scripts/archive-state.mjs +31 -0
  12. package/scripts/branch-state.mjs +31 -0
  13. package/scripts/branch-work.mjs +25 -0
  14. package/scripts/can-edit-issue.mjs +45 -0
  15. package/scripts/capture.mjs +22 -0
  16. package/scripts/check-archive.mjs +19 -0
  17. package/scripts/check-issue.mjs +41 -0
  18. package/scripts/checklist.mjs +29 -0
  19. package/scripts/cli.mjs +28 -0
  20. package/scripts/commit-change.mjs +50 -0
  21. package/scripts/conflict-scope.mjs +19 -0
  22. package/scripts/design-fetch.mjs +103 -0
  23. package/scripts/device.mjs +12 -0
  24. package/scripts/digest.mjs +50 -0
  25. package/scripts/doctor.mjs +47 -0
  26. package/scripts/failure-comment.mjs +30 -0
  27. package/scripts/fix-guard.mjs +38 -0
  28. package/scripts/fix-stash.mjs +57 -0
  29. package/scripts/fix-state.mjs +28 -0
  30. package/scripts/fix-tasks.mjs +22 -0
  31. package/scripts/go-local.mjs +49 -0
  32. package/scripts/go-plan.mjs +82 -0
  33. package/scripts/init.mjs +109 -0
  34. package/scripts/intent-to-add.mjs +20 -0
  35. package/scripts/issue-coverage.mjs +30 -0
  36. package/scripts/issue-hash.mjs +44 -0
  37. package/scripts/issue-sections.mjs +42 -0
  38. package/scripts/label-change.mjs +35 -0
  39. package/scripts/lib/appid.mjs +12 -0
  40. package/scripts/lib/archive-check.mjs +29 -0
  41. package/scripts/lib/archive-state.mjs +15 -0
  42. package/scripts/lib/branch-work.mjs +13 -0
  43. package/scripts/lib/branch.mjs +43 -0
  44. package/scripts/lib/can-edit-issue.mjs +34 -0
  45. package/scripts/lib/checklist.mjs +68 -0
  46. package/scripts/lib/cli.mjs +82 -0
  47. package/scripts/lib/commit-change.mjs +57 -0
  48. package/scripts/lib/config.mjs +296 -0
  49. package/scripts/lib/design-fetch.mjs +156 -0
  50. package/scripts/lib/device.mjs +67 -0
  51. package/scripts/lib/doctor.mjs +636 -0
  52. package/scripts/lib/execution-mode.mjs +24 -0
  53. package/scripts/lib/failure-comment.mjs +19 -0
  54. package/scripts/lib/fix-guard.mjs +23 -0
  55. package/scripts/lib/fix-stash.mjs +17 -0
  56. package/scripts/lib/fix.mjs +31 -0
  57. package/scripts/lib/github.mjs +161 -0
  58. package/scripts/lib/go-local.mjs +102 -0
  59. package/scripts/lib/init.mjs +328 -0
  60. package/scripts/lib/intent-to-add.mjs +32 -0
  61. package/scripts/lib/issue-check.mjs +63 -0
  62. package/scripts/lib/issue-coverage.mjs +47 -0
  63. package/scripts/lib/issue-hash.mjs +25 -0
  64. package/scripts/lib/issue-parse.mjs +194 -0
  65. package/scripts/lib/issue-sections.mjs +43 -0
  66. package/scripts/lib/labels.mjs +65 -0
  67. package/scripts/lib/language.mjs +9 -0
  68. package/scripts/lib/logaudit.mjs +123 -0
  69. package/scripts/lib/messages.mjs +341 -0
  70. package/scripts/lib/next-step.mjs +153 -0
  71. package/scripts/lib/openspec-commands.mjs +44 -0
  72. package/scripts/lib/openspec-status.mjs +16 -0
  73. package/scripts/lib/plan.mjs +256 -0
  74. package/scripts/lib/platform-of.mjs +32 -0
  75. package/scripts/lib/platform-scope.mjs +32 -0
  76. package/scripts/lib/platforms.mjs +90 -0
  77. package/scripts/lib/preflight.mjs +71 -0
  78. package/scripts/lib/protected-paths.mjs +102 -0
  79. package/scripts/lib/requeue.mjs +23 -0
  80. package/scripts/lib/review-adapters.mjs +37 -0
  81. package/scripts/lib/review-brief.mjs +43 -0
  82. package/scripts/lib/review-events.mjs +56 -0
  83. package/scripts/lib/review-state.mjs +157 -0
  84. package/scripts/lib/reviewers.mjs +39 -0
  85. package/scripts/lib/role-skills.mjs +112 -0
  86. package/scripts/lib/run-flags.mjs +16 -0
  87. package/scripts/lib/run-usage.mjs +65 -0
  88. package/scripts/lib/scan.mjs +73 -0
  89. package/scripts/lib/ship.mjs +161 -0
  90. package/scripts/lib/sla.mjs +90 -0
  91. package/scripts/lib/stack-profile.mjs +169 -0
  92. package/scripts/lib/staging.mjs +21 -0
  93. package/scripts/lib/status-data.mjs +109 -0
  94. package/scripts/lib/status.mjs +199 -0
  95. package/scripts/lib/tasks-file.mjs +81 -0
  96. package/scripts/lib/web-review.mjs +98 -0
  97. package/scripts/lib/wip-commit.mjs +24 -0
  98. package/scripts/lib/yaml-lite.mjs +157 -0
  99. package/scripts/needs-reviewer.mjs +41 -0
  100. package/scripts/next-step.mjs +39 -0
  101. package/scripts/pick-reviewer.mjs +42 -0
  102. package/scripts/platform-of.mjs +49 -0
  103. package/scripts/preflight.mjs +32 -0
  104. package/scripts/render-message.mjs +35 -0
  105. package/scripts/requeue.mjs +53 -0
  106. package/scripts/review-adapter.mjs +32 -0
  107. package/scripts/review-brief.mjs +29 -0
  108. package/scripts/review-event.mjs +70 -0
  109. package/scripts/review-sla.mjs +41 -0
  110. package/scripts/review-state.mjs +171 -0
  111. package/scripts/review-who.mjs +21 -0
  112. package/scripts/role-skills.mjs +53 -0
  113. package/scripts/run-usage.mjs +22 -0
  114. package/scripts/scan-diff.mjs +16 -0
  115. package/scripts/ship-check.mjs +54 -0
  116. package/scripts/spec-ready.mjs +40 -0
  117. package/scripts/stack-profile.mjs +67 -0
  118. package/scripts/stage.mjs +14 -0
  119. package/scripts/status.mjs +47 -0
  120. package/scripts/usage-limit.mjs +17 -0
  121. package/scripts/validate-config.mjs +44 -0
  122. package/scripts/web-review.mjs +134 -0
  123. package/skills/doctor/SKILL.md +28 -0
  124. package/skills/fix/SKILL.md +129 -0
  125. package/skills/go/SKILL.md +250 -0
  126. package/skills/init/SKILL.md +72 -0
  127. package/skills/issue/SKILL.md +128 -0
  128. package/skills/issue/references/update.md +15 -0
  129. package/skills/review/SKILL.md +157 -0
  130. package/skills/ship/SKILL.md +128 -0
  131. package/skills/status/SKILL.md +27 -0
  132. package/skills/think/SKILL.md +76 -0
  133. package/stacks/flutter.md +63 -0
  134. package/stacks/laravel.md +65 -0
  135. package/stacks/vue.md +70 -0
  136. package/templates/issue.md +99 -0
  137. package/templates/relevo.yml +59 -0
  138. package/templates/workflows/relevo-digest.yml +54 -0
  139. package/templates/workflows/relevo-go.yml +268 -0
  140. package/templates/workflows/relevo-queue.yml +72 -0
  141. package/templates/workflows/relevo-review.yml +60 -0
  142. package/templates/workflows/relevo-sla.yml +53 -0
@@ -0,0 +1,341 @@
1
+ // Hand-off messages. Implements the hand-off requirements of
2
+ // openspec/specs/issue-lifecycle/spec.md.
3
+ //
4
+ // A line is dropped when any of its {{placeholders}} has no value, so optional
5
+ // parts (riskiest area, due time, log link) simply disappear when unknown.
6
+ // Long values are shortened: long logs are linked, never embedded.
7
+
8
+ import { shortHash } from './issue-hash.mjs';
9
+ import { pickLanguage, SUPPORTED_LANGUAGES } from './language.mjs';
10
+ import { nextStep, nextStepPrefix } from './next-step.mjs';
11
+
12
+ // Kept here as well so that existing imports keep working.
13
+ export { pickLanguage, SUPPORTED_LANGUAGES };
14
+
15
+ const MAX_VALUE_LENGTH = 400;
16
+
17
+ const REQUIRED = {
18
+ built: ['assignee', 'issue', 'title', 'tasks_done', 'tasks_total', 'branch', 'scenarios'],
19
+ qa_failed: ['assignee', 'issue', 'title', 'finding', 'branch'],
20
+ needs_spec: ['spec_writer', 'issue', 'title', 'missing'],
21
+ changes_requested: ['author', 'issue', 'reviewer', 'summary'],
22
+ needs_reviewer: ['spec_writer', 'issue', 'title', 'pr'],
23
+ approved: ['recipient', 'issue', 'reviewer'],
24
+ review_reminder: ['reviewer', 'issue', 'pr', 'hours'],
25
+ review_escalation: ['spec_writer', 'issue', 'pr', 'reviewer', 'hours'],
26
+ no_milestone: ['actor', 'issue'],
27
+ cloud_not_configured: ['actor', 'issue'],
28
+ no_assignee: ['actor', 'issue'],
29
+ ready_denied: ['issue'],
30
+ ready_denied_maintainer: ['issue'],
31
+ blocked_notice: ['issue', 'title', 'blockers'],
32
+ queued_capacity: ['issue', 'title', 'assignee', 'built', 'limit'],
33
+ queued_agents: ['issue', 'title'],
34
+ fix_done: ['issue', 'title', 'summary', 'qa', 'commit', 'iteration'],
35
+ };
36
+
37
+ // The lifecycle state whose next step each message shows (see next-step.mjs, the only place that knows the commands).
38
+ const STATE_OF = {
39
+ built: 'built',
40
+ qa_failed: 'qa-failed',
41
+ needs_spec: 'needs-spec',
42
+ changes_requested: 'changes-requested',
43
+ approved: 'pr-open',
44
+ };
45
+
46
+ const CATALOG = {
47
+ en: {
48
+ built: [
49
+ '@{{assignee}} #{{issue}} "{{title}}" is built ✅',
50
+ '',
51
+ 'What happened: {{tasks_done}} of {{tasks_total}} tasks done, QA passed.',
52
+ 'Branch: {{branch}}',
53
+ 'Built from the issue text: {{issue_hash_short}}',
54
+ 'What to test: {{scenarios}} scenarios (checklist below)',
55
+ 'Riskiest area, check it first: {{risk}}',
56
+ 'Your next step: {{command}}',
57
+ 'Expected by: {{due}}',
58
+ '',
59
+ '{{checklist}}',
60
+ ],
61
+ qa_failed: [
62
+ '@{{assignee}} #{{issue}} "{{title}}" failed QA ❌',
63
+ '',
64
+ 'What happened: QA still rejected it after the retries{{reason_suffix}}.',
65
+ 'Finding: {{finding}}{{location_suffix}}',
66
+ '{{branch_line}}',
67
+ 'Full QA report: {{log_url}}',
68
+ '{{next_line}}',
69
+ ],
70
+ needs_spec: [
71
+ '@{{spec_writer}} #{{issue}} "{{title}}" is back with you: the spec needs work',
72
+ '',
73
+ 'What is missing: {{missing}}',
74
+ '{{next_line}}',
75
+ ],
76
+ changes_requested: [
77
+ '@{{author}} #{{issue}}: {{reviewer}} requested changes',
78
+ '',
79
+ 'Summary: {{summary}}',
80
+ 'Full review: {{pr}}',
81
+ 'Your next step: {{command}}',
82
+ ],
83
+ approved: [
84
+ '@{{recipient}} #{{issue}} was approved by {{reviewer}} ✅',
85
+ '',
86
+ 'Pull request: {{pr}}',
87
+ 'Your next step: {{command}}',
88
+ ],
89
+ needs_reviewer: [
90
+ '@{{spec_writer}} #{{issue}} "{{title}}" has no available reviewer',
91
+ '',
92
+ 'Pull request: {{pr}}',
93
+ 'Your next step: assign a reviewer or ask someone to take it.',
94
+ ],
95
+ review_reminder: [
96
+ '@{{reviewer}} reminder: #{{issue}} has been waiting for your review for {{hours}} hours',
97
+ '',
98
+ 'Pull request: {{pr}}',
99
+ ],
100
+ review_escalation: [
101
+ '@{{spec_writer}} #{{issue}} has waited {{hours}} hours without a review from @{{reviewer}}',
102
+ '',
103
+ 'Pull request: {{pr}}',
104
+ 'Your next step: decide who reviews it.',
105
+ ],
106
+ cloud_not_configured: [
107
+ '#{{issue}} is ready, but the cloud cannot build it: this repository has no model credential',
108
+ '',
109
+ '@{{actor}} your next step: run `/relevo:go {{issue}}` on your machine, or add the secret ANTHROPIC_API_KEY or CLAUDE_CODE_OAUTH_TOKEN so that the cloud can build it. The issue stays ready.',
110
+ ],
111
+ no_milestone: [
112
+ '#{{issue}} was not started: it has no milestone',
113
+ '',
114
+ '@{{actor}} your next step: add the issue to a milestone and label it ready again.',
115
+ ],
116
+ no_assignee: [
117
+ '#{{issue}} was not started: it has no assignee',
118
+ '',
119
+ '@{{actor}} your next step: assign the issue to the person who will review it and label it ready again.',
120
+ ],
121
+ ready_denied: [
122
+ '#{{issue}} was not started: only people with write access can mark an issue ready, so the label was removed',
123
+ 'Applied by: @{{actor}}',
124
+ ],
125
+ ready_denied_maintainer: [
126
+ '#{{issue}} was not started: this repository is public, so only maintainers can mark an issue ready, and the label was removed',
127
+ 'Applied by: @{{actor}}',
128
+ ],
129
+ blocked_notice: [
130
+ '#{{issue}} "{{title}}" is blocked',
131
+ '',
132
+ 'Waiting for {{blockers}} to be merged. It starts automatically when they are.',
133
+ ],
134
+ queued_capacity: [
135
+ '#{{issue}} "{{title}}" is queued',
136
+ '',
137
+ '{{assignee}} already has {{built}} built issues waiting for review (the limit is {{limit}}). It starts automatically when one of them is reviewed.',
138
+ ],
139
+ queued_agents: [
140
+ '#{{issue}} "{{title}}" is queued',
141
+ '',
142
+ 'The limit of concurrent agents is reached. It starts automatically when a run finishes.',
143
+ ],
144
+ fix_done: [
145
+ '#{{issue}} "{{title}}" fixed (iteration {{iteration}})',
146
+ '',
147
+ 'What changed: {{summary}}',
148
+ 'QA: {{qa}}',
149
+ 'Commit: {{commit}}',
150
+ '@{{recipient}} {{ask}}',
151
+ ],
152
+ },
153
+ es: {
154
+ built: [
155
+ '@{{assignee}} #{{issue}} "{{title}}" está construida ✅',
156
+ '',
157
+ 'Qué pasó: {{tasks_done}} de {{tasks_total}} tareas hechas, QA aprobado.',
158
+ 'Rama: {{branch}}',
159
+ 'Construida con el texto del issue: {{issue_hash_short}}',
160
+ 'Qué probar: {{scenarios}} escenarios (checklist abajo)',
161
+ 'Zona más riesgosa, revísala primero: {{risk}}',
162
+ 'Tu siguiente paso: {{command}}',
163
+ 'Esperada para: {{due}}',
164
+ '',
165
+ '{{checklist}}',
166
+ ],
167
+ qa_failed: [
168
+ '@{{assignee}} #{{issue}} "{{title}}" no pasó QA ❌',
169
+ '',
170
+ 'Qué pasó: QA la rechazó otra vez tras los reintentos{{reason_suffix}}.',
171
+ 'Hallazgo: {{finding}}{{location_suffix}}',
172
+ '{{branch_line}}',
173
+ 'Reporte completo de QA: {{log_url}}',
174
+ '{{next_line}}',
175
+ ],
176
+ needs_spec: [
177
+ '@{{spec_writer}} #{{issue}} "{{title}}" regresa contigo: la spec necesita trabajo',
178
+ '',
179
+ 'Qué falta: {{missing}}',
180
+ '{{next_line}}',
181
+ ],
182
+ changes_requested: [
183
+ '@{{author}} #{{issue}}: {{reviewer}} pidió cambios',
184
+ '',
185
+ 'Resumen: {{summary}}',
186
+ 'Revisión completa: {{pr}}',
187
+ 'Tu siguiente paso: {{command}}',
188
+ ],
189
+ approved: [
190
+ '@{{recipient}} #{{issue}} fue aprobada por {{reviewer}} ✅',
191
+ '',
192
+ 'Pull request: {{pr}}',
193
+ 'Tu siguiente paso: {{command}}',
194
+ ],
195
+ needs_reviewer: [
196
+ '@{{spec_writer}} #{{issue}} "{{title}}" no tiene revisor disponible',
197
+ '',
198
+ 'Pull request: {{pr}}',
199
+ 'Tu siguiente paso: asigna un revisor o pide que alguien lo tome.',
200
+ ],
201
+ review_reminder: [
202
+ '@{{reviewer}} recordatorio: #{{issue}} lleva {{hours}} horas esperando tu revisión',
203
+ '',
204
+ 'Pull request: {{pr}}',
205
+ ],
206
+ review_escalation: [
207
+ '@{{spec_writer}} #{{issue}} lleva {{hours}} horas sin revisión de @{{reviewer}}',
208
+ '',
209
+ 'Pull request: {{pr}}',
210
+ 'Tu siguiente paso: decide quién lo revisa.',
211
+ ],
212
+ cloud_not_configured: [
213
+ '#{{issue}} está listo, pero la nube no puede construirlo: este repositorio no tiene una credencial del modelo',
214
+ '',
215
+ '@{{actor}} tu siguiente paso: ejecuta `/relevo:go {{issue}}` en tu máquina, o agrega el secreto ANTHROPIC_API_KEY o CLAUDE_CODE_OAUTH_TOKEN para que la nube pueda construirlo. El issue sigue en ready.',
216
+ ],
217
+ no_milestone: [
218
+ '#{{issue}} no se inició: no tiene milestone',
219
+ '',
220
+ '@{{actor}} tu siguiente paso: agrega el issue a un milestone y márcalo como ready otra vez.',
221
+ ],
222
+ no_assignee: [
223
+ '#{{issue}} no se inició: no tiene responsable',
224
+ '',
225
+ '@{{actor}} tu siguiente paso: asigna el issue a quien lo va a revisar y márcalo como ready otra vez.',
226
+ ],
227
+ ready_denied: [
228
+ '#{{issue}} no se inició: solo quienes tienen acceso de escritura pueden marcar un issue como ready, así que se quitó la etiqueta',
229
+ 'Aplicada por: @{{actor}}',
230
+ ],
231
+ ready_denied_maintainer: [
232
+ '#{{issue}} no se inició: este repositorio es público, así que solo quienes mantienen el proyecto pueden marcar un issue como ready, y se quitó la etiqueta',
233
+ 'Aplicada por: @{{actor}}',
234
+ ],
235
+ blocked_notice: [
236
+ '#{{issue}} "{{title}}" está bloqueado',
237
+ '',
238
+ 'Espera a que se fusionen {{blockers}}. Empieza solo cuando lo estén.',
239
+ ],
240
+ queued_capacity: [
241
+ '#{{issue}} "{{title}}" está en cola',
242
+ '',
243
+ '{{assignee}} ya tiene {{built}} issues construidos esperando revisión (el límite es {{limit}}). Empieza solo cuando se revise uno.',
244
+ ],
245
+ queued_agents: [
246
+ '#{{issue}} "{{title}}" está en cola',
247
+ '',
248
+ 'Se alcanzó el límite de agentes simultáneos. Empieza solo cuando termine una ejecución.',
249
+ ],
250
+ fix_done: [
251
+ '#{{issue}} "{{title}}" corregido (iteración {{iteration}})',
252
+ '',
253
+ 'Qué cambió: {{summary}}',
254
+ 'QA: {{qa}}',
255
+ 'Commit: {{commit}}',
256
+ '@{{recipient}} {{ask}}',
257
+ ],
258
+ },
259
+ };
260
+
261
+ export const MESSAGE_KINDS = Object.keys(REQUIRED);
262
+
263
+ // ", N of M tasks are done" for the work that was kept, when the numbers are known.
264
+ function tasksInfo(values, es) {
265
+ if (!present(values.tasks_done) || !present(values.tasks_total)) return '';
266
+ return es ? ` (${values.tasks_done} de ${values.tasks_total} tareas hechas)` : ` (${values.tasks_done} of ${values.tasks_total} tasks done)`;
267
+ }
268
+
269
+ function present(value) {
270
+ return value !== undefined && value !== null && String(value).trim() !== '';
271
+ }
272
+
273
+ function shorten(value) {
274
+ const text = String(value);
275
+ return text.length > MAX_VALUE_LENGTH ? `${text.slice(0, MAX_VALUE_LENGTH)}… (truncated; see the full log)` : text;
276
+ }
277
+
278
+ export function renderMessage(kind, data = {}, language = 'en') {
279
+ if (!REQUIRED[kind]) throw new Error(`unknown message kind "${kind}"`);
280
+ const values = { ...data };
281
+ REQUIRED[kind].forEach((field) => {
282
+ if (!present(values[field])) throw new Error(`message "${kind}" needs "${field}"`);
283
+ });
284
+ const language_ = pickLanguage(language);
285
+ const es = language_ === 'es';
286
+ if (kind === 'built' && present(values.issue_hash)) values.issue_hash_short = shortHash(values.issue_hash);
287
+
288
+ // The next step comes from one table. needs-spec depends on why the issue came back, qa-failed on what the branch holds.
289
+ if (STATE_OF[kind]) {
290
+ const reason = kind === 'needs_spec' ? values.reason ?? 'missing' : kind === 'qa_failed' ? values.recovery ?? (values.branch_state === 'kept' ? 'kept' : 'fix') : null;
291
+ const step = nextStep({ state: STATE_OF[kind], reason, issue: values.issue, language: language_ });
292
+ if (!present(values.command) && step.command) values.command = step.command;
293
+ if (kind === 'needs_spec') values.next_line = `${nextStepPrefix(language_)}: ${step.command}: ${step.hint}.`;
294
+ if (kind === 'qa_failed') {
295
+ // For kept work the next step says what the command does and the other way forward; the others are the bare command.
296
+ const kept = step.reason === 'kept';
297
+ values.next_line = step.command ? `${nextStepPrefix(language_)}: ${values.command ?? step.command}${kept ? ` (${step.hint})` : ''}` : `${nextStepPrefix(language_)}: ${step.hint}.`;
298
+ }
299
+ }
300
+ if (kind === 'qa_failed') {
301
+ const location = present(values.file) ? `${values.file}${present(values.line) ? `:${values.line}` : ''}` : '';
302
+ values.location_suffix = location ? ` (${location})` : '';
303
+ // `pushed: false` is the older way to say that the branch was not pushed because of a credential or a marker.
304
+ const state = values.branch_state ?? (values.pushed === false ? 'unsafe' : 'pushed');
305
+ if (!['pushed', 'unsafe', 'not_pushed', 'existing', 'kept'].includes(state)) throw new Error(`message "qa_failed" has an unknown branch_state "${state}"`);
306
+ if (state === 'existing' && !present(values.recovery)) throw new Error('message "qa_failed" needs "recovery" when the branch already existed');
307
+ const branchLines = {
308
+ pushed: es
309
+ ? `La rama ${values.branch} está subida; el estado no avanzó y no se abrió ningún pull request.`
310
+ : `The branch ${values.branch} is pushed; the state did not advance and no pull request was opened.`,
311
+ unsafe: es
312
+ ? `La rama ${values.branch} NO se subió (se encontró una posible credencial, un marcador temporal o una ruta protegida); el estado no avanzó y no se abrió ningún pull request.`
313
+ : `The branch ${values.branch} was NOT pushed (a possible credential, a temporary marker or a protected path was found); the state did not advance and no pull request was opened.`,
314
+ not_pushed: es
315
+ ? `La rama ${values.branch} no se subió; el estado no avanzó y no se abrió ningún pull request.`
316
+ : `The branch ${values.branch} was not pushed; the state did not advance and no pull request was opened.`,
317
+ kept: es
318
+ ? `La rama ${values.branch} tiene el trabajo que QA no aprobó${tasksInfo(values, es)}; se guardó como trabajo en curso para que no se pierda. El estado no avanzó y no se abrió ningún pull request.`
319
+ : `The branch ${values.branch} holds the work that QA did not approve${tasksInfo(values, es)}; it was kept as work in progress so nothing is lost. The state did not advance and no pull request was opened.`,
320
+ existing: es
321
+ ? `La rama ${values.branch} ya existía en el remoto, así que no se tocó; el estado no avanzó y no se abrió ningún pull request.`
322
+ : `The branch ${values.branch} already existed on the remote, so it was not touched; the state did not advance and no pull request was opened.`,
323
+ };
324
+ values.branch_line = branchLines[state];
325
+ values.reason_suffix = present(values.reason) ? ` (${values.reason})` : '';
326
+ }
327
+
328
+ const lines = CATALOG[pickLanguage(language)][kind];
329
+ const out = [];
330
+ lines.forEach((template) => {
331
+ const names = [...template.matchAll(/\{\{(\w+)\}\}/g)].map((m) => m[1]);
332
+ const optionalEmpty = names.some((n) => !present(values[n]) && !n.endsWith('_suffix'));
333
+ if (names.length > 0 && optionalEmpty) return;
334
+ out.push(template.replace(/\{\{(\w+)\}\}/g, (_, n) => (n === 'checklist' ? String(values[n] ?? '') : shorten(values[n] ?? ''))));
335
+ });
336
+
337
+ // Collapse repeated blank lines and trim the ends.
338
+ const collapsed = out.filter((l, i) => !(l === '' && (out[i - 1] === '' || i === 0)));
339
+ while (collapsed.length > 0 && collapsed[collapsed.length - 1] === '') collapsed.pop();
340
+ return collapsed.join('\n');
341
+ }
@@ -0,0 +1,153 @@
1
+ // The next step of an issue, from one table, in the language of the repository.
2
+ // Implements "The next step comes from one source" of the issue-lifecycle specification.
3
+ //
4
+ // Every message and report that tells a person what to do next asks this table, so the command, who runs it and
5
+ // the short hint cannot drift apart. A test checks that every lifecycle state has an entry.
6
+
7
+ import { pickLanguage } from './language.mjs';
8
+
9
+ // command: the command to run (`{n}` is the issue number), or null when nobody has to act yet.
10
+ // who: the role that runs it; the words for each role are in WHO.
11
+ const TABLE = {
12
+ ready: {
13
+ command: null,
14
+ who: 'nobody',
15
+ hint: { en: 'the agent starts it by itself', es: 'el agente la empieza solo' },
16
+ },
17
+ queued: {
18
+ command: null,
19
+ who: 'nobody',
20
+ hint: { en: 'it starts when the assignee has capacity', es: 'empieza cuando la persona asignada tenga capacidad' },
21
+ },
22
+ building: {
23
+ command: null,
24
+ who: 'nobody',
25
+ hint: { en: 'the agent is building it', es: 'el agente la está construyendo' },
26
+ },
27
+ blocked: {
28
+ command: null,
29
+ who: 'nobody',
30
+ hint: { en: 'it starts when the issues it depends on are merged', es: 'empieza cuando se fusionen los issues de los que depende' },
31
+ },
32
+ built: {
33
+ command: '/relevo:review {n}',
34
+ who: 'assignee',
35
+ hint: { en: 'review it on a device', es: 'revísala en un dispositivo' },
36
+ },
37
+ 'in-review': {
38
+ command: '/relevo:review {n}',
39
+ who: 'assignee',
40
+ hint: { en: 'continue the review; if something fails, run /relevo:fix {n}', es: 'continúa la revisión; si algo falla, ejecuta /relevo:fix {n}' },
41
+ },
42
+ 'pr-open': {
43
+ command: '/relevo:ship {n}',
44
+ who: 'approver',
45
+ hint: { en: 'after someone other than the author approves the pull request on GitHub', es: 'cuando alguien que no sea el autor apruebe el pull request en GitHub' },
46
+ },
47
+ 'needs-spec': {
48
+ // The reason decides what the spec writer has to do.
49
+ missing: {
50
+ command: '/relevo:issue {n}',
51
+ who: 'spec_writer',
52
+ hint: { en: 'complete what is missing, then label it ready again', es: 'completa lo que falta y vuelve a ponerle ready' },
53
+ },
54
+ ambiguous: {
55
+ command: '/relevo:issue {n}',
56
+ who: 'spec_writer',
57
+ hint: {
58
+ en: 'decide the open question (run /relevo:think #{n} first if you need to explore it), then label it ready again',
59
+ es: 'decide la pregunta abierta (ejecuta antes /relevo:think #{n} si necesitas explorarla) y vuelve a ponerle ready',
60
+ },
61
+ },
62
+ },
63
+ 'qa-failed': {
64
+ // The recovery depends on what is on the branch.
65
+ fix: {
66
+ command: '/relevo:fix {n}',
67
+ who: 'assignee',
68
+ hint: { en: 'describe what QA found', es: 'describe lo que encontró QA' },
69
+ },
70
+ // The branch holds the work that QA rejected, committed as work in progress: correct it, or rethink the issue.
71
+ kept: {
72
+ command: '/relevo:fix {n}',
73
+ who: 'assignee',
74
+ hint: {
75
+ en: 'it checks and corrects the work that was kept; to rethink the issue instead, use /relevo:think and then /relevo:issue {n}',
76
+ es: 'verifica y corrige el trabajo que se guardó; para repensar el issue, usa /relevo:think y luego /relevo:issue {n}',
77
+ },
78
+ },
79
+ 'delete-branch': {
80
+ command: null,
81
+ who: 'assignee',
82
+ hint: {
83
+ en: 'the branch holds only the specification: delete the remote branch and label the issue ready again',
84
+ es: 'la rama solo tiene la especificación: borra la rama remota y vuelve a ponerle ready al issue',
85
+ },
86
+ },
87
+ },
88
+ 'changes-requested': {
89
+ command: '/relevo:fix {n}',
90
+ who: 'author',
91
+ hint: { en: 'fix what the reviewer asked for', es: 'corrige lo que pidió quien revisa' },
92
+ },
93
+ 'needs-reviewer': {
94
+ command: null,
95
+ who: 'spec_writer',
96
+ hint: { en: 'assign a reviewer or ask someone to take it', es: 'asigna a alguien que revise o pide que alguien lo tome' },
97
+ },
98
+ };
99
+
100
+ const WHO = {
101
+ en: { assignee: 'the assignee', spec_writer: 'the spec writer', author: 'the author', approver: 'whoever merges', nobody: '' },
102
+ es: { assignee: 'la persona asignada', spec_writer: 'quien escribe las specs', author: 'el autor', approver: 'quien fusiona', nobody: '' },
103
+ };
104
+
105
+ const PREFIX = {
106
+ en: { act: 'Your next step', wait: 'Nothing to do now' },
107
+ es: { act: 'Tu siguiente paso', wait: 'Nada que hacer ahora' },
108
+ };
109
+
110
+ /** The states that have an entry: every lifecycle state. */
111
+ export const NEXT_STEP_STATES = Object.keys(TABLE);
112
+
113
+ /** Reasons for the states whose next step depends on one. */
114
+ export const NEXT_STEP_REASONS = {
115
+ 'needs-spec': ['missing', 'ambiguous'],
116
+ 'qa-failed': ['fix', 'kept', 'delete-branch'],
117
+ };
118
+
119
+ const fill = (text, issue) => String(text).replaceAll('{n}', String(issue ?? ''));
120
+
121
+ /**
122
+ * The next step for an issue in `state`.
123
+ * `reason` picks the variant of `needs-spec` (`missing` by default, or `ambiguous`) and of `qa-failed`
124
+ * (`fix` by default, `kept` when the branch holds the work that QA rejected, or `delete-branch` when the branch holds only the specification).
125
+ * Returns { state, reason, command, who, hint, line }; `command` is null when nobody has to act yet.
126
+ */
127
+ export function nextStep({ state, reason = null, issue = null, language = 'en' } = {}) {
128
+ const entry = TABLE[state];
129
+ if (!entry) throw new Error(`unknown state "${state}": expected one of ${NEXT_STEP_STATES.join(', ')}`);
130
+ const variants = NEXT_STEP_REASONS[state];
131
+ let chosen = entry;
132
+ let used = null;
133
+ if (variants) {
134
+ used = reason ?? variants[0];
135
+ chosen = entry[used];
136
+ if (!chosen) throw new Error(`unknown reason "${reason}" for "${state}": expected one of ${variants.join(', ')}`);
137
+ }
138
+ const lang = pickLanguage(language);
139
+ const needsIssue = [chosen.command, chosen.hint.en].some((t) => String(t).includes('{n}'));
140
+ if (needsIssue && (issue === null || issue === undefined || issue === '')) throw new Error(`the next step of "${state}" needs the issue number`);
141
+
142
+ const command = chosen.command === null ? null : fill(chosen.command, issue);
143
+ const hint = fill(chosen.hint[lang], issue);
144
+ const who = WHO[lang][chosen.who];
145
+ const person = who ? ` (${who})` : '';
146
+ const line = command === null
147
+ ? `${chosen.who === 'nobody' ? PREFIX[lang].wait : PREFIX[lang].act}: ${hint}${chosen.who === 'nobody' ? '' : person}.`
148
+ : `${PREFIX[lang].act}: ${command}${person}: ${hint}.`;
149
+ return { state, reason: used, command, who: chosen.who, hint, line };
150
+ }
151
+
152
+ /** The words that open a next-step line in a language ("Your next step"), for messages that build their own line. */
153
+ export const nextStepPrefix = (language) => PREFIX[pickLanguage(language)].act;
@@ -0,0 +1,44 @@
1
+ // The OpenSpec commands each part of Relevo may use. Implements "OpenSpec commands come from a closed list" of the
2
+ // go specification. A test (tests/openspec-commands.test.mjs) compares this table with the text of the skill and the
3
+ // agents, so a command cannot be added to a prompt without being added here, on purpose.
4
+ //
5
+ // Commands are written the way `openspecSubcommand` normalises them:
6
+ // `--version`, `list`, `new change`, `status`, `validate`, `instructions apply`, `instructions <artifact>`.
7
+
8
+ export const OPENSPEC_COMMANDS = {
9
+ go: ['--version', 'list', 'new change', 'status', 'instructions <artifact>', 'instructions apply', 'validate'],
10
+ // The developer and QA only read the state of the change; they never create or archive anything.
11
+ dev: ['status', 'instructions apply'],
12
+ qa: ['status', 'instructions apply'],
13
+ // `think` and `issue` only read: the specifications and the changes that exist.
14
+ think: ['list', 'show', 'status'],
15
+ issue: ['list', 'show'],
16
+ // `review` only finds the change folder.
17
+ review: ['list'],
18
+ };
19
+
20
+ /** Subcommands that no unattended run may use, whatever the list says. */
21
+ export const NEVER = ['init', 'archive', 'update', 'sync'];
22
+
23
+ /**
24
+ * Normalise what follows `openspec` in a command into the form used by the table.
25
+ * `['instructions', 'apply', '--change', 'x']` -> `instructions apply`; `['instructions', 'specs']` -> `instructions <artifact>`.
26
+ * Returns null when there is nothing to normalise.
27
+ */
28
+ export function openspecSubcommand(tokens) {
29
+ const [first, second] = tokens.filter(Boolean);
30
+ if (!first) return null;
31
+ if (first === '--version' || first === '-V') return '--version';
32
+ if (first === 'new') return second === 'change' ? 'new change' : 'new';
33
+ if (first === 'instructions') {
34
+ if (!second || second.startsWith('-')) return 'instructions';
35
+ return ['apply', 'archive'].includes(second) ? `instructions ${second}` : 'instructions <artifact>';
36
+ }
37
+ return first;
38
+ }
39
+
40
+ /** Is the command (the tokens after `openspec`) allowed for `part` (`go`, `dev`, `qa`, `think`, `issue` or `review`)? */
41
+ export function isAllowed(part, tokens) {
42
+ const sub = openspecSubcommand(tokens);
43
+ return Boolean(sub) && (OPENSPEC_COMMANDS[part] ?? []).includes(sub);
44
+ }
@@ -0,0 +1,16 @@
1
+ // Reads the JSON of `openspec status --change <id> --json`.
2
+ // Implements "The specification is complete before it is validated" of the go specification.
3
+
4
+ /** Artifacts that are neither done nor skipped, in the order OpenSpec lists them. */
5
+ export function pendingArtifacts(status) {
6
+ return (status?.artifacts ?? []).filter((a) => !['done', 'skipped'].includes(a.status)).map((a) => a.id);
7
+ }
8
+
9
+ /**
10
+ * Is the specification of the change complete? True when it has artifacts and every one is `done` or `skipped`
11
+ * (a change that declares `skip_specs` has its specs artifact `skipped`, and that is complete).
12
+ */
13
+ export function planningComplete(status) {
14
+ const artifacts = status?.artifacts ?? [];
15
+ return artifacts.length > 0 && pendingArtifacts(status).length === 0;
16
+ }