diff --git a/.pql/changelog/ticket_history/2026-06.sql b/.pql/changelog/ticket_history/2026-06.sql index 2af9aef5..9d5af6ab 100644 --- a/.pql/changelog/ticket_history/2026-06.sql +++ b/.pql/changelog/ticket_history/2026-06.sql @@ -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; diff --git a/.pql/changelog/tickets/2026-06.sql b/.pql/changelog/tickets/2026-06.sql index 1c9379fc..b7fa338e 100644 --- a/.pql/changelog/tickets/2026-06.sql +++ b/.pql/changelog/tickets/2026-06.sql @@ -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); diff --git a/CHANGELOG.md b/CHANGELOG.md index 9d39d490..b96fca46 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) diff --git a/lib/builtin/claude/src/agent_bootstrap.dart b/lib/builtin/claude/src/agent_bootstrap.dart index ae7e0fbc..91941af1 100644 --- a/lib/builtin/claude/src/agent_bootstrap.dart +++ b/lib/builtin/claude/src/agent_bootstrap.dart @@ -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: diff --git a/lib/builtin/claude/src/session_orchestrator.dart b/lib/builtin/claude/src/session_orchestrator.dart index 0b6ea1ce..55587a4d 100644 --- a/lib/builtin/claude/src/session_orchestrator.dart +++ b/lib/builtin/claude/src/session_orchestrator.dart @@ -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 = [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)); diff --git a/test/builtin/claude/agent_bootstrap_test.dart b/test/builtin/claude/agent_bootstrap_test.dart index 56edf121..a9690fdb 100644 --- a/test/builtin/claude/agent_bootstrap_test.dart +++ b/test/builtin/claude/agent_bootstrap_test.dart @@ -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:*)'); diff --git a/test/builtin/claude/session_orchestrator_test.dart b/test/builtin/claude/session_orchestrator_test.dart index 72bced7d..55098c5a 100644 --- a/test/builtin/claude/session_orchestrator_test.dart +++ b/test/builtin/claude/session_orchestrator_test.dart @@ -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 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'));