team-toon-tack 2.0.3 → 2.3.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.
@@ -6,7 +6,7 @@ const __filename = fileURLToPath(import.meta.url);
6
6
  const __dirname = path.dirname(__filename);
7
7
  import { decode, encode } from "@toon-format/toon";
8
8
  import prompts from "prompts";
9
- import { buildConfig, buildLocalConfig, findTeamKey, findUserKey, getDefaultStatusTransitions, } from "./lib/config-builder.js";
9
+ import { buildConfig, buildLocalConfig, buildTeamsConfig, findTeamKey, findUserKey, getDefaultStatusTransitions, } from "./lib/config-builder.js";
10
10
  import { fileExists, getLinearClient, getPaths, } from "./utils.js";
11
11
  function parseArgs(args) {
12
12
  const options = { interactive: true };
@@ -82,77 +82,157 @@ async function promptForApiKey(options) {
82
82
  }
83
83
  return apiKey;
84
84
  }
85
- async function selectTeams(teams, options) {
86
- let selectedTeams = [teams[0]];
87
- let primaryTeam = teams[0];
85
+ async function selectDevTeam(teams, options) {
86
+ let devTeam = teams[0];
88
87
  if (options.team) {
89
88
  const found = teams.find((t) => t.name.toLowerCase() === options.team?.toLowerCase());
90
89
  if (found) {
91
- selectedTeams = [found];
92
- primaryTeam = found;
90
+ devTeam = found;
93
91
  }
94
92
  }
95
93
  else if (options.interactive && teams.length > 1) {
94
+ console.log("\nšŸ‘Øā€šŸ’» Dev Team Configuration:");
96
95
  const response = await prompts({
97
- type: "multiselect",
98
- name: "teamIds",
99
- message: "Select teams to sync (space to select, enter to confirm):",
96
+ type: "select",
97
+ name: "teamId",
98
+ message: "Select your dev team (for work-on/done commands):",
100
99
  choices: teams.map((t) => ({ title: t.name, value: t.id })),
101
- min: 1,
102
100
  });
103
- if (response.teamIds && response.teamIds.length > 0) {
104
- selectedTeams = teams.filter((t) => response.teamIds.includes(t.id));
105
- if (selectedTeams.length > 1) {
106
- const primaryResponse = await prompts({
107
- type: "select",
108
- name: "primaryTeamId",
109
- message: "Select your primary team (for work-on/done commands):",
110
- choices: selectedTeams.map((t) => ({ title: t.name, value: t.id })),
111
- });
112
- primaryTeam =
113
- selectedTeams.find((t) => t.id === primaryResponse.primaryTeamId) ||
114
- selectedTeams[0];
115
- }
116
- else {
117
- primaryTeam = selectedTeams[0];
118
- }
101
+ if (response.teamId) {
102
+ devTeam = teams.find((t) => t.id === response.teamId) || teams[0];
119
103
  }
120
104
  }
121
- return { selected: selectedTeams, primary: primaryTeam };
105
+ return devTeam;
106
+ }
107
+ async function selectDevTestingStatus(devStates, options) {
108
+ if (!options.interactive || devStates.length === 0) {
109
+ return getDefaultStatusTransitions(devStates).testing;
110
+ }
111
+ console.log("\nšŸ” Dev Team Testing/Review Status:");
112
+ const stateChoices = devStates.map((s) => ({
113
+ title: `${s.name} (${s.type})`,
114
+ value: s.name,
115
+ }));
116
+ const response = await prompts({
117
+ type: "select",
118
+ name: "testingStatus",
119
+ message: "Select testing/review status for dev team (used when strict_review mode):",
120
+ choices: [
121
+ { title: "(Skip - no testing status)", value: undefined },
122
+ ...stateChoices,
123
+ ],
124
+ initial: 0,
125
+ });
126
+ return response.testingStatus;
122
127
  }
123
- async function selectQaPmTeam(teams, primaryTeam, options) {
128
+ async function selectQaPmTeams(teams, devTeam, teamStatesMap, teamsConfig, options) {
124
129
  // Only ask if there are multiple teams and interactive mode
125
130
  if (!options.interactive || teams.length <= 1) {
126
- return undefined;
131
+ return [];
127
132
  }
128
- // Filter out primary team from choices
129
- const otherTeams = teams.filter((t) => t.id !== primaryTeam.id);
133
+ // Filter out dev team from choices
134
+ const otherTeams = teams.filter((t) => t.id !== devTeam.id);
130
135
  if (otherTeams.length === 0) {
131
- return undefined;
136
+ return [];
132
137
  }
133
- console.log("\nšŸ”— QA/PM Team Configuration:");
138
+ console.log("\nšŸ”— QA/PM Teams Configuration:");
134
139
  const response = await prompts({
135
- type: "select",
136
- name: "qaPmTeamId",
137
- message: "Select QA/PM team (for cross-team parent issue updates):",
140
+ type: "multiselect",
141
+ name: "qaPmTeamIds",
142
+ message: "Select QA/PM teams for cross-team parent updates (space to select, enter to confirm):",
138
143
  choices: [
139
- {
140
- title: "(None - skip)",
141
- value: undefined,
142
- description: "No cross-team parent updates",
143
- },
144
144
  ...otherTeams.map((t) => ({
145
145
  title: t.name,
146
146
  value: t.id,
147
147
  description: "Parent issues in this team will be updated to Testing",
148
148
  })),
149
149
  ],
150
- initial: 0,
150
+ hint: "- Press space to select, enter to confirm. Leave empty to skip.",
151
151
  });
152
- if (!response.qaPmTeamId) {
153
- return undefined;
152
+ if (!response.qaPmTeamIds || response.qaPmTeamIds.length === 0) {
153
+ return [];
154
+ }
155
+ // For each selected QA/PM team, select its testing status
156
+ const qaPmTeams = [];
157
+ for (const teamId of response.qaPmTeamIds) {
158
+ const team = teams.find((t) => t.id === teamId);
159
+ if (!team)
160
+ continue;
161
+ const teamStates = teamStatesMap.get(teamId) || [];
162
+ const defaults = getDefaultStatusTransitions(teamStates);
163
+ // Find team key
164
+ const teamKey = Object.entries(teamsConfig).find(([_, t]) => t.id === teamId)?.[0] ||
165
+ team.name.toLowerCase().replace(/[^a-z0-9]/g, "_");
166
+ if (teamStates.length === 0) {
167
+ // No states available, use default
168
+ if (defaults.testing) {
169
+ qaPmTeams.push({
170
+ team: teamKey,
171
+ testing_status: defaults.testing,
172
+ });
173
+ }
174
+ continue;
175
+ }
176
+ const stateChoices = teamStates.map((s) => ({
177
+ title: `${s.name} (${s.type})`,
178
+ value: s.name,
179
+ }));
180
+ const statusResponse = await prompts({
181
+ type: "select",
182
+ name: "testingStatus",
183
+ message: `Select testing status for ${team.name}:`,
184
+ choices: [
185
+ { title: "(Skip this team)", value: undefined },
186
+ ...stateChoices,
187
+ ],
188
+ initial: defaults.testing
189
+ ? stateChoices.findIndex((c) => c.value === defaults.testing) + 1
190
+ : 0,
191
+ });
192
+ if (statusResponse.testingStatus) {
193
+ qaPmTeams.push({
194
+ team: teamKey,
195
+ testing_status: statusResponse.testingStatus,
196
+ });
197
+ }
154
198
  }
155
- return teams.find((t) => t.id === response.qaPmTeamId);
199
+ return qaPmTeams;
200
+ }
201
+ async function selectCompletionMode(hasQaPmTeams, options) {
202
+ if (!options.interactive) {
203
+ return hasQaPmTeams ? "upstream_strict" : "simple";
204
+ }
205
+ console.log("\nāœ… Completion Mode Configuration:");
206
+ const defaultMode = hasQaPmTeams ? 2 : 0; // upstream_strict if has QA/PM teams, else simple
207
+ const response = await prompts({
208
+ type: "select",
209
+ name: "mode",
210
+ message: "How should tasks be completed?",
211
+ choices: [
212
+ {
213
+ title: "Simple",
214
+ value: "simple",
215
+ description: "Mark task as done directly",
216
+ },
217
+ {
218
+ title: "Strict Review",
219
+ value: "strict_review",
220
+ description: "Mark task to dev team's testing status",
221
+ },
222
+ {
223
+ title: "Upstream Strict (recommended with QA/PM)",
224
+ value: "upstream_strict",
225
+ description: "Done + parent to testing, fallback to testing if no parent",
226
+ },
227
+ {
228
+ title: "Upstream Not Strict",
229
+ value: "upstream_not_strict",
230
+ description: "Done + parent to testing, no fallback",
231
+ },
232
+ ],
233
+ initial: defaultMode,
234
+ });
235
+ return response.mode || (hasQaPmTeams ? "upstream_strict" : "simple");
156
236
  }
157
237
  async function selectUser(users, options) {
158
238
  let currentUser = users[0];
@@ -220,19 +300,14 @@ async function selectStatusSource(options) {
220
300
  });
221
301
  return response.statusSource || "remote";
222
302
  }
223
- async function selectStatusMappings(devStates, qaStates, options) {
303
+ async function selectStatusMappings(devStates, options) {
224
304
  // Use dev team states for todo, in_progress, done, blocked
225
- // Use qa team states for testing (fallback to dev team if not set)
305
+ // Testing status is now configured separately (dev_testing_status and qa_pm_teams)
226
306
  const devDefaults = getDefaultStatusTransitions(devStates);
227
- const testingStates = qaStates && qaStates.length > 0 ? qaStates : devStates;
228
- const testingDefaults = getDefaultStatusTransitions(testingStates);
229
307
  if (!options.interactive || devStates.length === 0) {
230
- return {
231
- ...devDefaults,
232
- testing: testingDefaults.testing,
233
- };
308
+ return devDefaults;
234
309
  }
235
- console.log("\nšŸ“Š Configure status mappings:");
310
+ console.log("\nšŸ“Š Configure status mappings (dev team):");
236
311
  const devStateChoices = devStates.map((s) => ({
237
312
  title: `${s.name} (${s.type})`,
238
313
  value: s.name,
@@ -258,29 +333,8 @@ async function selectStatusMappings(devStates, qaStates, options) {
258
333
  choices: devStateChoices,
259
334
  initial: devStateChoices.findIndex((c) => c.value === devDefaults.done),
260
335
  });
261
- // Testing uses qa team states (or dev team if qa not set)
262
- const testingStateChoices = testingStates.map((s) => ({
263
- title: `${s.name} (${s.type})`,
264
- value: s.name,
265
- }));
266
- const testingChoices = [
267
- { title: "(None)", value: undefined },
268
- ...testingStateChoices,
269
- ];
270
- const testingMessage = qaStates && qaStates.length > 0
271
- ? 'Select status for "Testing" (from QA team, for parent tasks):'
272
- : 'Select status for "Testing" (optional, for parent tasks):';
273
- const testingResponse = await prompts({
274
- type: "select",
275
- name: "testing",
276
- message: testingMessage,
277
- choices: testingChoices,
278
- initial: testingDefaults.testing
279
- ? testingChoices.findIndex((c) => c.value === testingDefaults.testing)
280
- : 0,
281
- });
282
336
  const blockedChoices = [
283
- { title: "(None)", value: undefined },
337
+ { title: "(Skip - no blocked status)", value: undefined },
284
338
  ...devStateChoices,
285
339
  ];
286
340
  const blockedResponse = await prompts({
@@ -296,7 +350,7 @@ async function selectStatusMappings(devStates, qaStates, options) {
296
350
  todo: todoResponse.todo || devDefaults.todo,
297
351
  in_progress: inProgressResponse.in_progress || devDefaults.in_progress,
298
352
  done: doneResponse.done || devDefaults.done,
299
- testing: testingResponse.testing,
353
+ testing: devDefaults.testing, // Will be overridden by dev_testing_status in LocalConfig
300
354
  blocked: blockedResponse.blocked,
301
355
  };
302
356
  }
@@ -346,147 +400,39 @@ async function updateGitignore(tttDir, interactive) {
346
400
  // Silently ignore gitignore errors
347
401
  }
348
402
  }
349
- async function installClaudeCommands(interactive, statusSource) {
403
+ async function showPluginInstallInstructions(interactive) {
350
404
  if (!interactive) {
351
- return { installed: false, prefix: "" };
405
+ return false;
352
406
  }
353
- console.log("\nšŸ¤– Claude Code Commands:");
354
- // Ask if user wants to install commands
355
- const { install } = await prompts({
407
+ console.log("\nšŸ¤– Claude Code Plugin:");
408
+ const { showInstructions } = await prompts({
356
409
  type: "confirm",
357
- name: "install",
358
- message: "Install Claude Code commands? (work-on, done-job, sync-linear)",
410
+ name: "showInstructions",
411
+ message: "Show Claude Code plugin installation instructions? (provides /ttt-* commands)",
359
412
  initial: true,
360
413
  });
361
- if (!install) {
362
- return { installed: false, prefix: "" };
363
- }
364
- // Ask for prefix
365
- const { prefixChoice } = await prompts({
366
- type: "select",
367
- name: "prefixChoice",
368
- message: "Command prefix style:",
369
- choices: [
370
- {
371
- title: "No prefix (recommended)",
372
- value: "",
373
- description: "/work-on, /done-job, /sync-linear",
374
- },
375
- {
376
- title: "ttt:",
377
- value: "ttt:",
378
- description: "/ttt:work-on, /ttt:done-job, /ttt:sync-linear",
379
- },
380
- {
381
- title: "linear:",
382
- value: "linear:",
383
- description: "/linear:work-on, /linear:done-job, /linear:sync-linear",
384
- },
385
- {
386
- title: "Custom...",
387
- value: "custom",
388
- description: "Enter your own prefix",
389
- },
390
- ],
391
- initial: 0,
392
- });
393
- let prefix = prefixChoice || "";
394
- if (prefixChoice === "custom") {
395
- const { customPrefix } = await prompts({
396
- type: "text",
397
- name: "customPrefix",
398
- message: "Enter custom prefix (e.g., 'my:'):",
399
- initial: "",
400
- });
401
- prefix = customPrefix || "";
402
- }
403
- // Find templates directory
404
- // Try multiple locations: installed package, local dev
405
- const possibleTemplatePaths = [
406
- path.join(__dirname, "..", "templates", "claude-code-commands"),
407
- path.join(__dirname, "..", "..", "templates", "claude-code-commands"),
408
- path.join(process.cwd(), "templates", "claude-code-commands"),
409
- ];
410
- let templateDir = null;
411
- for (const p of possibleTemplatePaths) {
412
- try {
413
- await fs.access(p);
414
- templateDir = p;
415
- break;
416
- }
417
- catch {
418
- // Try next path
419
- }
420
- }
421
- if (!templateDir) {
422
- // Try to get repo URL from package.json
423
- let repoUrl = "https://github.com/wayne930242/team-toon-tack";
424
- try {
425
- const pkgPaths = [
426
- path.join(__dirname, "..", "package.json"),
427
- path.join(__dirname, "..", "..", "package.json"),
428
- ];
429
- for (const pkgPath of pkgPaths) {
430
- try {
431
- const pkgContent = await fs.readFile(pkgPath, "utf-8");
432
- const pkg = JSON.parse(pkgContent);
433
- if (pkg.repository?.url) {
434
- // Parse git+https://github.com/user/repo.git format
435
- repoUrl = pkg.repository.url
436
- .replace(/^git\+/, "")
437
- .replace(/\.git$/, "");
438
- }
439
- break;
440
- }
441
- catch {
442
- // Try next path
443
- }
444
- }
445
- }
446
- catch {
447
- // Use default URL
448
- }
449
- console.log(" ⚠ Could not find command templates. Please copy manually from:");
450
- console.log(` ${repoUrl}/tree/main/templates/claude-code-commands`);
451
- return { installed: false, prefix };
452
- }
453
- // Create .claude/commands directory
454
- const commandsDir = path.join(process.cwd(), ".claude", "commands");
455
- await fs.mkdir(commandsDir, { recursive: true });
456
- // Copy and rename template files
457
- const templateFiles = await fs.readdir(templateDir);
458
- const commandFiles = templateFiles.filter((f) => f.endsWith(".md"));
459
- for (const file of commandFiles) {
460
- const baseName = file.replace(".md", "");
461
- const newFileName = prefix ? `${prefix}${baseName}.md` : file;
462
- const srcPath = path.join(templateDir, file);
463
- const destPath = path.join(commandsDir, newFileName);
464
- // Read template content
465
- let content = await fs.readFile(srcPath, "utf-8");
466
- // Update the name in frontmatter if prefix is used
467
- if (prefix) {
468
- content = content.replace(/^(---\s*\n[\s\S]*?name:\s*)(\S+)/m, `$1${prefix}${baseName}`);
469
- }
470
- // Modify content based on statusSource for work-on and done-job
471
- if (statusSource === "local") {
472
- if (baseName === "work-on" || baseName.endsWith("work-on")) {
473
- // Update description for local mode
474
- content = content.replace(/Select a task and update status to "In Progress" on both local and Linear\./, 'Select a task and update local status to "In Progress". (Linear will be updated when you run `sync --update`)');
475
- // Add reminder after Complete section
476
- content = content.replace(/Use `?\/done-job`? to mark task as completed/, "Use `/done-job` to mark task as completed\n\n### 7. Sync to Linear\n\nWhen ready to update Linear with all your changes:\n\n```bash\nttt sync --update\n```");
477
- }
478
- if (baseName === "done-job" || baseName.endsWith("done-job")) {
479
- // Update description for local mode
480
- content = content.replace(/Mark a task as done and update Linear with commit details\./, "Mark a task as done locally. (Run `ttt sync --update` to push changes to Linear)");
481
- // Add reminder at the end
482
- content = content.replace(/## What It Does\n\n- Linear issue status → "Done"/, "## What It Does\n\n- Local status → `completed`");
483
- content += `\n## Sync to Linear\n\nAfter completing tasks, push all changes to Linear:\n\n\`\`\`bash\nttt sync --update\n\`\`\`\n`;
484
- }
485
- }
486
- await fs.writeFile(destPath, content, "utf-8");
487
- console.log(` āœ“ .claude/commands/${newFileName}`);
488
- }
489
- return { installed: true, prefix };
414
+ if (!showInstructions) {
415
+ return false;
416
+ }
417
+ console.log("\nā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”");
418
+ console.log("│ Install team-toon-tack plugin in Claude Code: │");
419
+ console.log("ā”œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¤");
420
+ console.log("│ │");
421
+ console.log("│ 1. Add marketplace: │");
422
+ console.log("│ /plugin marketplace add wayne930242/team-toon-tack │");
423
+ console.log("│ │");
424
+ console.log("│ 2. Install plugin: │");
425
+ console.log("│ /plugin install team-toon-tack@wayne930242 │");
426
+ console.log("│ │");
427
+ console.log("│ Available commands after install: │");
428
+ console.log("│ /ttt-sync - Sync Linear issues │");
429
+ console.log("│ /ttt-work-on - Start working on a task │");
430
+ console.log("│ /ttt-done - Complete current task │");
431
+ console.log("│ /ttt-status - Show/modify task status │");
432
+ console.log("│ /ttt-get-issue - Fetch issue details │");
433
+ console.log("│ │");
434
+ console.log("ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜");
435
+ return true;
490
436
  }
491
437
  async function init() {
492
438
  const args = process.argv.slice(2);
@@ -536,12 +482,9 @@ async function init() {
536
482
  console.error("Error: No teams found in your Linear workspace.");
537
483
  process.exit(1);
538
484
  }
539
- // Select teams
540
- const { selected: selectedTeams, primary: primaryTeam } = await selectTeams(teams, options);
541
- console.log(` Teams: ${selectedTeams.map((t) => t.name).join(", ")}`);
542
- if (selectedTeams.length > 1) {
543
- console.log(` Primary: ${primaryTeam.name}`);
544
- }
485
+ // Select dev team (single selection)
486
+ const devTeam = await selectDevTeam(teams, options);
487
+ console.log(` Dev Team: ${devTeam.name}`);
545
488
  // Fetch data from ALL teams (not just primary) to support cross-team operations
546
489
  console.log(` Fetching data from ${teams.length} teams...`);
547
490
  // Collect users from all teams, but labels only from primary team
@@ -563,8 +506,8 @@ async function init() {
563
506
  allUsers.push(user);
564
507
  }
565
508
  }
566
- // Labels: only from primary team (dev team)
567
- if (team.id === primaryTeam.id) {
509
+ // Labels: only from dev team
510
+ if (team.id === devTeam.id) {
568
511
  const labelsData = await client.issueLabels({
569
512
  filter: { team: { id: { eq: team.id } } },
570
513
  });
@@ -589,7 +532,7 @@ async function init() {
589
532
  }
590
533
  teamStatesMap.set(team.id, teamStates);
591
534
  }
592
- catch (error) {
535
+ catch {
593
536
  console.warn(` ⚠ Could not fetch data for team ${team.name}, skipping...`);
594
537
  }
595
538
  }
@@ -597,34 +540,34 @@ async function init() {
597
540
  const labels = allLabels;
598
541
  const states = allStates;
599
542
  // Get team-specific states for status mapping
600
- const primaryTeamStates = teamStatesMap.get(primaryTeam.id) || [];
543
+ const devTeamStates = teamStatesMap.get(devTeam.id) || [];
601
544
  console.log(` Users: ${users.length}`);
602
- console.log(` Labels: ${labels.length} (from ${primaryTeam.name})`);
545
+ console.log(` Labels: ${labels.length} (from ${devTeam.name})`);
603
546
  console.log(` Workflow states: ${states.length}`);
604
- // Get cycle from primary team (for current work tracking)
605
- const selectedTeam = await client.team(primaryTeam.id);
547
+ // Get cycle from dev team (for current work tracking)
548
+ const selectedTeam = await client.team(devTeam.id);
606
549
  const currentCycle = (await selectedTeam.activeCycle);
607
550
  // User selections
608
551
  const currentUser = await selectUser(users, options);
609
552
  const defaultLabel = await selectLabelFilter(labels, options);
610
553
  const statusSource = await selectStatusSource(options);
611
- const qaPmTeam = await selectQaPmTeam(teams, primaryTeam, options);
612
- // Get qa team states for testing status mapping (fallback to primary team if not set)
613
- const qaTeamStates = qaPmTeam ? teamStatesMap.get(qaPmTeam.id) : undefined;
614
- const statusTransitions = await selectStatusMappings(primaryTeamStates, qaTeamStates, options);
554
+ // Status transitions for dev team (todo, in_progress, done, blocked)
555
+ const statusTransitions = await selectStatusMappings(devTeamStates, options);
556
+ // Dev team testing status (for strict_review mode)
557
+ const devTestingStatus = await selectDevTestingStatus(devTeamStates, options);
558
+ // Build preliminary teams config for selectQaPmTeams
559
+ const teamsConfig = buildTeamsConfig(teams);
560
+ // QA/PM teams selection (multiple, each with its own testing status)
561
+ const qaPmTeams = await selectQaPmTeams(teams, devTeam, teamStatesMap, teamsConfig, options);
562
+ // Completion mode selection
563
+ const completionMode = await selectCompletionMode(qaPmTeams.length > 0, options);
615
564
  // Build config
616
565
  const config = buildConfig(teams, users, labels, states, statusTransitions, currentCycle ?? undefined);
617
566
  // Find keys
618
567
  const currentUserKey = findUserKey(config.users, currentUser.id);
619
- const primaryTeamKey = findTeamKey(config.teams, primaryTeam.id);
620
- const selectedTeamKeys = selectedTeams
621
- .map((team) => findTeamKey(config.teams, team.id))
622
- .filter((key) => key !== undefined);
623
- const qaPmTeamKey = qaPmTeam
624
- ? findTeamKey(config.teams, qaPmTeam.id)
625
- : undefined;
626
- const localConfig = buildLocalConfig(currentUserKey, primaryTeamKey, selectedTeamKeys, defaultLabel, undefined, // excludeLabels
627
- statusSource, qaPmTeamKey);
568
+ const devTeamKey = findTeamKey(config.teams, devTeam.id);
569
+ const localConfig = buildLocalConfig(currentUserKey, devTeamKey, devTestingStatus, qaPmTeams, completionMode, defaultLabel, undefined, // excludeLabels
570
+ statusSource);
628
571
  // Write config files
629
572
  console.log("\nšŸ“ Writing configuration files...");
630
573
  await fs.mkdir(paths.baseDir, { recursive: true });
@@ -659,10 +602,12 @@ async function init() {
659
602
  localConfig.current_user = existingLocal.current_user;
660
603
  if (existingLocal.team)
661
604
  localConfig.team = existingLocal.team;
662
- if (existingLocal.teams)
663
- localConfig.teams = existingLocal.teams;
664
- if (existingLocal.qa_pm_team)
665
- localConfig.qa_pm_team = existingLocal.qa_pm_team;
605
+ if (existingLocal.dev_testing_status)
606
+ localConfig.dev_testing_status = existingLocal.dev_testing_status;
607
+ if (existingLocal.qa_pm_teams)
608
+ localConfig.qa_pm_teams = existingLocal.qa_pm_teams;
609
+ if (existingLocal.completion_mode)
610
+ localConfig.completion_mode = existingLocal.completion_mode;
666
611
  if (existingLocal.label)
667
612
  localConfig.label = existingLocal.label;
668
613
  if (existingLocal.exclude_labels)
@@ -679,20 +624,24 @@ async function init() {
679
624
  console.log(` āœ“ ${paths.localPath}`);
680
625
  // Update .gitignore (always use relative path .ttt)
681
626
  await updateGitignore(".ttt", options.interactive ?? true);
682
- // Install Claude Code commands
683
- const { installed: commandsInstalled, prefix: commandPrefix } = await installClaudeCommands(options.interactive ?? true, statusSource);
627
+ // Show Claude Code plugin installation instructions
628
+ const pluginInstructionsShown = await showPluginInstallInstructions(options.interactive ?? true);
684
629
  // Summary
685
630
  console.log("\nāœ… Initialization complete!\n");
686
631
  console.log("Configuration summary:");
687
- console.log(` Teams: ${selectedTeams.map((t) => t.name).join(", ")}`);
688
- if (selectedTeams.length > 1) {
689
- console.log(` Primary team: ${primaryTeam.name}`);
690
- }
632
+ console.log(` Dev Team: ${devTeam.name}`);
691
633
  console.log(` User: ${currentUser.displayName || currentUser.name} (${currentUser.email})`);
692
634
  console.log(` Label filter: ${defaultLabel || "(none)"}`);
693
635
  console.log(` Status source: ${statusSource === "local" ? "local (use 'sync --update' to push)" : "remote (immediate sync)"}`);
694
- if (qaPmTeam) {
695
- console.log(` QA/PM team: ${qaPmTeam.name}`);
636
+ console.log(` Completion mode: ${completionMode}`);
637
+ if (devTestingStatus) {
638
+ console.log(` Dev testing status: ${devTestingStatus}`);
639
+ }
640
+ if (qaPmTeams.length > 0) {
641
+ console.log(` QA/PM teams:`);
642
+ for (const qaPmTeam of qaPmTeams) {
643
+ console.log(` - ${qaPmTeam.team}: ${qaPmTeam.testing_status}`);
644
+ }
696
645
  }
697
646
  console.log(` (Use 'ttt config filters' to set excluded labels/users)`);
698
647
  if (currentCycle) {
@@ -702,27 +651,16 @@ async function init() {
702
651
  console.log(` Todo: ${statusTransitions.todo}`);
703
652
  console.log(` In Progress: ${statusTransitions.in_progress}`);
704
653
  console.log(` Done: ${statusTransitions.done}`);
705
- if (statusTransitions.testing) {
706
- console.log(` Testing: ${statusTransitions.testing}`);
707
- }
708
654
  if (statusTransitions.blocked) {
709
655
  console.log(` Blocked: ${statusTransitions.blocked}`);
710
656
  }
711
- if (commandsInstalled) {
712
- const cmdPrefix = commandPrefix ? `${commandPrefix}` : "";
713
- console.log(` Claude commands: /${cmdPrefix}work-on, /${cmdPrefix}done-job, /${cmdPrefix}sync-linear`);
714
- }
715
657
  console.log("\nNext steps:");
716
658
  console.log(" 1. Set LINEAR_API_KEY in your shell profile:");
717
659
  console.log(` export LINEAR_API_KEY="${apiKey}"`);
718
660
  console.log(" 2. Run sync: ttt sync");
719
- if (commandsInstalled) {
720
- const cmdPrefix = commandPrefix ? `${commandPrefix}` : "";
721
- console.log(` 3. In Claude Code: /${cmdPrefix}work-on next`);
722
- console.log(`\nšŸ’” Tip: Edit .claude/commands/${cmdPrefix}work-on.md to customize the "Verify" section for your project.`);
723
- }
724
- else {
725
- console.log(" 3. Start working: ttt work-on");
661
+ console.log(" 3. Start working: ttt work-on");
662
+ if (pluginInstructionsShown) {
663
+ console.log("\nšŸ’” Tip: Install the Claude Code plugin for /ttt-* commands.");
726
664
  }
727
665
  }
728
666
  init().catch(console.error);
@@ -1,4 +1,4 @@
1
- import type { Config, LocalConfig, StatusTransitions, TeamConfig, UserConfig, LabelConfig } from "../utils.js";
1
+ import type { CompletionMode, Config, LabelConfig, LocalConfig, QaPmTeamConfig, StatusTransitions, TeamConfig, UserConfig } from "../utils.js";
2
2
  export interface LinearTeam {
3
3
  id: string;
4
4
  name: string;
@@ -38,4 +38,4 @@ export declare function getDefaultStatusTransitions(states: LinearState[]): Stat
38
38
  export declare function buildConfig(teams: LinearTeam[], users: LinearUser[], labels: LinearLabel[], states: LinearState[], statusTransitions: StatusTransitions, currentCycle?: LinearCycle): Config;
39
39
  export declare function findUserKey(usersConfig: Record<string, UserConfig>, userId: string): string;
40
40
  export declare function findTeamKey(teamsConfig: Record<string, TeamConfig>, teamId: string): string;
41
- export declare function buildLocalConfig(currentUserKey: string, primaryTeamKey: string, selectedTeamKeys: string[], defaultLabel?: string, excludeLabels?: string[], statusSource?: "remote" | "local", qaPmTeam?: string): LocalConfig;
41
+ export declare function buildLocalConfig(currentUserKey: string, devTeamKey: string, devTestingStatus?: string, qaPmTeams?: QaPmTeamConfig[], completionMode?: CompletionMode, defaultLabel?: string, excludeLabels?: string[], statusSource?: "remote" | "local"): LocalConfig;
@@ -108,12 +108,13 @@ export function findTeamKey(teamsConfig, teamId) {
108
108
  return (Object.entries(teamsConfig).find(([_, t]) => t.id === teamId)?.[0] ||
109
109
  Object.keys(teamsConfig)[0]);
110
110
  }
111
- export function buildLocalConfig(currentUserKey, primaryTeamKey, selectedTeamKeys, defaultLabel, excludeLabels, statusSource, qaPmTeam) {
111
+ export function buildLocalConfig(currentUserKey, devTeamKey, devTestingStatus, qaPmTeams, completionMode, defaultLabel, excludeLabels, statusSource) {
112
112
  return {
113
113
  current_user: currentUserKey,
114
- team: primaryTeamKey,
115
- teams: selectedTeamKeys.length > 1 ? selectedTeamKeys : undefined,
116
- qa_pm_team: qaPmTeam,
114
+ team: devTeamKey,
115
+ dev_testing_status: devTestingStatus,
116
+ qa_pm_teams: qaPmTeams && qaPmTeams.length > 0 ? qaPmTeams : undefined,
117
+ completion_mode: completionMode,
117
118
  label: defaultLabel,
118
119
  exclude_labels: excludeLabels && excludeLabels.length > 0 ? excludeLabels : undefined,
119
120
  status_source: statusSource,