feat(claude): nudge fresh sessions to load the pql + clide skills (T-490)

A fresh Claude session now gets a one-line preamble (clideSkillsNote) telling it
to reach for the bundled pql + clide skills from its first turn rather than
rediscovering the workflows. Layered onto clideContextNote in the single
--append-system-prompt.

Gated on a NEW session (!spec.resume && !spec.isFork): new tabs and the
post-/clear respawn spawn with resume:false and get it; the account-change
respawn (T-480) and real resumes carry prior context (resume:true), and forks
inherit their source — none are re-nagged. Both skills already ship (pql
user-scope, clide repo-scope), so no precursor was needed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-28 10:53:08 +02:00
co-authored by Claude Opus 4.8
parent a46677facf
commit 84f4b20cb8
7 changed files with 108 additions and 0 deletions
@@ -7264,3 +7264,6 @@ INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, chang
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FGSQ8B7WDKZ174GJDGWW0GS0', 'status', 'backlog', 'done', NULL, '2026-06-28 06:35:16', '2026-06-28 06:35:16.152', '2026-06-28 06:35:16.152', NULL, '15de4a7ee941ffa6bb5dfa762e6d7ffc', 2) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FGSQ8YSZA5YNP1JE8SJHXWBG', 'status', 'backlog', 'done', NULL, '2026-06-28 06:41:47', '2026-06-28 06:41:47.934', '2026-06-28 06:41:47.934', NULL, 'df08fd309db50c322d5ea27267d23f7b', 2) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FGPG4WNNH3BWDRVZXYQTW7Z0', 'status', 'backlog', 'done', NULL, '2026-06-28 06:41:47', '2026-06-28 06:41:47.971', '2026-06-28 06:41:47.971', NULL, '8e792d212a7f92e5201296ed01873607', 2) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FGSQP1YPG70RPQJ4XZDH2T1M', 'status', 'backlog', 'in_progress', NULL, '2026-06-28 08:32:16', '2026-06-28 08:32:16.329', '2026-06-28 08:32:16.329', NULL, 'b212ea2e61acbb81bd5d089e97240c97', 2) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FGSQP1YPG70RPQJ4XZDH2T1M', 'status', 'in_progress', 'in_progress', NULL, '2026-06-28 08:32:36', '2026-06-28 08:32:36.557', '2026-06-28 08:32:36.557', NULL, '09e582bb30f33f9c7330e041067a48b6', 2) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FGSQP1YPG70RPQJ4XZDH2T1M', 'status', 'in_progress', 'done', NULL, '2026-06-28 08:53:08', '2026-06-28 08:53:08.149', '2026-06-28 08:53:08.149', NULL, 'fbe0fcce351e196779c912689805075f', 2) ON CONFLICT(hash) DO NOTHING;
+60
View File
@@ -9386,3 +9386,63 @@ INSERT INTO tickets (record_id, type, parent_record_id, title, description, stat
- Language/framework templates (git init + minimal files only).
- Remote creation (GitHub repo create) local init only.
- Re-homing existing projects between accounts beyond the existing Settings/badge switch.', 'done', 'medium', NULL, NULL, NULL, '2026-06-27 22:43:31.629', '2026-06-28 06:41:47.971', NULL, '01a340c5205eae82e1b2bb7efb3be179', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FGSQP1YPG70RPQJ4XZDH2T1M', 'task', NULL, 'Nudge fresh Claude sessions to load the pql + clide skills (spawn preamble)', 'On a fresh Claude session, prompt Claude to load/use the pql and clide skills so it reaches for them from the first turn instead of having to rediscover them. Fresh = a new tab, an empty session, and the post-/clear respawn — NOT a --resume of an existing session (that one already has its working context).
## Hook point
lib/builtin/claude/src/session_orchestrator.dart `_spawn` already builds a `preambles` list (currently `[clideContextNote(spec.cwd)]`, plus the team system prompt). Every spawn flows through here new tab, fork, teammate, AND the close-and-respawn after /clear (T-437) so it is the single clean place to add a skills-bootstrap preamble. Gate it on a NEW session (e.g. `!spec.resume`) so resumed sessions are not re-nagged.
## What to inject
A short preamble instructing Claude to load + use the pql skill (planning/vault queries) and the clide skill (driving the clide IDE via the `clide` CLI), when present. Keep it minimal and idempotent one or two sentences, not a wall of text.
Considerations:
- Confirm a `clide` skill exists (the pql skill is user-scope via `pql init`; a clide skill may need to ship/install first). If the clide skill does not yet exist, this ticket may spawn a precursor to author it.
- Don''t duplicate clideContextNote''s content; this is the ''reach for these skills'' nudge, layered on top.
- Verify the nudge actually lands: a fresh session should pick up the skills on its first relevant action.
## Acceptance
1. A new-tab / empty Claude session receives a preamble nudging it to load the pql + clide skills.
2. The post-/clear respawn also gets it (it spawns through the same path).
3. A --resume of an existing session does NOT get re-injected.
4. The text is short and does not crowd out clideContextNote.', 'in_progress', 'medium', NULL, NULL, NULL, '2026-06-28 06:15:53.077', '2026-06-28 08:32:16.329', NULL, '9a76f6185e333841c1e24f36cdc7d215', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FGSQP1YPG70RPQJ4XZDH2T1M', 'task', NULL, 'Nudge fresh Claude sessions to load the pql + clide skills (spawn preamble)', 'On a fresh Claude session, prompt Claude to load/use the pql and clide skills so it reaches for them from the first turn instead of having to rediscover them. Fresh = a new tab, an empty session, and the post-/clear respawn — NOT a --resume of an existing session (that one already has its working context).
## Hook point
lib/builtin/claude/src/session_orchestrator.dart `_spawn` already builds a `preambles` list (currently `[clideContextNote(spec.cwd)]`, plus the team system prompt). Every spawn flows through here new tab, fork, teammate, AND the close-and-respawn after /clear (T-437) so it is the single clean place to add a skills-bootstrap preamble. Gate it on a NEW session (e.g. `!spec.resume`) so resumed sessions are not re-nagged.
## What to inject
A short preamble instructing Claude to load + use the pql skill (planning/vault queries) and the clide skill (driving the clide IDE via the `clide` CLI), when present. Keep it minimal and idempotent one or two sentences, not a wall of text.
Considerations:
- Confirm a `clide` skill exists (the pql skill is user-scope via `pql init`; a clide skill may need to ship/install first). If the clide skill does not yet exist, this ticket may spawn a precursor to author it.
- Don''t duplicate clideContextNote''s content; this is the ''reach for these skills'' nudge, layered on top.
- Verify the nudge actually lands: a fresh session should pick up the skills on its first relevant action.
## Acceptance
1. A new-tab / empty Claude session receives a preamble nudging it to load the pql + clide skills.
2. The post-/clear respawn also gets it (it spawns through the same path).
3. A --resume of an existing session does NOT get re-injected.
4. The text is short and does not crowd out clideContextNote.', 'in_progress', 'medium', NULL, NULL, NULL, '2026-06-28 06:15:53.077', '2026-06-28 08:32:36.557', NULL, '85e8d5cf44c4947b00f0f10f4f3906a5', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FGSQP1YPG70RPQJ4XZDH2T1M', 'task', NULL, 'Nudge fresh Claude sessions to load the pql + clide skills (spawn preamble)', 'On a fresh Claude session, prompt Claude to load/use the pql and clide skills so it reaches for them from the first turn instead of having to rediscover them. Fresh = a new tab, an empty session, and the post-/clear respawn — NOT a --resume of an existing session (that one already has its working context).
## Hook point
lib/builtin/claude/src/session_orchestrator.dart `_spawn` already builds a `preambles` list (currently `[clideContextNote(spec.cwd)]`, plus the team system prompt). Every spawn flows through here new tab, fork, teammate, AND the close-and-respawn after /clear (T-437) so it is the single clean place to add a skills-bootstrap preamble. Gate it on a NEW session (e.g. `!spec.resume`) so resumed sessions are not re-nagged.
## What to inject
A short preamble instructing Claude to load + use the pql skill (planning/vault queries) and the clide skill (driving the clide IDE via the `clide` CLI), when present. Keep it minimal and idempotent one or two sentences, not a wall of text.
Considerations:
- Confirm a `clide` skill exists (the pql skill is user-scope via `pql init`; a clide skill may need to ship/install first). If the clide skill does not yet exist, this ticket may spawn a precursor to author it.
- Don''t duplicate clideContextNote''s content; this is the ''reach for these skills'' nudge, layered on top.
- Verify the nudge actually lands: a fresh session should pick up the skills on its first relevant action.
## Acceptance
1. A new-tab / empty Claude session receives a preamble nudging it to load the pql + clide skills.
2. The post-/clear respawn also gets it (it spawns through the same path).
3. A --resume of an existing session does NOT get re-injected.
4. The text is short and does not crowd out clideContextNote.', 'done', 'medium', NULL, NULL, NULL, '2026-06-28 06:15:53.077', '2026-06-28 08:53:08.149', NULL, '1c8842605724aab0461805297ad03ac7', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
+3
View File
@@ -54,6 +54,9 @@ heading, and (b) bumping `pubspec.yaml` `version:` in the same commit.
### Changed
- **Fresh Claude sessions are nudged to use the bundled skills.** New tabs and
post-`/clear` respawns get a one-line prompt to load the `pql` + `clide`
skills from the first turn; resumed/forked sessions are left alone. (T-490)
- **Claude meta sidebar facelift.** The Activity, Team, and Config tabs render
their sections as elevated cards with small-caps headers, matching the
settings overlay's card design. (T-158)
@@ -58,6 +58,16 @@ String clideContextNote(String workspaceRoot) =>
'is how you observe and drive the same workspace the user sees — prefer it for IDE actions so '
'your work and the user\'s stay in one shared workspace.';
/// Nudge a FRESH session to reach for the bundled skills from its first turn
/// rather than rediscovering the workflows (T-490). Layered on top of
/// [clideContextNote]; only injected for new sessions (not --resume / forks),
/// so a session that already carries context is never re-nagged.
String clideSkillsNote() =>
'Two skills are available in this workspace — load and use them from your first turn instead '
'of rediscovering their workflows: `pql` (planning, decisions, tickets, and vault queries) and '
'`clide` (driving this IDE). Reach for the matching skill whenever a task touches planning or '
'tickets, or the clide surface.';
/// Build the environment DELTA to overlay on a hosted session's inherited
/// environment (T-215). `Process.start` keeps the parent environment by
/// default, so this returns only the keys to add/override:
@@ -244,6 +244,11 @@ class ClaudeSessionOrchestrator extends ChangeNotifier {
// note and the team preamble merge into ONE --append-system-prompt (claude
// honours a single one).
final preambles = <String>[clideContextNote(spec.cwd)];
// Nudge a FRESH session to reach for the bundled skills (T-490). A new tab
// and the post-/clear respawn spawn with resume:false; the account-change
// respawn (T-480) and real resumes carry prior context (resume:true), and a
// fork inherits its source — none of those are re-nagged.
if (!spec.resume && !spec.isFork) preambles.add(clideSkillsNote());
if (spec.team) {
final name = spec.memberName ?? spec.role;
broker.addMember(TeamMemberRef(id: spec.id, name: name, role: spec.role));
@@ -28,6 +28,17 @@ void main() {
});
});
group('clideSkillsNote (T-490)', () {
test('names both skills, nudges first-turn use, and stays short', () {
final note = clideSkillsNote();
expect(note, contains('`pql`'));
expect(note, contains('`clide`'));
expect(note, contains('from your first turn'));
// Acceptance #4: a short nudge, not a wall of text crowding clideContextNote.
expect(note.length, lessThan(360));
});
});
group('clideBashAllowRule (T-217)', () {
test('is the command-scoped Bash rule and rides on --allowedTools', () {
expect(clideBashAllowRule, 'Bash(clide:*)');
@@ -53,6 +53,22 @@ void main() {
expect(spawnedArgs.single, isNot(contains('--effort')));
});
test('a fresh session gets the skills nudge; resume + fork do not (T-490)', () async {
String appendPrompt(List<String> args) {
final i = args.indexOf('--append-system-prompt');
return i >= 0 ? args[i + 1] : '';
}
await orch.spawn(spec('primary')); // fresh (resume:false)
expect(appendPrompt(spawnedArgs.single), contains('from your first turn'), reason: 'fresh session is nudged');
await orch.spawn(SpawnSpec(id: 'resumed', role: 'r', sessionId: 'r-uuid', cwd: '/repo', resume: true));
expect(appendPrompt(spawnedArgs.last), isNot(contains('from your first turn')), reason: 'resume carries context');
await orch.spawn(SpawnSpec(id: 'fork', role: 'f', sessionId: 'f-uuid', cwd: '/repo', forkSourceSessionId: 'primary-uuid'));
expect(appendPrompt(spawnedArgs.last), isNot(contains('from your first turn')), reason: 'fork inherits its source');
});
test('spawns multiple concurrent sessions, each with its own process', () async {
await orch.spawn(spec('primary'));
await orch.spawn(spec('teammate:tyre'));