Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
85 commits
Select commit Hold shift + click to select a range
b9298d3
docs(spec): SPEC-46 — the CLI is a client, and handoff is a command
leduckhc Aug 7, 2026
ac487b4
docs(spec): SPEC-46 D13/D14 — route a stranded prompt up the lineage,…
leduckhc Aug 8, 2026
1c898ca
docs(spec): SPEC-46 D15 — makit new owns a worktree, handoff inherits…
leduckhc Aug 8, 2026
cdeb5b0
docs(spec): SPEC-46 D16 — parallel agents may share a worktree, ungua…
leduckhc Aug 8, 2026
38ef861
docs(spec): SPEC-46 rev 2 — apply dual codex review, full scope kept
leduckhc Aug 8, 2026
6ef7605
docs(spec): SPEC-46 rev 3 + implementation plan
leduckhc Aug 8, 2026
a7bf3ed
feat(protocol): SPEC-46 shared contract — lineage, caps, principal, c…
leduckhc Aug 8, 2026
b27f609
test(stub): SPEC-46 T15 — the stub can be blocked, and can fail
leduckhc Aug 8, 2026
d8fcee1
feat(storage): SPEC-46 T9 — persist + project session lineage
leduckhc Aug 8, 2026
dc143d3
feat(app): SPEC-46 T9 — parse session lineage on the Flutter DTO
leduckhc Aug 8, 2026
5e0136b
pi-agent: Lineage persistence and Flutter DTO
leduckhc Aug 8, 2026
202cb92
Merge branch 'pi-agent-6ad7b712-2453-473' into feat/cli-client
leduckhc Aug 8, 2026
93a3140
feat(ws): SPEC-46 T1 — cli.grant mints a revocable CLI device
leduckhc Aug 8, 2026
a78246f
chore: drop a leaked app/.dart_tool symlink
leduckhc Aug 8, 2026
484e76a
feat(ws): SPEC-46 T2 — resolve a principal at hello
leduckhc Aug 8, 2026
48bd2aa
feat(ws): SPEC-46 T3 — per-session agent token, in-memory by construc…
leduckhc Aug 8, 2026
96eb016
feat(ws): SPEC-46 T4 — enforce the capability map at the router
leduckhc Aug 8, 2026
5b1f6dc
feat(ws): SPEC-46 T5 — gate event fanout on the principal
leduckhc Aug 8, 2026
c82e3cb
Merge branch 'pi-agent-c2fd6b36-1584-4c0' into feat/cli-client
leduckhc Aug 9, 2026
3eec6a2
feat(cli): SPEC-46 T6 — the shared WSS client and its own credential
leduckhc Aug 9, 2026
45b9da9
feat(cli): SPEC-46 T7 — makit ls over WSS, with the output and exit-c…
leduckhc Aug 9, 2026
06133f7
feat(cli): SPEC-46 T8 — attach re-homed on the CLI's credential; devi…
leduckhc Aug 9, 2026
b55ade5
refactor(cli): SPEC-46 — one connect path for every session verb
leduckhc Aug 9, 2026
fb9349d
feat(cli): SPEC-46 T18 — pure handoff-manifest parse + deterministic …
leduckhc Aug 9, 2026
54f0cf8
feat(cli): SPEC-46 T12 — makit new, with a worktree of its own
leduckhc Aug 9, 2026
0ff2f43
Merge SPEC-46 T18 — handoff manifest rendering
leduckhc Aug 9, 2026
abf92ee
feat(ws): SPEC-46 T10 — derive session.spawn parentId/origin from the…
leduckhc Aug 9, 2026
e316d5a
feat(ws): SPEC-46 T11 — bound spawn depth and live fan-out server-side
leduckhc Aug 9, 2026
0ac6cc1
feat(ws): SPEC-46 T13 — session.transcript, a bounded transcript slic…
leduckhc Aug 9, 2026
6ebb55d
Merge SPEC-46 T10/T11/T13 — lineage from the credential, spawn bounds…
leduckhc Aug 9, 2026
814ac88
feat(cli): SPEC-46 T19 — makit handoff: a manifest, the parent's tree…
leduckhc Aug 9, 2026
fb286b4
feat(cli): SPEC-46 T14 — send, a message to a session over send.message
leduckhc Aug 9, 2026
a2fb9be
feat(cli): SPEC-46 T14 — tail, replay + stream a session over sub
leduckhc Aug 9, 2026
6022974
feat(cli): SPEC-46 T14 — resume, re-attach a cold session via session…
leduckhc Aug 9, 2026
96a0195
feat(cli): SPEC-46 T14 — rm, archive by default and kill only with --…
leduckhc Aug 9, 2026
95065c5
Merge SPEC-46 T14 — send, tail, resume, rm
leduckhc Aug 9, 2026
1156457
feat(ws): SPEC-46 T20 — audience ladder + stored eligibility + respon…
leduckhc Aug 9, 2026
f23ec68
feat(ws): SPEC-46 T20 — gate --yolo to human credentials at session.s…
leduckhc Aug 9, 2026
2ccd840
feat(cli): SPEC-46 T16/T17 — wait (edge-triggered) and run, plus a sa…
leduckhc Aug 9, 2026
8686435
Merge SPEC-46 T20 — the audience ladder, pending eligibility, respons…
leduckhc Aug 9, 2026
0180e50
test(e2e): SPEC-46 — a control socket in the keyless loop, so the CLI…
leduckhc Aug 9, 2026
47af05e
feat(ws): SPEC-46 T21 — carry a self-describing session caption on sr…
leduckhc Aug 9, 2026
71bec1d
feat(app): SPEC-46 T21 — caption a stranded prompt from the srv.reque…
leduckhc Aug 9, 2026
9796333
Merge SPEC-46 T21 — captioned prompts for a session the app never opened
leduckhc Aug 9, 2026
bb3f222
feat(app): SPEC-46 T21 — caption an elicitation too, not just a permi…
leduckhc Aug 9, 2026
29a1615
fix(manager): SPEC-46 D3/D9 — the agent's env carries its real projec…
leduckhc Aug 9, 2026
9f8d462
feat(cli): SPEC-46 — send --attach uploads to POST /media instead of …
leduckhc Aug 9, 2026
09ece4c
docs(spec): SPEC-46 — tick P0/P1 acceptance, record what is proven na…
leduckhc Aug 9, 2026
bb17902
docs(plan): SPEC-46 P2 — drop makit log, and why approve/answer needs…
leduckhc Aug 9, 2026
4688a31
feat(cli): SPEC-46 U1/U2 — makit tree (lineage projection) and makit …
leduckhc Aug 9, 2026
1678028
feat(app): SPEC-46 U5 — caption handed-off sessions in the list
leduckhc Aug 9, 2026
225e333
fix(cli/ws): SPEC-46 — replay is not a turn; a human may state its ha…
leduckhc Aug 9, 2026
334ea67
feat(cli): SPEC-46 U3 — makit approve <id> [--deny]
leduckhc Aug 9, 2026
e6a19fc
feat(cli): SPEC-46 U3 — makit answer <id> TEXT
leduckhc Aug 9, 2026
2830196
Merge SPEC-46 U5 — 'handed off from …' on the session row
leduckhc Aug 9, 2026
024e0ca
feat: SPEC-46 U3 — approve/answer, with a stub that raises real promp…
leduckhc Aug 9, 2026
5ec9746
docs(plan): SPEC-46 U4 — thread/fork's real shape, its rollout precon…
leduckhc Aug 9, 2026
a7fada1
fix(app): SPEC-46 — lineage with no reason is 'Continued from', since…
leduckhc Aug 9, 2026
1a0d6ba
feat(ws): SPEC-46 U4 — forkSession() adapter seam, codex thread/fork
leduckhc Aug 9, 2026
f87bda6
feat(ws): SPEC-46 U4 — session.fork command, gated on capabilities.fork
leduckhc Aug 9, 2026
ff47ae7
feat(ws): SPEC-46 U4 — makit fork <id> [--agent A] [--worktree]
leduckhc Aug 9, 2026
165be7e
Merge SPEC-46 U4 — session.fork, the item SPEC-29 left pending
leduckhc Aug 9, 2026
8b3368c
test(ws): SPEC-46 U4 — lock session.fork out of agent tokens
leduckhc Aug 9, 2026
e43848b
fix(ws): SPEC-46 U4 — do not invent an '<agent>-acp' component in the…
leduckhc Aug 9, 2026
ba4e385
style(app): SPEC-46 — dart format the files this branch touched
leduckhc Aug 9, 2026
48a09ea
fix(ws): SPEC-46 D17 — gate every session read path on the principal,…
leduckhc Aug 9, 2026
e4656d1
fix(test): SPEC-46 — a second e2e harness must refuse, not clobber th…
leduckhc Aug 9, 2026
0d6622c
fix(ws): SPEC-46 U4 — fork always uses source harness, never defaults…
leduckhc Aug 10, 2026
9064a84
fix(ws/codex): SPEC-46 U4 — a fork inherits its source's harness, re-…
leduckhc Aug 10, 2026
457fd64
fix(cli): SPEC-46 — every verb reaches an exit code, and its credenti…
leduckhc Aug 11, 2026
249b11d
fix(ws,manager): SPEC-46 — an agent's token dies with its session, an…
leduckhc Aug 11, 2026
b055394
fix(app): SPEC-46 D14 — a captioned prompt stays readable on a phone
leduckhc Aug 11, 2026
c866c25
Merge origin/main into feat/cli-client — adopt SPEC-29's close/reopen…
leduckhc Aug 11, 2026
eeb24f8
fix(test): keep the shared snapshots fixture byte-identical across ap…
leduckhc Aug 11, 2026
1bef356
fix(test): force the stub server's sockets shut so a stalled request …
leduckhc Aug 11, 2026
6c4cb7f
test(server): bound every test at 120s so a hang names itself instead…
leduckhc Aug 11, 2026
9a7e82b
fix(cli): bound the snapshot waits, so no verb can hang without an ex…
leduckhc Aug 11, 2026
f1a810c
fix(test): script live events on subscription, not on a timer — the C…
leduckhc Aug 11, 2026
6071522
refactor(cli): centralize the async ceremony in connectCli helpers
leduckhc Aug 11, 2026
46cb578
refactor(cli): one withClient() owns the connect/refuse/close contrac…
leduckhc Aug 11, 2026
a5f50bc
refactor(cli): declarative parseFlags() parser — replaces 59 hand-rol…
leduckhc Aug 11, 2026
05f8175
refactor(cli): one declarative argv parser, replacing 59 hand-rolled …
leduckhc Aug 11, 2026
b6b0441
docs(ws): clarify capability map is forward-looking, not exhaustive
leduckhc Aug 11, 2026
2b7ae84
Merge origin/main into feat/cli-client — adopt SPEC-46 docs (preview …
leduckhc Aug 11, 2026
73c0f6b
Merge origin/main into feat/cli-client
leduckhc Aug 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added .pnpm-store/v11/index.db
Binary file not shown.
1 change: 1 addition & 0 deletions .pnpm-store/v11/projects/739d8fce9f21779d2680f9e8d181f4ee
21 changes: 21 additions & 0 deletions app/lib/store/models.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1124,6 +1124,9 @@ class Session {
this.resumable = false,
this.closed = false,
this.orphaned = false,
this.parentId,
this.handoffReason,
this.origin,
this.queued = const [],
});

Expand Down Expand Up @@ -1171,6 +1174,18 @@ class Session {
/// repo root (no recreate-worktree path).
final bool orphaned;

/// SPEC-46 lineage (D10): the session this one was handed off / spawned from,
/// so the app can caption "handed off from …". Null for a session with no
/// parent (every session created before SPEC-46, and every app-spawned one).
final String? parentId;

/// SPEC-46 (D10): why the handoff happened, as written by the outgoing agent.
final String? handoffReason;

/// SPEC-46 (D10): which client created this session ("app"/"cli"/"agent").
/// Null on pre-SPEC-46 rows; a plain string so an unknown value never throws.
final String? origin;

/// Messages submitted while the agent was busy that could not be steered into
/// the running turn (SPEC-35), oldest first. They are delivered one per idle
/// transition and can be cancelled until then.
Expand All @@ -1191,6 +1206,9 @@ class Session {
bool? resumable,
bool? closed,
bool? orphaned,
String? parentId,
String? handoffReason,
String? origin,
List<QueuedMessage>? queued,
}) => Session(
id: id,
Expand All @@ -1210,6 +1228,9 @@ class Session {
resumable: resumable ?? this.resumable,
closed: closed ?? this.closed,
orphaned: orphaned ?? this.orphaned,
parentId: parentId ?? this.parentId,
handoffReason: handoffReason ?? this.handoffReason,
origin: origin ?? this.origin,
queued: queued ?? this.queued,
);
}
Expand Down
5 changes: 5 additions & 0 deletions app/lib/transport/codec.dart
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,11 @@ class WireCodec {
resumable: j['resumable'] == true,
closed: j['closed'] == true,
orphaned: j['orphaned'] == true,
parentId: j['parentId'] is String ? j['parentId'] as String : null,
handoffReason: j['handoffReason'] is String
? j['handoffReason'] as String
: null,
origin: j['origin'] is String ? j['origin'] as String : null,
queued: decodeQueued(j['queued']),
),
);
Expand Down
40 changes: 40 additions & 0 deletions app/lib/ui/home/session_tile.dart
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,28 @@ class SessionTile extends ConsumerWidget {
],
),
const SizedBox(height: kSpace2),
if (_handoffCaption(session) case final caption?) ...[
Row(
children: [
Icon(
PhosphorIconsLight.arrowBendUpRight,
size: 12,
color: cs.outline,
),
const SizedBox(width: kSpace4),
Expanded(
child: Text(
caption,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.bodySmall
?.copyWith(color: cs.outline),
),
),
],
),
const SizedBox(height: kSpace2),
],
Text(
session.pending
? 'Send a message to create a branch'
Expand Down Expand Up @@ -168,6 +190,24 @@ class SessionTile extends ConsumerWidget {
s == SessionStatus.awaitingApproval ||
s == SessionStatus.error;

/// SPEC-46 D10: a session with lineage was handed off from another session,
/// so the row explains itself rather than appearing as a mystery title. The
/// caption renders whenever [Session.parentId] is set and never depends on
/// resolving the parent — which may be closed or simply not cached — and
/// carries the outgoing agent's reason when one was written.
static String? _handoffCaption(Session session) {
if (session.parentId == null) return null;
final reason = session.handoffReason?.trim();
// Lineage with no reason is not necessarily a handoff: `makit fork` (U4)
// branches a conversation natively and deliberately writes no reason, because
// a fork is not a handoff (D6). "Continued from" is true of both; claiming a
// handoff would mislabel every forked session in the one place the user meets
// it.
return reason == null || reason.isEmpty
? 'Continued from another session'
: 'Handed off — $reason';
}

/// Confirms the close, then requests it and only reports the row as
/// dismissed once the server acknowledges. Returning false on failure keeps
/// the row in place (the session is still in [sessionsProvider]), so a failed
Expand Down
92 changes: 84 additions & 8 deletions app/lib/ui/widgets/srv_request_handler.dart
Original file line number Diff line number Diff line change
Expand Up @@ -334,11 +334,18 @@ class _SrvRequestHandlerState extends ConsumerState<SrvRequestHandler>
context: ctx,
barrierDismissible: false,
builder: (dctx) => AlertDialog(
// D14 made this dialog open-ended: the caption carries a whole handoff
// reason and the message is whatever the agent wrote. Unscrollable, a
// long prompt is text the user can neither finish reading nor scroll
// past to reach Deny/Approve — which trains them to answer without
// reading, the very thing captioning them was meant to prevent.
scrollable: true,
title: Text(body['title']?.toString() ?? 'Confirm'),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_PromptCaption(session: body['session']),
Text(body['message']?.toString() ?? ''),
if (body['preview'] != null) ...[
const SizedBox(height: kSpace8),
Expand Down Expand Up @@ -387,15 +394,28 @@ class _SrvRequestHandlerState extends ConsumerState<SrvRequestHandler>
context: ctx,
barrierDismissible: false,
builder: (dctx) => AlertDialog(
// Scrollable for the same reason as the permission prompt, and more so:
// an 8-line field sits under the caption here, with the keyboard up.
scrollable: true,
title: Text(body['title']?.toString() ?? 'Input'),
content: TextField(
controller: controller,
autofocus: true,
minLines: multiline ? 3 : 1,
maxLines: multiline ? 8 : 1,
decoration: InputDecoration(
hintText: body['placeholder']?.toString(),
),
// Captioned like a permission prompt (D14): D13 routes an elicitation up
// the same ladder, so this dialog can equally reach a phone that has
// never opened the session asking the question.
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_PromptCaption(session: body['session']),
TextField(
controller: controller,
autofocus: true,
minLines: multiline ? 3 : 1,
maxLines: multiline ? 8 : 1,
decoration: InputDecoration(
hintText: body['placeholder']?.toString(),
),
),
],
),
actions: [
TextButton(
Expand Down Expand Up @@ -493,3 +513,59 @@ class _SrvRequestHandlerState extends ConsumerState<SrvRequestHandler>
@override
Widget build(BuildContext context) => widget.child;
}

/// SPEC-46 D14 — a self-describing header for a prompt whose session the phone
/// may never have subscribed to. Sourced entirely from the `srv.request`
/// envelope's `session` block (title, agent/harness, handoff origin), never
/// from cached store state, so a stranded prompt reached at rung 3 of the D13
/// ladder is still attributable. Renders nothing when the block is absent
/// (existing prompts are unchanged).
class _PromptCaption extends StatelessWidget {
const _PromptCaption({required this.session});

final Object? session;

@override
Widget build(BuildContext context) {
final s = session;
if (s is! Map) return const SizedBox.shrink();
final title = s['title']?.toString();
final agent = s['agent']?.toString();
final handoffReason = s['handoffReason']?.toString();
if ((title == null || title.isEmpty) &&
(agent == null || agent.isEmpty) &&
(handoffReason == null || handoffReason.isEmpty)) {
return const SizedBox.shrink();
}

final theme = Theme.of(context);
final heading = [
if (title != null && title.isNotEmpty) title,
if (agent != null && agent.isNotEmpty) agent,
].join(' · ');

return Padding(
padding: const EdgeInsets.only(bottom: kSpace12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
if (heading.isNotEmpty)
Text(
heading,
style: theme.textTheme.labelLarge?.copyWith(
color: theme.colorScheme.primary,
),
),
if (handoffReason != null && handoffReason.isNotEmpty)
Text(
'Handed off: $handoffReason',
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
],
),
);
}
}
45 changes: 45 additions & 0 deletions app/test/session_resumable_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,49 @@ void main() {
expect(sessions!.single.closed, isTrue);
});
});

group('Session lineage fields (SPEC-46 D10)', () {
test(
'decodeSessions parses parentId, handoffReason, origin when present',
() {
final sessions = WireCodec.decodeSessions([
{
'id': 's1',
'projectId': 'p1',
'agent': 'codex',
'title': 'handed off',
'status': 'idle',
'policy': 'ask-on-risky',
'parentId': 'parent-1',
'handoffReason': 'out of context',
'origin': 'agent',
},
]);
final s = sessions!.single;
expect(s.parentId, 'parent-1');
expect(s.handoffReason, 'out of context');
expect(s.origin, 'agent');
},
);

test(
'lineage defaults to null when absent (pre-SPEC-46 row, no throw)',
() {
final sessions = WireCodec.decodeSessions([
{
'id': 's2',
'projectId': 'p1',
'agent': 'pi',
'title': 'legacy',
'status': 'idle',
'policy': 'ask-on-risky',
},
]);
final s = sessions!.single;
expect(s.parentId, isNull);
expect(s.handoffReason, isNull);
expect(s.origin, isNull);
},
);
});
}
Loading
Loading