@aemonge-dev/pi-stardeck 0.9.3 → 0.10.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aemonge-dev/pi-stardeck",
3
- "version": "0.9.3",
3
+ "version": "0.10.0",
4
4
  "description": "A Vim- and tmux-inspired command deck for Pi",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -357,32 +357,22 @@ function activityTile(state: HeaderDeckState, theme: Theme): string {
357
357
  return label ? color(theme, 'text', label) : color(theme, 'dim', 'awaiting Human');
358
358
  }
359
359
 
360
- function formatTurnElapsed(ms: number): string {
361
- // Matches the turn meter's span-idle ceiling: 99m59s999ms → `99:59`.
362
- const bounded = Math.min(
363
- (99 * 60 + 59) * 1_000 + 999,
364
- Math.max(0, Math.floor(Number.isFinite(ms) ? ms : 0)),
365
- );
366
- const totalSeconds = Math.floor(bounded / 1_000);
367
- return `${Math.floor(totalSeconds / 60)}:${String(totalSeconds % 60).padStart(2, '0')}`;
368
- }
369
-
370
360
  function turnCostTile(state: HeaderDeckState, theme: Theme): string {
371
- // Activity lane right: the turn cluster — liveness icon, turn time,
372
- // and tokens in one aligned group (Human amendment 2026-10-23: the
373
- // deck's only timer). Idle keeps the accepted dim `— · 0 tok`; the
374
- // quiet-activity-row contract depends on it.
361
+ // Activity lane right: the turn cluster — turn time and tokens in one
362
+ // aligned group (Human amendment 2026-10-23: the deck's only timer;
363
+ // Human amendment 2026-10-24: the liveness icon is gone — the ticking
364
+ // chronometer `mmm:ss'cc` is the liveness). Idle keeps the accepted
365
+ // dim `— · 0 tok`; the quiet-activity-row contract depends on it.
375
366
  const turn = state.header?.turn ?? null;
376
367
  if (!turn) return '';
377
368
  if (turn.tokens <= 0 && turn.elapsedMs <= 0) {
378
369
  return color(theme, 'dim', '— · 0 tok');
379
370
  }
380
- return `${color(theme, 'customMessageLabel', '󰠭')} ${color(
371
+ return `${color(theme, 'warning', formatDeckElapsed(turn.elapsedMs), true)} ${color(
381
372
  theme,
382
- 'warning',
383
- formatTurnElapsed(turn.elapsedMs),
384
- true,
385
- )} ${color(theme, 'dim', '·')} ${color(theme, 'warning', formatTokens(turn.tokens))}`;
373
+ 'dim',
374
+ '·',
375
+ )} ${color(theme, 'warning', formatTokens(turn.tokens))}`;
386
376
  }
387
377
 
388
378
  function gitRight(state: HeaderDeckState, theme: Theme): string {
@@ -24,8 +24,10 @@ export const DECK_TITLE_ACTIVITY_CLASSES = [
24
24
 
25
25
  export type DeckTitleActivityClass = (typeof DECK_TITLE_ACTIVITY_CLASSES)[number];
26
26
 
27
- /** Short target label bound from the accepted schema. */
28
- export const DECK_TITLE_TARGET_MAX = 48;
27
+ /** Short target label bound from the accepted schema (Human amendment
28
+ * 2026-10-24: 48 → 120, mirroring the status producer — the deck's
29
+ * render-time width crop owns the real limit now). */
30
+ export const DECK_TITLE_TARGET_MAX = 120;
29
31
 
30
32
  /** Task/step totals bound from the accepted schema. */
31
33
  export const DECK_TITLE_TOTAL_MAX = 999;
@@ -69,14 +69,16 @@ ephemeral Session Work state. Display priority is OpenSpec
69
69
  Task → Session Work → safe direct fallback. OpenSpec focus clears active Session
70
70
  Work when acquired, preventing hidden or stale work from resurfacing later.
71
71
 
72
- Session Work is stored only as versioned custom entries on the active Pi session branch;
73
- it never creates a repository status file. It supports this lifecycle:
72
+ Session Work is a legacy surface: its writer tool and `/work` command are retired, and
73
+ only old sessions' versioned custom entries remain, restored fail-soft on the active
74
+ Pi session branch (never a repository status file). OpenSpec focus is stored the same
75
+ way and supports this lifecycle:
74
76
 
75
77
  ```text
76
- /work
77
- /work set Implement session metadata
78
- /work validate
79
- /work clear
78
+ /openspec-focus set <change> <task>
79
+ /openspec-focus validate
80
+ /openspec-focus replace <change> <task>
81
+ /openspec-focus clear
80
82
  ```
81
83
 
82
84
  When no OpenSpec portfolio is available, the matching row-one footer fallback is the
@@ -84,10 +86,14 @@ compact `▶ Focus` or `◇ Validate`; an entirely empty
84
86
  project uses ` no OpenSpec`. The detailed Session Work intent remains in the header
85
87
  instead of being duplicated in the footer. Active and validation states also replace the
86
88
  fallback terminal title. Reload, resume, and tree navigation restore the newest valid
87
- branch entry. The agent-callable `work_focus` tool exposes the same `status`, `set`,
88
- `validate`, and `clear` actions. The agent-callable `openspec_focus` tool likewise
89
- shares the command's `status`, safeguarded `set`, deliberate `replace`, and persistent
90
- `clear` transitions.
89
+ branch entry. The agent-callable `openspec_focus` tool shares the command's `status`,
90
+ safeguarded `set`, deliberate `replace`, validating `validate`, and persistent `clear`
91
+ transitions; the `awaitingValidation` flag marks a focused task as awaiting Human
92
+ validation and auto-clears on the next `set`, `replace`, or `clear`. The
93
+ agent-callable `title` tool sets the sanitized session title shown when no OpenSpec
94
+ focus owns the row. The retired `work_focus` and `stage` instruments are gone: stages
95
+ derive from events and Taskflow phases, and legacy `session-work` entries from old
96
+ sessions still restore and render, fail-soft.
91
97
 
92
98
  Global routing belongs to native Ramona policy rather than this status package. This
93
99
  package does not inject parent routing prompts, block parent tools, create Session Work
@@ -20,12 +20,12 @@ import {
20
20
  sameOpenSpecFocus,
21
21
  sameSessionSubject,
22
22
  sameSessionWorkFocus,
23
+ sameTaskFocus,
23
24
  SESSION_SUBJECT_ENTRY_TYPE,
24
25
  SESSION_WORK_FOCUS_ENTRY_TYPE,
25
26
  type SessionSubject,
26
27
  type SessionWorkFocus,
27
28
  sessionSubject,
28
- sessionWorkFocus,
29
29
  taskflowRunFocus,
30
30
  taskFocus,
31
31
  } from './src/active-work.ts';
@@ -77,7 +77,6 @@ import {
77
77
  } from './src/reducer.ts';
78
78
  import type {
79
79
  CockpitSnapshot,
80
- StageValue,
81
80
  CommandRunner,
82
81
  DiagnosticsState,
83
82
  LoadedConfig,
@@ -90,7 +89,7 @@ import type {
90
89
 
91
90
  type SessionContext = ExtensionContext | ExtensionCommandContext;
92
91
  type Domain = RuntimeErrorRecord['domain'];
93
- type OpenSpecFocusAction = 'status' | 'set' | 'replace' | 'clear';
92
+ type OpenSpecFocusAction = 'status' | 'set' | 'replace' | 'validate' | 'clear';
94
93
 
95
94
  type StatusHealthCondition = {
96
95
  id: string;
@@ -229,9 +228,13 @@ class GalacticaStatusRuntime {
229
228
  this.workFocus = workFocus;
230
229
  this.subject = subject;
231
230
  this.sessionName = sessionName;
232
- this.liveLifecycle = workFocus.state === 'validation' ? 'waiting' : 'listening';
233
- this.liveActivity =
234
- workFocus.state === 'validation' ? { kind: 'awaiting-validation' } : undefined;
231
+ const restoredAwaitingValidation =
232
+ (focus.mode === 'task' && focus.awaitingValidation) ||
233
+ workFocus.state === 'validation';
234
+ this.liveLifecycle = restoredAwaitingValidation ? 'waiting' : 'listening';
235
+ this.liveActivity = restoredAwaitingValidation
236
+ ? { kind: 'awaiting-validation' }
237
+ : undefined;
235
238
  this.store = new AtomicStatusStore({
236
239
  cwd: ctx.cwd,
237
240
  projectRoot: ctx.cwd,
@@ -729,6 +732,11 @@ class GalacticaStatusRuntime {
729
732
  this.focus = focus;
730
733
  this.recordEvent({ type: 'focus/openspec', at: Date.now(), focus });
731
734
  this.store.clearOpenSpec();
735
+ if (focus.mode === 'task' && focus.awaitingValidation) {
736
+ this.setLiveProjection('waiting', { kind: 'awaiting-validation' });
737
+ } else if (focus.mode === 'none' && this.workFocus.state === 'clear') {
738
+ this.setLiveProjection('listening');
739
+ }
732
740
  this.publish();
733
741
  this.launch(this.refresh(false), 'openspec');
734
742
  }
@@ -754,20 +762,6 @@ class GalacticaStatusRuntime {
754
762
  this.publish();
755
763
  }
756
764
 
757
- declareStage(stage: StageValue): boolean {
758
- if (!this.currentTurnId) return false;
759
- const changed = this.runtimeSpans.setStage(this.currentTurnId, stage, true);
760
- this.recordEvent({
761
- type: 'run/stage',
762
- at: Date.now(),
763
- id: this.currentTurnId,
764
- stage,
765
- declared: true,
766
- });
767
- this.setLiveProjection(stage);
768
- return changed;
769
- }
770
-
771
765
  setLiveProjection(lifecycle: HeaderLifecycle, activity?: HeaderActivity): void {
772
766
  if (
773
767
  this.liveLifecycle === lifecycle &&
@@ -1081,19 +1075,11 @@ export default function galacticaStatus(pi: ExtensionAPI): void {
1081
1075
  runtime?.setFocus(focus);
1082
1076
  }
1083
1077
 
1084
- function workStatus(focus: SessionWorkFocus): string {
1085
- return focus.state === 'clear'
1086
- ? 'Work focus: clear'
1087
- : `Work focus: ${focus.state} · ${focus.intent}`;
1088
- }
1089
-
1090
1078
  function subjectStatus(subject: SessionSubject): string {
1091
- return subject.state === 'clear'
1092
- ? 'Session subject: clear'
1093
- : `Session subject: ${subject.title}`;
1079
+ return subject.state === 'clear' ? 'Title: clear' : `Title: ${subject.title}`;
1094
1080
  }
1095
1081
 
1096
- function applySubjectAction(
1082
+ function applyTitleAction(
1097
1083
  action: 'status' | 'set',
1098
1084
  title?: string,
1099
1085
  ): { ok: boolean; message: string; subject: SessionSubject } {
@@ -1107,7 +1093,7 @@ export default function galacticaStatus(pi: ExtensionAPI): void {
1107
1093
  if (!next || next.state !== 'set') {
1108
1094
  return {
1109
1095
  ok: false,
1110
- message: 'Session subject requires a non-empty title',
1096
+ message: 'Title requires a non-empty title',
1111
1097
  subject: current,
1112
1098
  };
1113
1099
  }
@@ -1115,79 +1101,20 @@ export default function galacticaStatus(pi: ExtensionAPI): void {
1115
1101
  return { ok: true, message: subjectStatus(next), subject: next };
1116
1102
  }
1117
1103
 
1118
- function applyWorkAction(
1119
- action: 'status' | 'set' | 'validate' | 'clear',
1120
- intent?: string,
1121
- ): { ok: boolean; message: string; focus: SessionWorkFocus } {
1122
- const current = runtime?.currentWorkFocus() ?? NO_SESSION_WORK_FOCUS;
1123
- const openSpec = runtime?.currentFocus() ?? NO_OPEN_SPEC_FOCUS;
1124
-
1125
- if (action === 'status') {
1126
- return { ok: true, message: workStatus(current), focus: current };
1127
- }
1128
- if (action === 'clear') {
1129
- if (current.state !== 'clear') persistAndApplyWorkFocus(NO_SESSION_WORK_FOCUS);
1130
- return {
1131
- ok: true,
1132
- message: 'Work focus cleared',
1133
- focus: NO_SESSION_WORK_FOCUS,
1134
- };
1135
- }
1136
- if (openSpec.mode === 'task') {
1137
- return {
1138
- ok: false,
1139
- message:
1140
- `OpenSpec focus is ${openSpec.change} › ${openSpec.taskId}; ` +
1141
- 'clear it before setting ephemeral work',
1142
- focus: current,
1143
- };
1144
- }
1145
- if (action === 'validate') {
1146
- if (current.state === 'clear') {
1147
- return {
1148
- ok: false,
1149
- message: 'No active Work focus to validate',
1150
- focus: current,
1151
- };
1152
- }
1153
- const validation = sessionWorkFocus('validation', current.intent);
1154
- if (!validation) {
1155
- return { ok: false, message: 'Invalid Work focus', focus: current };
1156
- }
1157
- if (!sameSessionWorkFocus(current, validation)) {
1158
- persistAndApplyWorkFocus(validation);
1159
- }
1160
- return { ok: true, message: workStatus(validation), focus: validation };
1161
- }
1162
-
1163
- const active = sessionWorkFocus('active', intent);
1164
- if (!active) {
1165
- return {
1166
- ok: false,
1167
- message: 'Work focus requires a non-empty intent',
1168
- focus: current,
1169
- };
1170
- }
1171
- if (!sameSessionWorkFocus(current, active)) persistAndApplyWorkFocus(active);
1172
- return { ok: true, message: workStatus(active), focus: active };
1173
- }
1174
-
1175
1104
  function applyOpenSpecFocusAction(
1176
1105
  action: OpenSpecFocusAction,
1177
1106
  change?: string,
1178
1107
  taskId?: string,
1179
1108
  ): { ok: boolean; message: string; focus: OpenSpecFocus } {
1180
1109
  const current = runtime?.currentFocus() ?? NO_OPEN_SPEC_FOCUS;
1110
+ const statusMessage = (focus: OpenSpecFocus): string =>
1111
+ focus.mode === 'task'
1112
+ ? `OpenSpec focus: ${focus.change} › ${focus.taskId}` +
1113
+ (focus.awaitingValidation ? ' — awaiting Human validation' : '')
1114
+ : 'OpenSpec focus: none';
1181
1115
 
1182
1116
  if (action === 'status') {
1183
- return {
1184
- ok: true,
1185
- message:
1186
- current.mode === 'task'
1187
- ? `OpenSpec focus: ${current.change} › ${current.taskId}`
1188
- : 'OpenSpec focus: none',
1189
- focus: current,
1190
- };
1117
+ return { ok: true, message: statusMessage(current), focus: current };
1191
1118
  }
1192
1119
  if (action === 'clear') {
1193
1120
  persistAndApplyFocus(NO_OPEN_SPEC_FOCUS);
@@ -1197,6 +1124,18 @@ export default function galacticaStatus(pi: ExtensionAPI): void {
1197
1124
  focus: NO_OPEN_SPEC_FOCUS,
1198
1125
  };
1199
1126
  }
1127
+ if (action === 'validate') {
1128
+ if (current.mode !== 'task') {
1129
+ return {
1130
+ ok: false,
1131
+ message: 'OpenSpec focus has no task to validate',
1132
+ focus: current,
1133
+ };
1134
+ }
1135
+ const validated = { ...current, awaitingValidation: true };
1136
+ if (!sameOpenSpecFocus(current, validated)) persistAndApplyFocus(validated);
1137
+ return { ok: true, message: statusMessage(validated), focus: validated };
1138
+ }
1200
1139
 
1201
1140
  const requested = taskFocus(change, taskId);
1202
1141
  if (!requested) {
@@ -1209,7 +1148,7 @@ export default function galacticaStatus(pi: ExtensionAPI): void {
1209
1148
  if (
1210
1149
  action === 'set' &&
1211
1150
  current.mode === 'task' &&
1212
- !sameOpenSpecFocus(current, requested)
1151
+ !sameTaskFocus(current, requested)
1213
1152
  ) {
1214
1153
  return {
1215
1154
  ok: false,
@@ -1222,7 +1161,7 @@ export default function galacticaStatus(pi: ExtensionAPI): void {
1222
1161
  if (!sameOpenSpecFocus(current, requested)) persistAndApplyFocus(requested);
1223
1162
  return {
1224
1163
  ok: true,
1225
- message: `OpenSpec focus: ${requested.change} › ${requested.taskId}`,
1164
+ message: statusMessage(requested),
1226
1165
  focus: requested,
1227
1166
  };
1228
1167
  }
@@ -1360,80 +1299,11 @@ export default function galacticaStatus(pi: ExtensionAPI): void {
1360
1299
  });
1361
1300
 
1362
1301
  pi.registerTool({
1363
- name: 'work_focus',
1364
- label: 'Work Focus',
1365
- description:
1366
- 'Show, set, mark for validation, or clear explicit ephemeral work metadata for this Pi session',
1367
- promptSnippet: 'Manage explicit ephemeral Session Work focus metadata',
1368
- parameters: Type.Object({
1369
- action: StringEnum(['status', 'set', 'validate', 'clear'] as const),
1370
- intent: Type.Optional(
1371
- Type.String({
1372
- description: 'Required only for set: the concise current intent',
1373
- }),
1374
- ),
1375
- }),
1376
- async execute(_toolCallId, params) {
1377
- const result = applyWorkAction(params.action, params.intent);
1378
- return {
1379
- content: [
1380
- {
1381
- type: 'text',
1382
- text: result.ok ? result.message : `Error: ${result.message}`,
1383
- },
1384
- ],
1385
- details: {
1386
- ok: result.ok,
1387
- focus: result.focus,
1388
- },
1389
- };
1390
- },
1391
- });
1392
-
1393
- pi.registerTool({
1394
- name: 'stage',
1395
- label: 'Stage',
1302
+ name: 'title',
1303
+ label: 'Session Title',
1396
1304
  description:
1397
- 'Declare the current stage of the Ramona loop (status truth; one call per transition)',
1398
- promptSnippet: 'Declare the current loop stage for status truth',
1399
- parameters: Type.Object({
1400
- stage: StringEnum([
1401
- 'understanding',
1402
- 'working',
1403
- 'assuring',
1404
- 'learning',
1405
- 'answering',
1406
- 'waiting',
1407
- 'blocked',
1408
- 'listening',
1409
- 'aborted',
1410
- ] as const),
1411
- }),
1412
- async execute(_toolCallId, params) {
1413
- const declared = runtime?.declareStage(params.stage) ?? false;
1414
- return {
1415
- content: [
1416
- {
1417
- type: 'text' as const,
1418
- text: declared
1419
- ? `Stage declared: ${params.stage}`
1420
- : `Stage recorded: ${params.stage} (no active turn span)`,
1421
- },
1422
- ],
1423
- details: { kind: 'stage-declaration', stage: params.stage },
1424
- };
1425
- },
1426
- });
1427
-
1428
- pi.registerTool({
1429
- name: 'subject',
1430
- label: 'Session Subject',
1431
- description:
1432
- 'Show or set the sanitized session subject that keeps the focus row populated beneath OpenSpec and Session Work',
1433
- promptSnippet: 'Manage the automatic session subject title',
1434
- promptGuidelines: [
1435
- 'Author a short sanitized subject (at most 120 characters, never raw prompt text) at the first conversational turn; revise it only when the topic clearly shifts. Never require Human action for it.',
1436
- ],
1305
+ 'Show or set the sanitized session title that keeps the focus row populated beneath OpenSpec and Session Work. Call this at the first conversational turn with a short sanitized title (at most 120 characters, never raw prompt text) and revise it only when the topic clearly shifts; never require Human action for it',
1306
+ promptSnippet: 'Manage the automatic session title',
1437
1307
  parameters: Type.Object({
1438
1308
  action: StringEnum(['status', 'set'] as const),
1439
1309
  title: Type.Optional(
@@ -1443,7 +1313,7 @@ export default function galacticaStatus(pi: ExtensionAPI): void {
1443
1313
  ),
1444
1314
  }),
1445
1315
  async execute(_toolCallId, params) {
1446
- const result = applySubjectAction(params.action, params.title);
1316
+ const result = applyTitleAction(params.action, params.title);
1447
1317
  return {
1448
1318
  content: [
1449
1319
  {
@@ -1463,14 +1333,11 @@ export default function galacticaStatus(pi: ExtensionAPI): void {
1463
1333
  name: 'openspec_focus',
1464
1334
  label: 'OpenSpec Focus',
1465
1335
  description:
1466
- 'Show, safely set, deliberately replace, or clear branch-local OpenSpec Task focus',
1336
+ 'Show, safely set, deliberately replace, validate, or clear branch-local OpenSpec Task focus. Use only for explicit Human direction or an exact declared OpenSpec Task handoff; never infer material Plan selection or Human validation',
1467
1337
  promptSnippet:
1468
1338
  'Manage exact branch-local OpenSpec Task focus with replacement safeguards',
1469
- promptGuidelines: [
1470
- 'Use openspec_focus only for explicit Human direction or an exact declared OpenSpec Task handoff; never infer material Plan selection or Human validation.',
1471
- ],
1472
1339
  parameters: Type.Object({
1473
- action: StringEnum(['status', 'set', 'replace', 'clear'] as const),
1340
+ action: StringEnum(['status', 'set', 'replace', 'validate', 'clear'] as const),
1474
1341
  change: Type.Optional(
1475
1342
  Type.String({
1476
1343
  description: 'Required for set and replace: the exact OpenSpec change ID',
@@ -1503,32 +1370,6 @@ export default function galacticaStatus(pi: ExtensionAPI): void {
1503
1370
  },
1504
1371
  });
1505
1372
 
1506
- pi.registerCommand('work', {
1507
- description: "Show, set, validate, or clear this session's ephemeral Work focus",
1508
- handler: async (args, ctx) => {
1509
- const trimmed = args.trim();
1510
- if (!trimmed || trimmed.toLowerCase() === 'status') {
1511
- ctx.ui.notify(applyWorkAction('status').message, 'info');
1512
- return;
1513
- }
1514
-
1515
- const [rawAction, ...rest] = trimmed.split(/\s+/u);
1516
- const action = rawAction?.toLowerCase();
1517
- if (action === 'set') {
1518
- const result = applyWorkAction('set', rest.join(' '));
1519
- ctx.ui.notify(result.message, result.ok ? 'info' : 'warning');
1520
- return;
1521
- }
1522
- if ((action === 'validate' || action === 'clear') && rest.length === 0) {
1523
- const result = applyWorkAction(action);
1524
- ctx.ui.notify(result.message, result.ok ? 'info' : 'warning');
1525
- return;
1526
- }
1527
-
1528
- ctx.ui.notify('Usage: /work [status|set <intent>|validate|clear]', 'warning');
1529
- },
1530
- });
1531
-
1532
1373
  pi.registerCommand('openspec-focus', {
1533
1374
  description: "Show, clear, or explicitly set this session's OpenSpec task focus",
1534
1375
  handler: async (args, ctx) => {
@@ -1544,6 +1385,12 @@ export default function galacticaStatus(pi: ExtensionAPI): void {
1544
1385
  return;
1545
1386
  }
1546
1387
 
1388
+ if (parts.length === 1 && parts[0]?.toLowerCase() === 'validate') {
1389
+ const result = applyOpenSpecFocusAction('validate');
1390
+ ctx.ui.notify(result.message, result.ok ? 'info' : 'warning');
1391
+ return;
1392
+ }
1393
+
1547
1394
  const rawAction = parts[0]?.toLowerCase();
1548
1395
  const action: 'set' | 'replace' = ['set', 'replace'].includes(rawAction ?? '')
1549
1396
  ? (rawAction as 'set' | 'replace')
@@ -8,6 +8,7 @@ export type TaskOpenSpecFocus = {
8
8
  mode: 'task';
9
9
  change: string;
10
10
  taskId: string;
11
+ awaitingValidation: boolean;
11
12
  };
12
13
 
13
14
  export type OpenSpecFocus = { mode: 'none' } | TaskOpenSpecFocus;
@@ -52,18 +53,24 @@ function explicitId(value: unknown): string | undefined {
52
53
  export function taskFocus(
53
54
  change: unknown,
54
55
  taskId: unknown,
56
+ awaitingValidation?: unknown,
55
57
  ): TaskOpenSpecFocus | undefined {
56
58
  const normalizedChange = explicitId(change);
57
59
  const normalizedTaskId = explicitId(taskId);
58
60
  if (!normalizedChange || !normalizedTaskId) return undefined;
59
- return { mode: 'task', change: normalizedChange, taskId: normalizedTaskId };
61
+ return {
62
+ mode: 'task',
63
+ change: normalizedChange,
64
+ taskId: normalizedTaskId,
65
+ awaitingValidation: awaitingValidation === true,
66
+ };
60
67
  }
61
68
 
62
69
  export function parseOpenSpecFocus(value: unknown): OpenSpecFocus | undefined {
63
70
  if (!isRecord(value)) return undefined;
64
71
  if (value.mode === 'none') return NO_OPEN_SPEC_FOCUS;
65
72
  if (value.mode !== 'task') return undefined;
66
- return taskFocus(value.change, value.taskId);
73
+ return taskFocus(value.change, value.taskId, value.awaitingValidation);
67
74
  }
68
75
 
69
76
  export function sessionWorkFocus(
@@ -162,7 +169,9 @@ export function taskflowRunFocus(input: unknown): TaskOpenSpecFocus | undefined
162
169
  : undefined;
163
170
  }
164
171
 
165
- export function sameOpenSpecFocus(left: OpenSpecFocus, right: OpenSpecFocus): boolean {
172
+ /** Task identity (mode · change · taskId) — the replace-safeguard's
173
+ * question; the validation flag is state, not identity. */
174
+ export function sameTaskFocus(left: OpenSpecFocus, right: OpenSpecFocus): boolean {
166
175
  if (left.mode !== right.mode) return false;
167
176
  return (
168
177
  left.mode === 'none' ||
@@ -172,6 +181,17 @@ export function sameOpenSpecFocus(left: OpenSpecFocus, right: OpenSpecFocus): bo
172
181
  );
173
182
  }
174
183
 
184
+ export function sameOpenSpecFocus(left: OpenSpecFocus, right: OpenSpecFocus): boolean {
185
+ if (left.mode !== right.mode) return false;
186
+ return (
187
+ left.mode === 'none' ||
188
+ (right.mode === 'task' &&
189
+ left.change === right.change &&
190
+ left.taskId === right.taskId &&
191
+ left.awaitingValidation === right.awaitingValidation)
192
+ );
193
+ }
194
+
175
195
  export function sameSessionWorkFocus(
176
196
  left: SessionWorkFocus,
177
197
  right: SessionWorkFocus,
@@ -22,8 +22,11 @@ export const TITLE_ACTIVITY_CLASSES = [
22
22
 
23
23
  export type TitleActivityClass = (typeof TITLE_ACTIVITY_CLASSES)[number];
24
24
 
25
- /** Short target label bound from the accepted schema. */
26
- export const TITLE_TARGET_MAX = 48;
25
+ /** Short target label bound from the accepted schema (Human amendment
26
+ * 2026-10-24: 48 → 120 — the deck's activity lane now owns the full
27
+ * width beside the turn cluster, so the protocol bound yields to the
28
+ * render-time crop instead of pre-cutting subagent/bash labels). */
29
+ export const TITLE_TARGET_MAX = 120;
27
30
 
28
31
  /** Task/step totals bound from the accepted schema. */
29
32
  export const TITLE_TOTAL_MAX = 999;
@@ -165,7 +168,7 @@ function parseTitleWaiver(value: unknown): TitleWaiverState | null | undefined {
165
168
 
166
169
  /**
167
170
  * Defensive parser for incoming title-state payloads. Unknown versions,
168
- * malformed shapes, and out-of-bounds values (target > 48 characters,
171
+ * malformed shapes, and out-of-bounds values (target > 120 characters,
169
172
  * totals > 999) reject the whole object with `null`; it never throws.
170
173
  */
171
174
  export function parseTitleState(value: unknown): TitleState | null {