prjct-cli 1.0.0 → 1.1.1

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/CHANGELOG.md CHANGED
@@ -1,5 +1,86 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.1.1] - 2026-02-06
4
+
5
+ ### Bug Fixes
6
+
7
+ - visual grouping with boxes and tables for structured output (PRJ-134) (#110)
8
+
9
+
10
+ ## [1.1.1] - 2026-02-05
11
+
12
+ ### Improved
13
+
14
+ - **Visual grouping for structured output (PRJ-134)**: Added `out.section()` and integrated `out.box()`, `out.table()`, `out.list()` into sync, doctor, and status commands
15
+
16
+ ### Implementation Details
17
+
18
+ Added `out.section(title)` method to the unified output system (`core/utils/output.ts`) — bold title with dim underline, chainable, quiet-mode aware.
19
+
20
+ Refactored three commands to use unified output helpers instead of raw `console.log`:
21
+ - **Sync** (`analysis.ts`): Summary metrics in `out.box()`, generated items via `out.section()` + `out.list()`
22
+ - **Doctor** (`doctor-service.ts`): Section headers via `out.section()`, recommendations via `out.list()`, summary via `out.done()`/`out.warn()`/`out.fail()`
23
+ - **Status** (`staleness-checker.ts`): Key-value details wrapped in box-drawing characters
24
+
25
+ ### Learnings
26
+
27
+ - `staleness-checker.formatStatus()` returns a string (not direct output), so `out.box()` can't be used directly — used inline box-drawing chars instead
28
+ - Doctor service had its own icon logic for check results that was worth preserving alongside the new section headers
29
+ - Unified output helpers reduce code while maintaining the same visual style
30
+
31
+ ### Test Plan
32
+
33
+ #### For QA
34
+ 1. Run `prjct sync` — verify boxed "Sync Summary" with metrics, "Generated" section header with underline, `✓` bullet items
35
+ 2. Run `prjct doctor` — verify bold+underline section headers for "System Tools", "Project Status", "Recommendations"
36
+ 3. Run `prjct status` — verify key-value details in box-drawing characters
37
+ 4. Run with `--quiet` flag — verify no visual output is printed
38
+
39
+ #### For Users
40
+ **What changed:** CLI output now uses visual grouping (boxes, section headers, structured lists) for better scannability
41
+ **How to use:** No changes needed — output is automatically improved
42
+ **Breaking changes:** None
43
+
44
+ ## [1.1.0] - 2026-02-05
45
+
46
+ ### Features
47
+
48
+ - visual workflow status command (PRJ-140) (#109)
49
+
50
+
51
+ ## [1.1.0] - 2026-02-05
52
+
53
+ ### Features
54
+
55
+ - **Workflow visualization (PRJ-140)**: New `p. status` command with visual workflow diagram
56
+
57
+ ### Implementation Details
58
+
59
+ Added visual workflow status template showing:
60
+ - ASCII workflow diagram with current position indicator (sync → task → work → done → ship)
61
+ - Subtask tree visualization with status icons (✅/🔄/⬜)
62
+ - Progress bar for subtask completion
63
+ - Paused tasks, queue summary, and recent ships
64
+ - Context staleness indicator from `prjct status --json`
65
+ - Compact mode for single-line status output
66
+
67
+ ### Learnings
68
+
69
+ - Template-first approach: Complex visualizations can be defined entirely in markdown templates without code changes
70
+
71
+ ### Test Plan
72
+
73
+ #### For QA
74
+ 1. Run `prjct sync` to install new status template
75
+ 2. Run `p. status` - verify workflow diagram displays
76
+ 3. Verify subtask tree shows correct status icons
77
+ 4. Test `p. status compact` for single-line output
78
+
79
+ #### For Users
80
+ - New `p. status` command shows visual workflow overview
81
+ - No breaking changes
82
+
83
+
3
84
  ## [1.0.0] - 2026-02-05
4
85
 
5
86
  ### Features
@@ -478,67 +478,55 @@ export class AnalysisCommands extends PrjctCommandsBase {
478
478
  // ═══════════════════════════════════════════════════════════════════════
479
479
  // SUCCESS LINE - Immediate confirmation with timing
480
480
  // ═══════════════════════════════════════════════════════════════════════
481
- console.log(`✅ Synced ${result.stats.name || 'project'} (${(elapsed / 1000).toFixed(1)}s)\n`)
481
+ out.done(`Synced ${result.stats.name || 'project'} (${(elapsed / 1000).toFixed(1)}s)`)
482
+ console.log('')
482
483
 
483
484
  // ═══════════════════════════════════════════════════════════════════════
484
- // KEY METRICS - Single scannable line
485
+ // SUMMARY BOX - Key metrics grouped visually
485
486
  // ═══════════════════════════════════════════════════════════════════════
486
- // Only show compression rate if meaningful (> 10%)
487
487
  const compressionPct = result.syncMetrics?.compressionRate
488
488
  ? Math.round(result.syncMetrics.compressionRate * 100)
489
489
  : 0
490
- const metricsLine = [
491
- `${result.stats.fileCount} files → ${contextFilesCount} context`,
492
- `${agentCount} agents`,
493
- compressionPct > 10 ? `${compressionPct}% reduction` : null,
494
- ]
495
- .filter(Boolean)
496
- .join(' | ')
497
- console.log(metricsLine)
498
-
499
- // Stack and branch info
500
490
  const framework = result.stats.frameworks.length > 0 ? ` (${result.stats.frameworks[0]})` : ''
501
- console.log(`Stack: ${result.stats.ecosystem}${framework} | Branch: ${result.git.branch}\n`)
491
+ const boxLines = [
492
+ `${result.stats.fileCount} files → ${contextFilesCount} context | ${agentCount} agents${compressionPct > 10 ? ` | ${compressionPct}% reduction` : ''}`,
493
+ `Stack: ${result.stats.ecosystem}${framework} | Branch: ${result.git.branch}`,
494
+ ]
495
+ out.box('Sync Summary', boxLines.join('\n'))
502
496
 
503
497
  // ═══════════════════════════════════════════════════════════════════════
504
498
  // CHANGES SECTION - What was generated/updated
505
499
  // ═══════════════════════════════════════════════════════════════════════
506
- console.log('Generated:')
507
-
508
- // Context files (condensed)
500
+ const generatedItems: string[] = []
509
501
  if (result.contextFiles.length > 0) {
510
- console.log(` ✓ ${result.contextFiles.length} context files`)
502
+ generatedItems.push(`${result.contextFiles.length} context files`)
511
503
  }
512
-
513
- // AI tools
514
504
  const successTools = result.aiTools?.filter((t) => t.success) || []
515
505
  if (successTools.length > 0) {
516
- const toolNames = successTools.map((t) => t.toolId).join(', ')
517
- console.log(` ✓ AI tools: ${toolNames}`)
506
+ generatedItems.push(`AI tools: ${successTools.map((t) => t.toolId).join(', ')}`)
518
507
  }
519
-
520
- // Agents (show count with breakdown)
521
508
  if (agentCount > 0) {
522
509
  const agentSummary =
523
510
  domainAgentCount > 0
524
511
  ? `${agentCount} agents (${domainAgentCount} domain)`
525
512
  : `${agentCount} agents`
526
- console.log(` ✓ ${agentSummary}`)
513
+ generatedItems.push(agentSummary)
527
514
  }
528
-
529
- // Skills
530
515
  if (result.skills.length > 0) {
531
516
  const skillWord = result.skills.length === 1 ? 'skill' : 'skills'
532
- console.log(` ✓ ${result.skills.length} ${skillWord}`)
517
+ generatedItems.push(`${result.skills.length} ${skillWord}`)
533
518
  }
534
519
 
520
+ out.section('Generated')
521
+ out.list(generatedItems, { bullet: '✓' })
535
522
  console.log('')
536
523
 
537
524
  // ═══════════════════════════════════════════════════════════════════════
538
525
  // STATUS INDICATOR - Repository state
539
526
  // ═══════════════════════════════════════════════════════════════════════
540
527
  if (result.git.hasChanges) {
541
- console.log('⚠️ Uncommitted changes detected\n')
528
+ out.warn('Uncommitted changes detected')
529
+ console.log('')
542
530
  }
543
531
 
544
532
  // ═══════════════════════════════════════════════════════════════════════
@@ -15,6 +15,7 @@ import path from 'node:path'
15
15
  import chalk from 'chalk'
16
16
  import configManager from '../infrastructure/config-manager'
17
17
  import pathManager from '../infrastructure/path-manager'
18
+ import out from '../utils/output'
18
19
  import { VERSION } from '../utils/version'
19
20
 
20
21
  // ============================================================================
@@ -367,32 +368,28 @@ class DoctorService {
367
368
  // ==========================================================================
368
369
 
369
370
  private printHeader(): void {
370
- console.log('')
371
- console.log(chalk.bold(`prjct doctor v${VERSION}`))
372
- console.log(chalk.dim('─'.repeat(40)))
371
+ out.section(`prjct doctor v${VERSION}`)
373
372
  }
374
373
 
375
374
  private printSection(title: string, checks: CheckResult[]): void {
376
- console.log('')
377
- console.log(chalk.bold(title))
375
+ out.section(title)
378
376
 
379
- for (const check of checks) {
377
+ const items = checks.map((check) => {
380
378
  const icon = this.getStatusIcon(check.status, check.optional)
381
379
  const name = check.name.padEnd(14)
382
380
  const detail = check.version || check.message || ''
383
381
  const optionalTag = check.optional && check.status === 'error' ? chalk.dim(' (optional)') : ''
382
+ return `${icon} ${name} ${chalk.dim(detail)}${optionalTag}`
383
+ })
384
384
 
385
- console.log(` ${icon} ${name} ${chalk.dim(detail)}${optionalTag}`)
385
+ for (const item of items) {
386
+ console.log(` ${item}`)
386
387
  }
387
388
  }
388
389
 
389
390
  private printRecommendations(recommendations: string[]): void {
390
- console.log('')
391
- console.log(chalk.bold('Recommendations'))
392
-
393
- for (const rec of recommendations) {
394
- console.log(` ${chalk.yellow('•')} ${rec}`)
395
- }
391
+ out.section('Recommendations')
392
+ out.list(recommendations, { bullet: chalk.yellow('') })
396
393
  }
397
394
 
398
395
  private printSummary(result: DoctorResult): void {
@@ -400,11 +397,11 @@ class DoctorService {
400
397
  console.log(chalk.dim('─'.repeat(40)))
401
398
 
402
399
  if (result.hasErrors) {
403
- console.log(chalk.red('Some required checks failed'))
400
+ out.fail('Some required checks failed')
404
401
  } else if (result.hasWarnings) {
405
- console.log(chalk.yellow('All required checks passed (some warnings)'))
402
+ out.warn('All required checks passed (some warnings)')
406
403
  } else {
407
- console.log(chalk.green('All checks passed'))
404
+ out.done('All checks passed')
408
405
  }
409
406
 
410
407
  console.log('')
@@ -195,23 +195,35 @@ export class StalenessChecker {
195
195
  lines.push('CLAUDE.md status: ✓ Fresh')
196
196
  }
197
197
 
198
- lines.push('─────────────────────────────')
199
-
198
+ // Build key-value table content
199
+ const details: string[] = []
200
200
  if (status.lastSyncCommit) {
201
- lines.push(`Last sync: ${status.lastSyncCommit}`)
201
+ details.push(`Last sync: ${status.lastSyncCommit}`)
202
202
  }
203
203
  if (status.currentCommit) {
204
- lines.push(`Current: ${status.currentCommit}`)
204
+ details.push(`Current: ${status.currentCommit}`)
205
205
  }
206
206
  if (status.commitsSinceSync > 0) {
207
- lines.push(`Commits since: ${status.commitsSinceSync}`)
207
+ details.push(`Commits since: ${status.commitsSinceSync}`)
208
208
  }
209
209
  if (status.daysSinceSync > 0) {
210
- lines.push(`Days since: ${status.daysSinceSync}`)
210
+ details.push(`Days since: ${status.daysSinceSync}`)
211
211
  }
212
212
  if (status.changedFiles.length > 0) {
213
- lines.push(`Files changed: ${status.changedFiles.length}`)
213
+ details.push(`Files changed: ${status.changedFiles.length}`)
214
214
  }
215
+
216
+ // Wrap details in a box
217
+ if (details.length > 0) {
218
+ const maxLen = Math.max(...details.map((l) => l.length))
219
+ const border = '─'.repeat(maxLen + 2)
220
+ lines.push(`┌${border}┐`)
221
+ for (const detail of details) {
222
+ lines.push(`│ ${detail.padEnd(maxLen)} │`)
223
+ }
224
+ lines.push(`└${border}┘`)
225
+ }
226
+
215
227
  if (status.significantChanges.length > 0) {
216
228
  lines.push(``)
217
229
  lines.push(`Significant changes:`)
@@ -206,6 +206,7 @@ interface Output {
206
206
  list(items: string[], options?: { bullet?: string; indent?: number }): Output
207
207
  table(rows: Array<Record<string, string | number>>, options?: { header?: boolean }): Output
208
208
  box(title: string, content: string): Output
209
+ section(title: string): Output
209
210
  stop(): Output
210
211
  step(current: number, total: number, msg: string): Output
211
212
  progress(current: number, total: number, msg?: string): Output
@@ -369,6 +370,15 @@ const out: Output = {
369
370
  return this
370
371
  },
371
372
 
373
+ // Section header: bold title + underline
374
+ section(title: string) {
375
+ this.stop()
376
+ if (quietMode) return this
377
+ console.log(`\n${chalk.bold(title)}`)
378
+ console.log(chalk.dim('─'.repeat(title.length)))
379
+ return this
380
+ },
381
+
372
382
  stop() {
373
383
  if (interval) {
374
384
  clearInterval(interval)
@@ -2495,6 +2495,15 @@ var init_output = __esm({
2495
2495
  console.log(chalk2.dim(`\u2514${border}\u2518`));
2496
2496
  return this;
2497
2497
  },
2498
+ // Section header: bold title + underline
2499
+ section(title) {
2500
+ this.stop();
2501
+ if (quietMode) return this;
2502
+ console.log(`
2503
+ ${chalk2.bold(title)}`);
2504
+ console.log(chalk2.dim("\u2500".repeat(title.length)));
2505
+ return this;
2506
+ },
2498
2507
  stop() {
2499
2508
  if (interval) {
2500
2509
  clearInterval(interval);
@@ -5448,6 +5457,7 @@ var init_doctor_service = __esm({
5448
5457
  "use strict";
5449
5458
  init_config_manager();
5450
5459
  init_path_manager();
5460
+ init_output();
5451
5461
  init_version();
5452
5462
  DoctorService = class {
5453
5463
  static {
@@ -5702,37 +5712,34 @@ var init_doctor_service = __esm({
5702
5712
  // OUTPUT
5703
5713
  // ==========================================================================
5704
5714
  printHeader() {
5705
- console.log("");
5706
- console.log(chalk3.bold(`prjct doctor v${VERSION}`));
5707
- console.log(chalk3.dim("\u2500".repeat(40)));
5715
+ output_default.section(`prjct doctor v${VERSION}`);
5708
5716
  }
5709
5717
  printSection(title, checks) {
5710
- console.log("");
5711
- console.log(chalk3.bold(title));
5712
- for (const check of checks) {
5718
+ output_default.section(title);
5719
+ const items = checks.map((check) => {
5713
5720
  const icon = this.getStatusIcon(check.status, check.optional);
5714
5721
  const name = check.name.padEnd(14);
5715
5722
  const detail = check.version || check.message || "";
5716
5723
  const optionalTag = check.optional && check.status === "error" ? chalk3.dim(" (optional)") : "";
5717
- console.log(` ${icon} ${name} ${chalk3.dim(detail)}${optionalTag}`);
5724
+ return `${icon} ${name} ${chalk3.dim(detail)}${optionalTag}`;
5725
+ });
5726
+ for (const item of items) {
5727
+ console.log(` ${item}`);
5718
5728
  }
5719
5729
  }
5720
5730
  printRecommendations(recommendations) {
5721
- console.log("");
5722
- console.log(chalk3.bold("Recommendations"));
5723
- for (const rec of recommendations) {
5724
- console.log(` ${chalk3.yellow("\u2022")} ${rec}`);
5725
- }
5731
+ output_default.section("Recommendations");
5732
+ output_default.list(recommendations, { bullet: chalk3.yellow("\u2022") });
5726
5733
  }
5727
5734
  printSummary(result) {
5728
5735
  console.log("");
5729
5736
  console.log(chalk3.dim("\u2500".repeat(40)));
5730
5737
  if (result.hasErrors) {
5731
- console.log(chalk3.red("\u2717 Some required checks failed"));
5738
+ output_default.fail("Some required checks failed");
5732
5739
  } else if (result.hasWarnings) {
5733
- console.log(chalk3.yellow("\u26A0 All required checks passed (some warnings)"));
5740
+ output_default.warn("All required checks passed (some warnings)");
5734
5741
  } else {
5735
- console.log(chalk3.green("\u2713 All checks passed"));
5742
+ output_default.done("All checks passed");
5736
5743
  }
5737
5744
  console.log("");
5738
5745
  }
@@ -18383,38 +18390,37 @@ ${formatFullDiff(diff)}`);
18383
18390
  const agentCount = result.agents.length;
18384
18391
  const domainAgentCount = result.agents.filter((a) => a.type === "domain").length;
18385
18392
  await command_installer_default.installGlobalConfig();
18386
- console.log(`\u2705 Synced ${result.stats.name || "project"} (${(elapsed / 1e3).toFixed(1)}s)
18387
- `);
18393
+ output_default.done(`Synced ${result.stats.name || "project"} (${(elapsed / 1e3).toFixed(1)}s)`);
18394
+ console.log("");
18388
18395
  const compressionPct = result.syncMetrics?.compressionRate ? Math.round(result.syncMetrics.compressionRate * 100) : 0;
18389
- const metricsLine = [
18390
- `${result.stats.fileCount} files \u2192 ${contextFilesCount} context`,
18391
- `${agentCount} agents`,
18392
- compressionPct > 10 ? `${compressionPct}% reduction` : null
18393
- ].filter(Boolean).join(" | ");
18394
- console.log(metricsLine);
18395
18396
  const framework = result.stats.frameworks.length > 0 ? ` (${result.stats.frameworks[0]})` : "";
18396
- console.log(`Stack: ${result.stats.ecosystem}${framework} | Branch: ${result.git.branch}
18397
- `);
18398
- console.log("Generated:");
18397
+ const boxLines = [
18398
+ `${result.stats.fileCount} files \u2192 ${contextFilesCount} context | ${agentCount} agents${compressionPct > 10 ? ` | ${compressionPct}% reduction` : ""}`,
18399
+ `Stack: ${result.stats.ecosystem}${framework} | Branch: ${result.git.branch}`
18400
+ ];
18401
+ output_default.box("Sync Summary", boxLines.join("\n"));
18402
+ const generatedItems = [];
18399
18403
  if (result.contextFiles.length > 0) {
18400
- console.log(` \u2713 ${result.contextFiles.length} context files`);
18404
+ generatedItems.push(`${result.contextFiles.length} context files`);
18401
18405
  }
18402
18406
  const successTools = result.aiTools?.filter((t) => t.success) || [];
18403
18407
  if (successTools.length > 0) {
18404
- const toolNames = successTools.map((t) => t.toolId).join(", ");
18405
- console.log(` \u2713 AI tools: ${toolNames}`);
18408
+ generatedItems.push(`AI tools: ${successTools.map((t) => t.toolId).join(", ")}`);
18406
18409
  }
18407
18410
  if (agentCount > 0) {
18408
18411
  const agentSummary = domainAgentCount > 0 ? `${agentCount} agents (${domainAgentCount} domain)` : `${agentCount} agents`;
18409
- console.log(` \u2713 ${agentSummary}`);
18412
+ generatedItems.push(agentSummary);
18410
18413
  }
18411
18414
  if (result.skills.length > 0) {
18412
18415
  const skillWord = result.skills.length === 1 ? "skill" : "skills";
18413
- console.log(` \u2713 ${result.skills.length} ${skillWord}`);
18416
+ generatedItems.push(`${result.skills.length} ${skillWord}`);
18414
18417
  }
18418
+ output_default.section("Generated");
18419
+ output_default.list(generatedItems, { bullet: "\u2713" });
18415
18420
  console.log("");
18416
18421
  if (result.git.hasChanges) {
18417
- console.log("\u26A0\uFE0F Uncommitted changes detected\n");
18422
+ output_default.warn("Uncommitted changes detected");
18423
+ console.log("");
18418
18424
  }
18419
18425
  showNextSteps("sync");
18420
18426
  return {
@@ -19574,21 +19580,30 @@ var init_staleness_checker = __esm({
19574
19580
  } else {
19575
19581
  lines.push("CLAUDE.md status: \u2713 Fresh");
19576
19582
  }
19577
- lines.push("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
19583
+ const details = [];
19578
19584
  if (status.lastSyncCommit) {
19579
- lines.push(`Last sync: ${status.lastSyncCommit}`);
19585
+ details.push(`Last sync: ${status.lastSyncCommit}`);
19580
19586
  }
19581
19587
  if (status.currentCommit) {
19582
- lines.push(`Current: ${status.currentCommit}`);
19588
+ details.push(`Current: ${status.currentCommit}`);
19583
19589
  }
19584
19590
  if (status.commitsSinceSync > 0) {
19585
- lines.push(`Commits since: ${status.commitsSinceSync}`);
19591
+ details.push(`Commits since: ${status.commitsSinceSync}`);
19586
19592
  }
19587
19593
  if (status.daysSinceSync > 0) {
19588
- lines.push(`Days since: ${status.daysSinceSync}`);
19594
+ details.push(`Days since: ${status.daysSinceSync}`);
19589
19595
  }
19590
19596
  if (status.changedFiles.length > 0) {
19591
- lines.push(`Files changed: ${status.changedFiles.length}`);
19597
+ details.push(`Files changed: ${status.changedFiles.length}`);
19598
+ }
19599
+ if (details.length > 0) {
19600
+ const maxLen = Math.max(...details.map((l) => l.length));
19601
+ const border = "\u2500".repeat(maxLen + 2);
19602
+ lines.push(`\u250C${border}\u2510`);
19603
+ for (const detail of details) {
19604
+ lines.push(`\u2502 ${detail.padEnd(maxLen)} \u2502`);
19605
+ }
19606
+ lines.push(`\u2514${border}\u2518`);
19592
19607
  }
19593
19608
  if (status.significantChanges.length > 0) {
19594
19609
  lines.push(``);
@@ -26859,7 +26874,7 @@ var require_package = __commonJS({
26859
26874
  "package.json"(exports, module) {
26860
26875
  module.exports = {
26861
26876
  name: "prjct-cli",
26862
- version: "1.0.0",
26877
+ version: "1.1.1",
26863
26878
  description: "Context layer for AI agents. Project context for Claude Code, Gemini CLI, and more.",
26864
26879
  main: "core/index.ts",
26865
26880
  bin: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prjct-cli",
3
- "version": "1.0.0",
3
+ "version": "1.1.1",
4
4
  "description": "Context layer for AI agents. Project context for Claude Code, Gemini CLI, and more.",
5
5
  "main": "core/index.ts",
6
6
  "bin": {
@@ -0,0 +1,207 @@
1
+ ---
2
+ allowed-tools: [Read, Bash]
3
+ ---
4
+
5
+ # p. status
6
+
7
+ Visual workflow status showing current position in the prjct lifecycle.
8
+
9
+ ## Step 1: Resolve Project Paths
10
+
11
+ ```bash
12
+ # Get projectId from local config
13
+ cat .prjct/prjct.config.json | grep -o '"projectId"[[:space:]]*:[[:space:]]*"[^"]*"' | cut -d'"' -f4
14
+ ```
15
+
16
+ Set `globalPath = ~/.prjct-cli/projects/{projectId}`
17
+
18
+ ## Step 2: Read State and Context
19
+
20
+ READ:
21
+ - `{globalPath}/storage/state.json` → current task, paused, previous
22
+ - `{globalPath}/storage/queue.json` → upcoming tasks
23
+ - `{globalPath}/storage/shipped.json` → recent ships
24
+ - `{globalPath}/project.json` → lastSync timestamp
25
+
26
+ ```bash
27
+ # Get staleness info
28
+ prjct status --json 2>/dev/null || echo '{"isStale": false}'
29
+ ```
30
+
31
+ ## Step 3: Determine Workflow Position
32
+
33
+ Based on state.json, determine current position:
34
+
35
+ ```
36
+ IF no currentTask AND no previousTask:
37
+ position = "ready" # Ready to start (after sync)
38
+ ELSE IF currentTask.status == "active":
39
+ position = "working" # In task
40
+ ELSE IF currentTask.status == "in_review":
41
+ position = "reviewing" # PR open, waiting for merge
42
+ ELSE IF currentTask.status == "shipped":
43
+ position = "shipped" # Ready for next task
44
+ ELSE:
45
+ position = "idle"
46
+ ```
47
+
48
+ ## Step 4: Calculate Progress
49
+
50
+ ```
51
+ IF currentTask.subtasks exists:
52
+ completed = count where status == "completed"
53
+ total = subtasks.length
54
+ percent = (completed / total) * 100
55
+ progressBar = generateBar(percent, 10) # 10 chars wide
56
+ ```
57
+
58
+ Progress bar generation:
59
+ ```
60
+ filled = floor(percent / 10)
61
+ empty = 10 - filled
62
+ bar = "█" × filled + "░" × empty
63
+ ```
64
+
65
+ ## Step 5: Format Subtask Tree
66
+
67
+ ```
68
+ FOR EACH subtask in currentTask.subtasks:
69
+ IF index == currentSubtaskIndex:
70
+ prefix = "🔄" # Current
71
+ ELSE IF subtask.status == "completed":
72
+ prefix = "✅" # Done
73
+ ELSE:
74
+ prefix = "⬜" # Pending
75
+
76
+ connector = (index == last) ? "└─" : "├─"
77
+ OUTPUT: " {connector} {prefix} {subtask.description}"
78
+ ```
79
+
80
+ ---
81
+
82
+ ## Output: Workflow Diagram
83
+
84
+ ```
85
+ 📊 WORKFLOW STATUS
86
+
87
+ ┌─────────────────────────────────────────────────────────┐
88
+ │ │
89
+ │ sync ──▶ task ──▶ [work] ──▶ done ──▶ ship │
90
+ │ ○ ○ ● ○ ○ │
91
+ │ ▲ │
92
+ │ YOU ARE HERE │
93
+ │ │
94
+ └─────────────────────────────────────────────────────────┘
95
+ ```
96
+
97
+ Position indicators:
98
+ - `○` = not active
99
+ - `●` = current position
100
+ - Arrow indicates flow direction
101
+
102
+ ---
103
+
104
+ ## Output: Full Status
105
+
106
+ ```
107
+ 📊 WORKFLOW STATUS
108
+
109
+ ┌─────────────────────────────────────────────────────────┐
110
+ │ sync ──▶ task ──▶ work ──▶ done ──▶ ship │
111
+ │ {s} {t} {w} {d} {h} │
112
+ └─────────────────────────────────────────────────────────┘
113
+
114
+ 🎯 Current: {currentTask.parentDescription}
115
+ Branch: {currentTask.branch}
116
+ Type: {currentTask.type} | Started: {elapsed}
117
+ {IF linearId: "Linear: {linearId}"}
118
+
119
+ Progress: {progressBar} {completed}/{total} subtasks
120
+ {subtask tree}
121
+
122
+ ⏸️ Paused: {pausedTasks[0].description or "none"}
123
+
124
+ 📋 Queue: {queueCount} tasks
125
+ {IF queueCount > 0:}
126
+ • {queue[0].description}
127
+ • {queue[1].description}
128
+ {... up to 3}
129
+
130
+ 🚀 Last ship: {previousTask.description} ({daysSince})
131
+ {IF previousTask.prUrl: "PR: {prUrl}"}
132
+
133
+ 📡 Context: {staleness status}
134
+ Last sync: {timeSinceSync}
135
+ {IF isStale: "⚠️ Run `p. sync` to refresh"}
136
+ ```
137
+
138
+ ---
139
+
140
+ ## Output: Compact (`p. status compact`)
141
+
142
+ Single-line summary:
143
+
144
+ ```
145
+ {position_emoji} {currentTask.description} │ {progressBar} {completed}/{total} │ 📋 {queueCount} │ {staleness_emoji}
146
+ ```
147
+
148
+ Position emojis:
149
+ - 🔄 = working
150
+ - 👀 = reviewing
151
+ - ✅ = shipped
152
+ - 💤 = idle
153
+
154
+ Staleness emojis:
155
+ - ✅ = fresh
156
+ - ⚠️ = stale
157
+
158
+ ---
159
+
160
+ ## Output: No Active Task
161
+
162
+ ```
163
+ 📊 WORKFLOW STATUS
164
+
165
+ ┌─────────────────────────────────────────────────────────┐
166
+ │ sync ──▶ task ──▶ work ──▶ done ──▶ ship │
167
+ │ ● ○ ○ ○ ○ │
168
+ └─────────────────────────────────────────────────────────┘
169
+
170
+ 💤 No active task
171
+
172
+ 📋 Queue: {queueCount} tasks
173
+ {IF queueCount > 0:}
174
+ • {queue[0].description}
175
+
176
+ 🚀 Last ship: {previousTask.description} ({daysSince})
177
+
178
+ Next: `p. task "description"` or `p. task PRJ-XXX`
179
+ ```
180
+
181
+ ---
182
+
183
+ ## Elapsed Time Formatting
184
+
185
+ ```
186
+ IF minutes < 60: "{minutes}m"
187
+ ELSE IF hours < 24: "{hours}h {minutes}m"
188
+ ELSE: "{days}d {hours}h"
189
+ ```
190
+
191
+ ---
192
+
193
+ ## Context Staleness
194
+
195
+ From `prjct status --json`:
196
+ ```json
197
+ {
198
+ "isStale": true,
199
+ "commitsSinceSync": 15,
200
+ "daysSinceSync": 3,
201
+ "significantChanges": ["package.json", "tsconfig.json"]
202
+ }
203
+ ```
204
+
205
+ Display:
206
+ - Fresh (< 10 commits, < 3 days): `✅ Fresh (synced {time} ago)`
207
+ - Stale: `⚠️ Stale ({commits} commits, {days}d) - run p. sync`