Comando y banda de marcha para la sesion de main.

Marchas guarda cada cambio explícito de marcha asociado al ID de la sesión. Los registros de más de 30 días se eliminan la próxima vez que el mod inicia una sesión. La retención de 30 días se cuenta desde el comando /marcha <marcha> más reciente; una sesión retomada cuya clave haya vencido empieza en Medium.
Las versiones anteriores guardaban solo el nombre de la marcha, sin fecha. Esas entradas heredadas se conservan porque no se puede determinar su antigüedad con seguridad; Medium heredado se trata como si no se hubiera cambiado la marcha, porque las versiones anteriores también lo guardaban por defecto.
Después de una compactación, Marchas restaura la postura actual cuando se ejecuta classic.SessionStart. Si ese hook no está disponible, la banda recuerda que hay que repetir /marcha <marcha>.
Volver con rewind a un punto anterior al cambio de marcha puede quitar el mensaje de postura aunque el store del plugin y la banda sigan mostrando la marcha más nueva. Este comportamiento se informa en el issue #163 y las APIs de sesión actuales no lo reconcilian.
Marchas ajusta el esfuerzo de cada solicitud de main sin cambiar el esfuerzo configurado para la sesión: Lazy pone un máximo de medium, Medium no lo reescribe y Proactive pone un mínimo de xhigh. Las solicitudes de subagentes quedan intactas. La respuesta visible de /marcha nombra el límite que aplica.
Un comando /effort <nivel> manual gana hasta el siguiente cambio de marcha y pasa a ser la nueva base, incluso si la persona repite el mismo nivel. /effort ultracode es un cambio de flujo de trabajo, no un ajuste manual del esfuerzo. /model tampoco es un ajuste manual: Marchas vuelve a registrar la base observada para el nuevo modelo. La base y el ajuste manual se guardan junto con la marcha para que sigan vigentes al reanudar la sesión.
El nivel que se elige en el menú de /effort (y el resultado de /effort auto) no le llega al mod: Marchas solo ve el esfuerzo de cada solicitud. Por eso un esfuerzo que cambia cuenta como ajuste manual solo si se abrió el menú o se ejecutó /effort auto después del último /marcha <marcha>; si cambia sin un /effort desde ese cambio de marcha, no es manual y la marcha lo sigue ajustando. Elegir en el menú el mismo nivel que ya estaba no cuenta como ajuste manual, porque el mod no puede distinguirlo de cerrar el menú sin elegir. Queda un caso que el mod no puede distinguir: si elegís un nivel en el menú, cambiás de marcha y después abrís el menú y lo cancelás, el nivel que habías elegido antes se toma como ajuste manual.
Marchas avisa en una línea cuando el modo autónomo coincide con algo que cambia lo que pasa sin supervisión. El aviso es una nota del transcript que el modelo no lee; si el transcript la rechaza, sale como toast.
/autonomo en Lazy: los items que no estén especificados van a esperar un cambio de marcha./autonomo en Proactive o con ultracode activo, o /marcha proactive o /effort ultracode con el modo autónomo ya activo: qué puede costar más sin supervisión (la exploración, las consultas y la vigilancia de Proactive; el workflow de ultracode para cada tarea sustantiva, sin límite de concurrencia) y cómo bajarlo (/marcha medium, o apagar ultracode).Marchas lee el valor isOn de modo-autonomo y se entera de que el modo se encendió cuando ese mod lo escribe, sin depender del orden entre los dos mods. Sigue ultracode con /effort ultracode y con los attachments ultra_effort_enter y ultra_effort_exit: gana el último. Un attachment no avisa, porque al reanudar se vuelven a pedir los del historial. Ultracode dura un proceso, así que una sesión reanudada empieza sin ultracode. Después de /clear el mod no ve ultracode hasta el siguiente /effort ultracode o attachment, y el aviso de Proactive dice que no lo ve. En una organización que administra Claude Code los attachments no le llegan al mod, que entonces solo ve /effort ultracode.
hooks/register.tsx 374 lines1import type { EngineInterface, Register } from 'claude-code'
2
3import type { Gear } from '../types'
4
5const GEARS: Gear[] = ['lazy', 'medium', 'proactive']
6const COLORS: Record<Gear, string> = { lazy: 'ide', medium: 'success', proactive: 'warning' }
7const KEY_PREFIX = 'marchas:'
8const RETENTION_MS = 30 * 24 * 60 * 60 * 1000
9const EFFORTS = ['low', 'medium', 'high', 'xhigh', 'max'] as const
10type Effort = typeof EFFORTS[number]
11const marker = (gear: Gear): string => {
12 const effort = {
13 lazy: 'esfuerzo m\u00e1ximo: medium',
14 medium: 'sin reescritura de esfuerzo',
15 proactive: 'esfuerzo m\u00ednimo: xhigh',
16 }[gear]
17
18 return `Marcha: ${gear} (${effort})`
19}
20const sessionKey = async ($: EngineInterface): Promise<string> => `marchas:${await $.session.id()}`
21// modo-autonomo's own value: any plugin reads another's `$.state`, only its owner writes it.
22const AUTONOMOUS = { plugin: 'modo-autonomo', key: 'isOn' } as const
23// Ultracode is per process and `$.state` lasts one: unset (after `/clear`) means the mod cannot see it.
24const ULTRACODE = { plugin: 'marchas', key: 'ultracode' } as const
25const LAZY_WARNING = 'Modo aut\u00f3nomo en Lazy: los items que no est\u00e9n especificados van a esperar un cambio de marcha.'
26
27const costWarning = (gear: Gear, ultracode: boolean | undefined): string | undefined => {
28 const proactive = gear === 'proactive'
29 if (!proactive && ultracode !== true) return undefined
30 const costs = [
31 ...(proactive ? ['la exploraci\u00f3n, las consultas y la vigilancia de Proactive'] : []),
32 ...(ultracode === true ? ['el workflow de ultracode para cada tarea sustantiva, sin l\u00edmite de concurrencia'] : []),
33 ]
34 const blind = proactive && ultracode === undefined ? ' No puedo ver si ultracode est\u00e1 activo.' : ''
35 const fixes = [...(ultracode === true ? ['apag\u00e1 ultracode'] : []), ...(proactive ? ['/marcha medium'] : [])]
36
37 return `Modo aut\u00f3nomo, lo que puede costar m\u00e1s sin supervisi\u00f3n: ${costs.join('; ')}.${blind} Para bajarlo: ${fixes.join(' o ')}`
38}
39
40// A transcript notice the model never reads; a toast if the notice is refused.
41const warn = async ($: EngineInterface, text: string | undefined): Promise<void> => {
42 if (text === undefined) return
43 const appended = await $.session.append({ message: { type: 'system', content: [{ type: 'text', text }] } }).catch(() => undefined)
44 if (appended?.uuid === undefined) $.ui.toast(text)
45}
46
47const autonomous = async ($: EngineInterface): Promise<boolean> => (await $.state.get(AUTONOMOUS)).value === true
48const isGear = (value: unknown): value is Gear => GEARS.includes(value as Gear)
49const isEffort = (value: unknown): value is Effort => EFFORTS.includes(value as Effort)
50type GearState = {
51 gear: Gear
52 shiftedAt: number
53 needsReminder?: boolean
54 effortBaseline?: Effort
55 effortModel?: string
56 effortOverride?: boolean
57 modelChanged?: boolean
58 // `/effort` opened its menu (or ran `auto`) since the last shift: its result never reaches the mod (#194).
59 effortPicker?: boolean
60}
61
62const gearState = (value: unknown): GearState | undefined => {
63 if (typeof value !== 'object' || value === null) return undefined
64 const state = value as Partial<GearState>
65
66 return isGear(state.gear) && typeof state.shiftedAt === 'number'
67 ? {
68 gear: state.gear,
69 shiftedAt: state.shiftedAt,
70 needsReminder: state.needsReminder === true,
71 ...(isEffort(state.effortBaseline) ? { effortBaseline: state.effortBaseline } : {}),
72 ...(typeof state.effortModel === 'string' ? { effortModel: state.effortModel } : {}),
73 effortOverride: state.effortOverride === true,
74 modelChanged: state.modelChanged === true,
75 effortPicker: state.effortPicker === true,
76 }
77 : undefined
78}
79
80const shiftedGear = (value: unknown): Gear | undefined => {
81 const state = gearState(value)
82 if (state !== undefined) return state.gear
83
84 // Older versions wrote Medium at session start, so legacy Medium is ambiguous.
85 return isGear(value) && value !== 'medium' ? value : undefined
86}
87
88const pruneOldKeys = async ($: EngineInterface): Promise<void> => {
89 const now = await $.clock.now()
90 for (const key of await $.store.keys()) {
91 if (!key.startsWith(KEY_PREFIX)) continue
92 const state = gearState(await $.store.get(key))
93 if (state !== undefined && now - state.shiftedAt > RETENTION_MS) {
94 await $.store.delete(key)
95 }
96 }
97}
98
99const posture = (gear: Gear): string => {
100 const opening = [
101 'Este mensaje de marcha reemplaza cualquier postura de marcha anterior.',
102 'En tu pr\u00f3ximo mensaje, empez\u00e1 con una l\u00ednea que nombre la marcha y qu\u00e9 cambia ahora.',
103 ]
104 const body: Record<Gear, string[]> = {
105 lazy: [
106 'El trabajo ya est\u00e1 especificado: el ticket cumple el contrato y lo que queda es despachar, vigilar y mergear (en modo un-agente, implementarlo). Le\u00e9 el ticket, sus comentarios y el estado del PR. Le\u00e9 c\u00f3digo solo hasta donde lo necesiten los chequeos del contrato, incluido el diff del PR frente a los hallazgos del review y los logs de los checks fallidos (en modo un-agente, hasta ubicar el cambio).',
107 'Hac\u00e9 tu propio trabajo inline; los reviews y verificadores requeridos siguen corriendo como subagentes. Corr\u00e9 el verificador independiente y el review de registro en modo un-agente con el esfuerzo que tendr\u00edan sin marcha. Omit\u00ed la investigaci\u00f3n opcional y el desaf\u00edo de premisas de ODD; los disparadores propios del contrato siguen vigentes.',
108 'Asum\u00ed los detalles de ejecuci\u00f3n y decilo; una regla o un criterio ambiguo en el ticket significa que est\u00e1 incompleto, no que haya que asumirlo. Quedate dentro del pedido: inform\u00e1 en una l\u00ednea cualquier problema que veas, pero no propongas seguimientos. Narr\u00e1 una l\u00ednea por evento y cerr\u00e1 breve pero completo. No propongas arena ni swarm.',
109 'Cuando un item deje de estar especificado (haya que escribir o completar su ticket, la exploraci\u00f3n no entre en un solo lote inline, falle la prueba de reanudaci\u00f3n, un requisito se vuelva problem\u00e1tico o aparezca una decisi\u00f3n de dise\u00f1o), decilo en una l\u00ednea y suger\u00ed subir de marcha en vez de estirarlo. Hasta que alguien cambie la marcha, ese item espera y segu\u00eds con el resto; ninguna regla fija se suspende.',
110 ],
111 medium: [
112 'Trabaj\u00e1 seg\u00fan ODD y el contrato.',
113 'Propon\u00e9 arena o swarm cuando un requisito muestre se\u00f1ales de la secci\u00f3n "Arena y swarm" del contrato.',
114 ],
115 proactive: [
116 'Mir\u00e1 m\u00e1s all\u00e1 del pedido. Antes de actuar, hac\u00e9 que subagentes exploren su entorno (llamadores, vecinos, issues abiertos relacionados; primero ADRs y glosario, Engram como pista) para que riesgos y oportunidades aparezcan antes de causar problemas, y reserv\u00e1 tu contexto para orquestar; deleg\u00e1 temprano las unidades independientes.',
117 'Propon\u00e9 investigar cada vez que un dato externo afecte una decisi\u00f3n y desafi\u00e1 cada premisa de alto impacto. Adem\u00e1s de las consultas requeridas, consult\u00e1 a Gentle Shell antes de escribir tickets, antes de despachar una tanda y cuando desaf\u00edes una premisa. Propon\u00e9 seguimientos, arreglos cercanos y tickets que valga la pena escribir en tu informe o en el mapa o issue paraguas, nunca en un ticket por despachar, y nunca crees issues.',
118 'Colabor\u00e1 con los workers de implementaci\u00f3n hasta que abran su PR: vigil\u00e1 sus issues y PRs en busca de preguntas; respond\u00e9 las preguntas de ejecuci\u00f3n donde las hayan hecho y tocales el timbre en su terminal, solo mientras su turno est\u00e9 en curso, con una l\u00ednea que apunte a la respuesta; llev\u00e1 las de producto e interpretaci\u00f3n al humano y transmit\u00ed la respuesta del mismo modo; avisales lo que cambiar\u00eda su trabajo y no pueden ver. Nunca los hagas esperar, nunca corrijas su c\u00f3digo, y dej\u00e1 en paz a los revisores.',
119 'Propon\u00e9 arena o swarm temprano, ante se\u00f1ales m\u00e1s d\u00e9biles, antes de que aparezca un problema.',
120 'Cont\u00e1 qu\u00e9 encontraste m\u00e1s all\u00e1 del pedido y por qu\u00e9 importa. Vigil\u00e1 de cerca y propon\u00e9 un handoff antes de que se llene tu contexto.',
121 ],
122 }
123
124 return [...opening, ...body[gear]].join('\n\n')
125}
126
127const mainCheckout = async ($: EngineInterface): Promise<string | undefined> => {
128 try {
129 const result = await $.process.run(
130 ['git', 'rev-parse', '--path-format=absolute', '--git-dir', '--git-common-dir', '--show-toplevel'],
131 { timeoutMs: 10_000 },
132 )
133 if (result.exitCode !== 0) {
134 return undefined
135 }
136 const [gitDir, commonDir, top] = result.stdout.split(/\r?\n/).map(path => path.trim())
137 const slashed = (path: string | undefined): string => (path ?? '').replace(/\\/g, '/').replace(/\/+$/, '')
138 if (gitDir === undefined || commonDir === undefined || slashed(gitDir).toLowerCase() !== slashed(commonDir).toLowerCase()) {
139 return undefined
140 }
141 const common = slashed(commonDir)
142
143 return common.endsWith('/.git') ? common.slice(0, -'/.git'.length) : slashed(top)
144 } catch {
145 return undefined
146 }
147}
148
149const enabled = async ($: EngineInterface): Promise<boolean> => {
150 const root = await mainCheckout($)
151 if (root === undefined) {
152 return false
153 }
154 if (!(await $.fs.exists(`${root}/.orca-stack.json`)) && !(await $.fs.exists(`${root}/.orca-stack.local.json`))) {
155 return false
156 }
157 try {
158 const agentsPath = `${root}/AGENTS.md`
159 if (!(await $.fs.exists(agentsPath))) {
160 return false
161 }
162 const agents = await $.fs.read(agentsPath)
163 const contract = /<!-- orca-stack:begin -->([\s\S]*?)<!-- orca-stack:end -->/i.exec(agents)?.[1]
164
165 return contract !== undefined && /^#{1,6}\s+Marchas\s*$/mi.test(contract)
166 } catch {
167 return false
168 }
169}
170
171const currentGear = async ($: EngineInterface): Promise<Gear> => {
172 const stored = await $.store.get(await sessionKey($))
173
174 return shiftedGear(stored) ?? 'medium'
175}
176
177const saveEffortState = async ($: EngineInterface, value: unknown, changes: Partial<GearState>): Promise<void> => {
178 const state = gearState(value)
179 await $.store.set(await sessionKey($), {
180 gear: shiftedGear(value) ?? 'medium',
181 shiftedAt: state?.shiftedAt ?? await $.clock.now(),
182 needsReminder: state?.needsReminder ?? false,
183 ...state,
184 ...changes,
185 } satisfies GearState)
186}
187
188export const register: Register = on => {
189 on('turn.step', async function* ($, e, next) {
190 if (!(await enabled($)) || e.agentId !== undefined || !isEffort(e.effort)) {
191 return yield* next(e)
192 }
193 const key = await sessionKey($)
194 const value = await $.store.get(key)
195 const gear = shiftedGear(value)
196 if (gear === undefined) return yield* next(e)
197 const state = gearState(value)
198 const changedModel = state?.modelChanged === true || (state?.effortModel !== undefined && state.effortModel !== e.model)
199 const changedEffort = state?.effortBaseline !== undefined && state.effortBaseline !== e.effort
200 const baseline = changedModel || changedEffort ? e.effort : state?.effortBaseline ?? e.effort
201 // A changed effort is manual only if `/effort` opened its menu since the last shift; one main step consumes the mark.
202 const effortOverride = changedModel
203 ? state?.effortOverride ?? false
204 : state?.effortOverride === true || (changedEffort && state?.effortPicker === true)
205 await $.store.set(key, {
206 gear,
207 shiftedAt: state?.shiftedAt ?? await $.clock.now(),
208 needsReminder: state?.needsReminder ?? false,
209 effortBaseline: baseline,
210 effortModel: e.model,
211 effortOverride,
212 modelChanged: false,
213 effortPicker: false,
214 } satisfies GearState)
215 if (gear === 'medium' || effortOverride) return yield* next(e)
216
217 const effort = gear === 'lazy'
218 ? EFFORTS.indexOf(baseline) > EFFORTS.indexOf('medium') ? 'medium' : baseline
219 : EFFORTS.indexOf(baseline) < EFFORTS.indexOf('xhigh') ? 'xhigh' : baseline
220
221 return yield* next({ ...e, effort })
222 })
223
224 on('session.start', async ($, e, next) => {
225 if (!(await enabled($))) {
226 return next(e)
227 }
228 await $.command.register({ name: 'marcha', description: 'Muestra o cambia la marcha de main', argumentHint: '[lazy|medium|proactive]' })
229 await pruneOldKeys($)
230 // A new process, a resume included, starts with ultracode off; a hot reload keeps what it saw.
231 if ((await $.state.get(ULTRACODE)).value === undefined) await $.state.set(ULTRACODE, false)
232
233 return next(e)
234 })
235
236 on('command.run', { command: 'marcha' }, async ($, e) => {
237 if (!(await enabled($))) {
238 return { text: '' }
239 }
240 const args = e.args.trim().toLowerCase()
241 if (args === '') {
242 return { text: marker(await currentGear($)) }
243 }
244 if (!GEARS.includes(args as Gear)) {
245 return { text: 'Marchas disponibles: lazy|medium|proactive' }
246 }
247 const gear = args as Gear
248 const before = await currentGear($)
249 const key = await sessionKey($)
250 const previous = gearState(await $.store.get(key))
251 await $.store.set(key, {
252 ...previous,
253 gear,
254 shiftedAt: await $.clock.now(),
255 needsReminder: false,
256 effortOverride: false,
257 modelChanged: previous?.modelChanged ?? false,
258 effortPicker: false,
259 } satisfies GearState)
260 if (gear === 'proactive' && before !== 'proactive' && await autonomous($)) {
261 await warn($, costWarning(gear, (await $.state.get(ULTRACODE)).value))
262 }
263
264 return { text: marker(gear), context: [posture(gear)] }
265 })
266
267 on('command.run', { command: 'effort' }, async ($, e, next) => {
268 const effort = e.args.trim().toLowerCase()
269 const active = await enabled($)
270 if (active && e.origin.kind === 'composer' && isEffort(effort)) {
271 const key = await sessionKey($)
272 const value = await $.store.get(key)
273 await saveEffortState($, value, {
274 effortBaseline: effort,
275 effortOverride: true,
276 modelChanged: false,
277 })
278 } else if (active && e.origin.kind === 'composer' && effort !== 'ultracode') {
279 await saveEffortState($, await $.store.get(await sessionKey($)), { effortPicker: true })
280 }
281 const result = await next(e)
282 if (active && effort === 'ultracode') {
283 const { value: before } = await $.state.get(ULTRACODE)
284 await $.state.set(ULTRACODE, true)
285 if (before !== true && await autonomous($)) await warn($, costWarning(await currentGear($), true))
286 }
287
288 return result
289 })
290
291 // The latest ultracode attachment tells its state; a resume asks the history's again, ending on exit (#153).
292 on('prompt.attachment', { type: ['ultra_effort_enter', 'ultra_effort_exit'] }, async ($, e, next) => {
293 if (await enabled($)) await $.state.set(ULTRACODE, e.type === 'ultra_effort_enter')
294
295 return next(e)
296 })
297
298 // modo-autonomo answers /autonomo itself, so the mod watches its write instead of the command.
299 on('state.set', { plugin: 'modo-autonomo', key: 'isOn' }, async ($, e, next) => {
300 const wasOn = await autonomous($)
301 const result = await next(e)
302 const landed = 'value' in result && result.value.isSet
303 if (e.value !== true || wasOn || !landed || !(await enabled($))) return result
304 const gear = await currentGear($)
305 await warn($, gear === 'lazy' ? LAZY_WARNING : undefined)
306 await warn($, costWarning(gear, (await $.state.get(ULTRACODE)).value))
307
308 return result
309 })
310
311 on('command.run', { command: 'model' }, async ($, e, next) => {
312 if (await enabled($) && e.origin.kind === 'composer') {
313 const value = await $.store.get(await sessionKey($))
314 await saveEffortState($, value, { modelChanged: true })
315 }
316
317 return next(e)
318 })
319
320 on('session.compact', async ($, e, next) => {
321 const result = await next(e)
322 if (!(await enabled($)) || ('skip' in result && result.skip !== undefined)) {
323 return result
324 }
325 const key = await sessionKey($)
326 const value = await $.store.get(key)
327 const gear = shiftedGear(value)
328 if (gear === undefined) return result
329 const state = gearState(value)
330 await $.store.set(key, {
331 ...state,
332 gear,
333 shiftedAt: state?.shiftedAt ?? await $.clock.now(),
334 needsReminder: true,
335 } satisfies GearState)
336
337 return result
338 })
339
340 on('classic.SessionStart', async ($, e, next) => {
341 const result = await next(e)
342 if (!(await enabled($)) || e.source !== 'compact') return result
343 const key = await sessionKey($)
344 const value = await $.store.get(key)
345 const gear = shiftedGear(value)
346 if (gear === undefined) return result
347 const state = gearState(value)
348 await $.store.set(key, {
349 ...state,
350 gear,
351 shiftedAt: state?.shiftedAt ?? await $.clock.now(),
352 needsReminder: false,
353 } satisfies GearState)
354
355 return {
356 ...result,
357 additionalContext: [...(result.additionalContext ?? []), posture(gear)],
358 }
359 })
360
361 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
362 if (!(await enabled($))) {
363 return next(e)
364 }
365 const { Text } = $.ui.resolve(e)
366 const gear = await currentGear($)
367 const state = gearState(await $.store.get(await sessionKey($)))
368 const label = gear[0].toUpperCase() + gear.slice(1)
369 const text = state?.needsReminder === true ? `${label} - repet\u00ed /marcha ${gear}` : label
370
371 return <Text color={COLORS[gear]}>{text}</Text>
372 })
373}
374types/index.d.ts 10 lines1export type Gear = 'lazy' | 'medium' | 'proactive'
2
3declare module 'claude-code' {
4 interface PluginState {
5 marchas: { ultracode: boolean }
6 // Read only: modo-autonomo owns and writes it (mods/modo-autonomo/types/index.d.ts).
7 'modo-autonomo': { isOn: boolean }
8 }
9}
10