@gotcos/glasses-server 6.44.3 → 6.44.4

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.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,22 @@
1
+ ## 6.44.4
2
+
3
+ Stage, a finish line, and no dispatch without one.
4
+
5
+ - A task row carries `stage` (planning, active or review) and `doneWhen`. Both
6
+ live in the tasks.md line, not a sidecar: the task id is a sha256 of the
7
+ normalized description, so an id-keyed store would orphan itself the first time
8
+ the text was edited. An unmarked row reads as planning, so nothing needed
9
+ migrating.
10
+ - `PATCH /api/tasks/:id` accepts `stage` and `doneWhen`.
11
+ - **`runTaskNow` refuses a task with no finish line**, `409 done_when_required`,
12
+ at the single choke point every dispatch passes through. An agent sent at a
13
+ task with no definition of done cannot succeed at it and cannot be judged to
14
+ have failed either.
15
+ - Fixes a round-trip bug older than this release: the writer joins end-of-line
16
+ markers with spaces while the reader's regex allowed none between them, so a
17
+ row carrying both a run time and an agent marker silently lost the run time on
18
+ every read.
19
+
1
20
  ## 6.44.3
2
21
 
3
22
  A task row carries enough to show and edit it.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gotcos/glasses-server",
3
- "version": "6.44.3",
3
+ "version": "6.44.4",
4
4
  "description": "COS Glasses \u2014 self-hosted AI heads-up-display server for Even G2 smart glasses, powered by Claude Code, Codex, Cursor Agent CLI, or local Ollama",
5
5
  "type": "module",
6
6
  "bin": {
@@ -358,6 +358,16 @@ export async function runTaskNow(id: string, domain: string, injected?: TaskDisp
358
358
  if (row.agent_state === 'running') {
359
359
  throw new TaskRunError(409, 'task_running', 'A run is already in flight for this task.')
360
360
  }
361
+ // No finish line, no dispatch. An agent sent at a task with no definition of
362
+ // done cannot succeed at it and cannot be judged to have failed either, so
363
+ // this fails closed at the one choke point every dispatch passes through.
364
+ if (!row.done_when || !row.done_when.trim()) {
365
+ throw new TaskRunError(
366
+ 409,
367
+ 'done_when_required',
368
+ 'Say what done looks like before running this task.',
369
+ )
370
+ }
361
371
  const runAt = parseRunAt(row.run_at)
362
372
  if (runAt && runAt.day > clock.day) {
363
373
  throw new TaskRunError(409, 'scheduled_future', 'Run now is not allowed on a future scheduled task.')
@@ -102,6 +102,12 @@ export interface BridgeTaskRow {
102
102
  agent_no: number | null
103
103
  section: string
104
104
  section_day: string | null
105
+ /** Workflow stage. null/absent means planning, the unmarked default.
106
+ * Optional so new server code against an older bridge degrades to planning
107
+ * rather than throwing during a partial deploy. */
108
+ stage?: 'planning' | 'active' | 'review' | null
109
+ /** The finish line. A dispatch is refused while this is empty. */
110
+ done_when?: string | null
105
111
  }
106
112
 
107
113
  export interface TaskFlags {
@@ -112,6 +118,10 @@ export interface TaskFlags {
112
118
  carriedOver: boolean
113
119
  }
114
120
 
121
+ /** Board stage. Planning is the default and carries no marker in tasks.md. */
122
+ export type TaskStage = 'planning' | 'active' | 'review'
123
+ export const TASK_STAGES: readonly TaskStage[] = ['planning', 'active', 'review']
124
+
115
125
  export interface TaskBoardRow {
116
126
  id: string
117
127
  ref: string
@@ -131,6 +141,9 @@ export interface TaskBoardRow {
131
141
  runAt?: string
132
142
  section: string
133
143
  sectionDay?: string
144
+ stage: TaskStage
145
+ /** Absent until someone says what finished looks like. Gates `run`. */
146
+ doneWhen?: string
134
147
  due: boolean
135
148
  missed: boolean
136
149
  failed: boolean
@@ -414,6 +427,8 @@ export function projectRow(
414
427
  ...(runAt ? { runAt: new Date(taskInstant(runAt.day, runAt.minutes, tz)).toISOString() } : {}),
415
428
  section: row.section,
416
429
  ...(row.section_day ? { sectionDay: row.section_day } : {}),
430
+ stage: row.stage ?? 'planning',
431
+ ...(row.done_when ? { doneWhen: row.done_when } : {}),
417
432
  ...mark,
418
433
  }
419
434
  }
@@ -486,6 +501,26 @@ export async function setTaskText(domain: string, id: string, text: string): Pro
486
501
  await withLockRetry(() => bridge(['task-set-text', domain, id], clean))
487
502
  }
488
503
 
504
+ /** Move a task between board stages. 'planning' clears the marker. */
505
+ export async function setTaskStage(domain: string, id: string, stage: TaskStage): Promise<void> {
506
+ if (!TASK_STAGES.includes(stage)) {
507
+ throw new TaskRunError(422, 'invalid_stage', `stage must be one of ${TASK_STAGES.join(', ')}`)
508
+ }
509
+ await withLockRetry(() => bridge(['task-set-stage', domain, id, stage === 'planning' ? '--clear' : stage]))
510
+ }
511
+
512
+ /** Set or clear the finish line. Empty clears it, which re-blocks `run`. */
513
+ export async function setTaskDoneWhen(domain: string, id: string, text: string): Promise<void> {
514
+ const clean = text.replace(/\s+/g, ' ').trim()
515
+ if (clean.length > 500) {
516
+ throw new TaskRunError(422, 'done_when_too_long', 'done_when must be 500 characters or fewer')
517
+ }
518
+ if (clean.includes('**')) {
519
+ throw new TaskRunError(422, 'invalid_done_when', 'done_when cannot contain ** markup')
520
+ }
521
+ await withLockRetry(() => bridge(['task-set-done-when', domain, id], clean))
522
+ }
523
+
489
524
  export async function moveTask(domain: string, id: string, section: string, nowMs = Date.now()): Promise<void> {
490
525
  const { clock } = briefContext(nowMs)
491
526
  const args = ['task-move', domain, id, '--section', section]
@@ -19,6 +19,9 @@ import {
19
19
  taskDomains,
20
20
  tasksGate,
21
21
  workBadgeCount,
22
+ setTaskStage,
23
+ setTaskDoneWhen,
24
+ type TaskStage,
22
25
  } from '../lib/task-store.js'
23
26
 
24
27
  export const tasksRouter = Router()
@@ -191,8 +194,12 @@ tasksRouter.patch('/tasks/:id', async (req, res) => {
191
194
  await moveTask(domain, req.params.id, String(body.section))
192
195
  } else if ('checked' in body) {
193
196
  await checkTask({ domain, id: req.params.id, checked: body.checked === true })
197
+ } else if ('stage' in body) {
198
+ await setTaskStage(domain, req.params.id, String(body.stage) as TaskStage)
199
+ } else if ('doneWhen' in body) {
200
+ await setTaskDoneWhen(domain, req.params.id, body.doneWhen == null ? '' : String(body.doneWhen))
194
201
  } else {
195
- throw new TaskRunError(400, 'invalid_patch', 'Expected text, runAt, section, or checked.')
202
+ throw new TaskRunError(400, 'invalid_patch', 'Expected text, runAt, section, checked, stage, or doneWhen.')
196
203
  }
197
204
  res.json({ ok: true })
198
205
  } catch (error) {