@boyingliu01/xp-gate 0.10.12 → 0.10.13

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.
@@ -102,6 +102,15 @@ describe('doctor', () => {
102
102
  );
103
103
  }
104
104
 
105
+ function tuiJsonPath() {
106
+ return path.join(tmpHome, '.config', 'opencode', 'tui.json');
107
+ }
108
+
109
+ function ensureTuiRegistered() {
110
+ fs.mkdirSync(path.dirname(tuiJsonPath()), { recursive: true });
111
+ fs.writeFileSync(tuiJsonPath(), JSON.stringify({ plugin: ['@boyingliu01/opencode-plugin/tui'] }, null, 2));
112
+ }
113
+
105
114
  function setupLocalInstall() {
106
115
  createXpGatePreCommit(projectHooksDir());
107
116
  createXpGatePrePush(projectHooksDir());
@@ -183,6 +192,7 @@ describe('doctor', () => {
183
192
 
184
193
  it('AC-05: doctor reports all checks passed for healthy local install', async () => {
185
194
  setupLocalInstall();
195
+ ensureTuiRegistered();
186
196
  seedVersionCache();
187
197
  mockExecSuccess();
188
198
  const { doctor } = require('../doctor');
@@ -198,6 +208,7 @@ describe('doctor', () => {
198
208
 
199
209
  it('AC-05: doctor reports all checks passed for healthy global install', async () => {
200
210
  setupGlobalInstall();
211
+ ensureTuiRegistered();
201
212
  seedVersionCache();
202
213
  mockExecSuccess();
203
214
  const { doctor } = require('../doctor');
@@ -352,6 +363,7 @@ describe('doctor', () => {
352
363
 
353
364
  it('AC-10: doctor --fix corrects core.hooksPath in global mode', async () => {
354
365
  setupGlobalInstall();
366
+ ensureTuiRegistered();
355
367
  seedVersionCache();
356
368
  // Mock hooksPath pointing somewhere else
357
369
  execSpy = vi.spyOn(childProcess, 'execSync').mockImplementation((cmd) => {
@@ -455,6 +467,7 @@ describe('doctor', () => {
455
467
 
456
468
  it('passes version check when config version matches package version', async () => {
457
469
  setupLocalInstall();
470
+ ensureTuiRegistered();
458
471
  seedVersionCache();
459
472
  // Write config with matching version
460
473
  const pkg = JSON.parse(fs.readFileSync(
@@ -476,6 +489,7 @@ describe('doctor', () => {
476
489
 
477
490
  it('passes version check when config has no version field (legacy)', async () => {
478
491
  setupLocalInstall();
492
+ ensureTuiRegistered();
479
493
  seedVersionCache();
480
494
  // Config without version field — legacy install, should not fail
481
495
  mockExecSuccess();
@@ -521,6 +535,7 @@ describe('doctor', () => {
521
535
 
522
536
  it('passes templateDir check when templateDir matches current platform', async () => {
523
537
  setupLocalInstall();
538
+ ensureTuiRegistered();
524
539
  seedVersionCache();
525
540
  // Write config with correct qoder templateDir
526
541
  const cfg = JSON.parse(fs.readFileSync(configFile(), 'utf8'));
@@ -546,6 +561,7 @@ describe('doctor', () => {
546
561
 
547
562
  it('passes templateDir check when config has no templateDir field (legacy)', async () => {
548
563
  setupLocalInstall();
564
+ ensureTuiRegistered();
549
565
  seedVersionCache();
550
566
  // Config without templateDir — legacy install, should not fail
551
567
  mockExecSuccess();
@@ -563,6 +579,7 @@ describe('doctor', () => {
563
579
 
564
580
  it('AC-004-01: doctor shows upgrade prompt at end when outdated', async () => {
565
581
  setupLocalInstall();
582
+ ensureTuiRegistered();
566
583
  mockExecSuccess();
567
584
  // Remove version cache so checkUpgrade hits the real npm registry
568
585
  const cachePath = path.join(tmpHome, '.xp-gate', 'version-cache.json');
@@ -582,6 +599,7 @@ describe('doctor', () => {
582
599
 
583
600
  it('AC-004-02: doctor does NOT show upgrade prompt when up to date', async () => {
584
601
  setupLocalInstall();
602
+ ensureTuiRegistered();
585
603
  mockExecSuccess();
586
604
  // Write a cache with a future version so checkUpgrade says "not outdated"
587
605
  const cachePath = path.join(tmpHome, '.xp-gate', 'version-cache.json');
@@ -606,6 +624,7 @@ describe('doctor', () => {
606
624
 
607
625
  it('AC-004-03: doctor does NOT fail when version check throws', async () => {
608
626
  setupLocalInstall();
627
+ ensureTuiRegistered();
609
628
  seedVersionCache();
610
629
  mockExecSuccess();
611
630
  // Write corrupt cache to trigger checkUpgrade error path
@@ -642,4 +661,140 @@ describe('doctor', () => {
642
661
  expect.stringContaining('Restored')
643
662
  );
644
663
  });
664
+
665
+ // === Check 9: TUI auto-registration (tui.json) ===
666
+
667
+ it('Check 9: PASS when tui.json has @boyingliu01/opencode-plugin/tui registered', async () => {
668
+ setupLocalInstall();
669
+ seedVersionCache();
670
+ mockExecSuccess();
671
+ // Create tui.json with the plugin registered
672
+ fs.mkdirSync(path.dirname(tuiJsonPath()), { recursive: true });
673
+ fs.writeFileSync(tuiJsonPath(), JSON.stringify({ plugin: ['@boyingliu01/opencode-plugin/tui'] }, null, 2));
674
+ const { doctor } = require('../doctor');
675
+
676
+ const result = await doctor([]);
677
+
678
+ expect(result).toBe(0);
679
+ expect(logSpy).toHaveBeenCalledWith(
680
+ expect.stringContaining('@boyingliu01/opencode-plugin/tui registered')
681
+ );
682
+ });
683
+
684
+ it('Check 9: FAIL when tui.json is missing', async () => {
685
+ setupLocalInstall();
686
+ seedVersionCache();
687
+ mockExecSuccess();
688
+ // Ensure no tui.json
689
+ if (fs.existsSync(tuiJsonPath())) fs.unlinkSync(tuiJsonPath());
690
+ const { doctor } = require('../doctor');
691
+
692
+ const result = await doctor([]);
693
+
694
+ expect(result).toBe(1);
695
+ expect(logSpy).toHaveBeenCalledWith(
696
+ expect.stringContaining('TUI registration: Not registered')
697
+ );
698
+ });
699
+
700
+ it('Check 9: FAIL when tui.json exists without @boyingliu01/opencode-plugin/tui entry', async () => {
701
+ setupLocalInstall();
702
+ seedVersionCache();
703
+ mockExecSuccess();
704
+ fs.mkdirSync(path.dirname(tuiJsonPath()), { recursive: true });
705
+ fs.writeFileSync(tuiJsonPath(), JSON.stringify({ plugin: ['some-other-plugin'] }, null, 2));
706
+ const { doctor } = require('../doctor');
707
+
708
+ const result = await doctor([]);
709
+
710
+ expect(result).toBe(1);
711
+ expect(logSpy).toHaveBeenCalledWith(
712
+ expect.stringContaining('TUI registration: Not registered')
713
+ );
714
+ });
715
+
716
+ it('Check 9: WARN when tui.json exists but has no plugin array', async () => {
717
+ setupLocalInstall();
718
+ seedVersionCache();
719
+ mockExecSuccess();
720
+ fs.mkdirSync(path.dirname(tuiJsonPath()), { recursive: true });
721
+ fs.writeFileSync(tuiJsonPath(), JSON.stringify({ someKey: 'value' }, null, 2));
722
+ const { doctor } = require('../doctor');
723
+
724
+ const result = await doctor([]);
725
+
726
+ expect(result).toBe(1);
727
+ expect(logSpy).toHaveBeenCalledWith(
728
+ expect.stringContaining('TUI registration: Not registered')
729
+ );
730
+ });
731
+
732
+ it('Check 9: FAIL with backup when tui.json is corrupt JSON', async () => {
733
+ setupLocalInstall();
734
+ seedVersionCache();
735
+ mockExecSuccess();
736
+ fs.mkdirSync(path.dirname(tuiJsonPath()), { recursive: true });
737
+ fs.writeFileSync(tuiJsonPath(), 'this is not { valid json');
738
+ const { doctor } = require('../doctor');
739
+
740
+ const result = await doctor([]);
741
+
742
+ expect(result).toBe(1);
743
+ expect(logSpy).toHaveBeenCalledWith(
744
+ expect.stringContaining('TUI registration: Corrupt')
745
+ );
746
+ });
747
+
748
+ // === --fix TUI registration ===
749
+
750
+ it('--fix: diagnoses TUI not registered then re-diagnoses after fix', async () => {
751
+ setupLocalInstall();
752
+ seedVersionCache();
753
+ mockExecSuccess();
754
+ // No tui.json
755
+ if (fs.existsSync(path.join(tmpHome, '.config', 'opencode'))) {
756
+ fs.rmSync(path.join(tmpHome, '.config', 'opencode'), { recursive: true, force: true });
757
+ }
758
+ const { doctor } = require('../doctor');
759
+
760
+ const result = await doctor(['--fix']);
761
+
762
+ expect(result).toBe(0);
763
+ // After fix, tui.json should exist with the plugin registered
764
+ expect(fs.existsSync(tuiJsonPath())).toBe(true);
765
+ const tui = JSON.parse(fs.readFileSync(tuiJsonPath(), 'utf8'));
766
+ expect(tui.plugin).toContain('@boyingliu01/opencode-plugin/tui');
767
+ });
768
+
769
+ it('--fix: append to existing tui.json plugin array', async () => {
770
+ setupLocalInstall();
771
+ seedVersionCache();
772
+ mockExecSuccess();
773
+ fs.mkdirSync(path.dirname(tuiJsonPath()), { recursive: true });
774
+ fs.writeFileSync(tuiJsonPath(), JSON.stringify({ plugin: ['some-other-plugin'] }, null, 2));
775
+ const { doctor } = require('../doctor');
776
+
777
+ const result = await doctor(['--fix']);
778
+
779
+ expect(result).toBe(0);
780
+ const tui = JSON.parse(fs.readFileSync(tuiJsonPath(), 'utf8'));
781
+ expect(tui.plugin).toContain('some-other-plugin');
782
+ expect(tui.plugin).toContain('@boyingliu01/opencode-plugin/tui');
783
+ });
784
+
785
+ it('--fix: idempotent — does not duplicate entry when already registered', async () => {
786
+ setupLocalInstall();
787
+ seedVersionCache();
788
+ mockExecSuccess();
789
+ fs.mkdirSync(path.dirname(tuiJsonPath()), { recursive: true });
790
+ fs.writeFileSync(tuiJsonPath(), JSON.stringify({ plugin: ['@boyingliu01/opencode-plugin/tui'] }, null, 2));
791
+ const { doctor } = require('../doctor');
792
+
793
+ const result = await doctor(['--fix']);
794
+
795
+ expect(result).toBe(0);
796
+ const tui = JSON.parse(fs.readFileSync(tuiJsonPath(), 'utf8'));
797
+ // Should still have exactly one entry
798
+ expect(tui.plugin.filter(p => p === '@boyingliu01/opencode-plugin/tui').length).toBe(1);
799
+ });
645
800
  });
package/lib/doctor.js CHANGED
@@ -229,6 +229,9 @@ function diagnose() {
229
229
  // --- Check 6: Environment dependencies ---
230
230
  checkEnv(checks);
231
231
 
232
+ // --- Check 7: TUI auto-registration ---
233
+ issues += diagnoseTuiRegistration(checks);
234
+
232
235
  return { checks, issues };
233
236
  }
234
237
 
@@ -438,6 +441,7 @@ function fixIssues(checks, config) {
438
441
  fixed = fixHooksByMode(config, srcDir) || fixed;
439
442
  fixed = fixGlobalHooksPath(config) || fixed;
440
443
  fixed = fixMissingAdapters(config.mode, srcDir, getAdaptersDirByMode(config)) || fixed;
444
+ fixed = fixTuiRegistration() || fixed;
441
445
 
442
446
  if (!fixed) {
443
447
  console.log(' No fixable issues found.');
@@ -499,6 +503,124 @@ function diagnoseOpenCodePlugin(checks) {
499
503
  return 0;
500
504
  }
501
505
 
506
+ /**
507
+ * TUI registration path and expected plugin entry.
508
+ */
509
+ const TUI_JSON_PATH = path.join(HOME_DIR, '.config', 'opencode', 'tui.json');
510
+ const TUI_PLUGIN_ENTRY = '@boyingliu01/opencode-plugin/tui';
511
+
512
+ /**
513
+ * Read and parse tui.json, returning { data, error }.
514
+ * data = parsed object on success, null on file missing, undefined on corrupt.
515
+ */
516
+ function readTuiJson() {
517
+ if (!fs.existsSync(TUI_JSON_PATH)) return { data: null, error: null };
518
+ try {
519
+ const raw = fs.readFileSync(TUI_JSON_PATH, 'utf8');
520
+ const data = JSON.parse(raw);
521
+ return { data, error: null };
522
+ } catch (e) {
523
+ return { data: undefined, error: `Corrupt JSON: ${e.message}` };
524
+ }
525
+ }
526
+
527
+ /**
528
+ * Check 9: TUI auto-registration in ~/.config/opencode/tui.json.
529
+ * @returns {number} issue count
530
+ */
531
+ function diagnoseTuiRegistration(checks) {
532
+ const { data, error } = readTuiJson();
533
+
534
+ if (error) {
535
+ checks.push({ name: 'TUI registration', status: 'FAIL', detail: error });
536
+ return 1;
537
+ }
538
+
539
+ if (data === null) {
540
+ checks.push({ name: 'TUI registration', status: 'FAIL', detail: 'Not registered' });
541
+ return 1;
542
+ }
543
+
544
+ const plugins = Array.isArray(data.plugin) ? data.plugin : [];
545
+ if (plugins.includes(TUI_PLUGIN_ENTRY)) {
546
+ checks.push({ name: 'TUI registration', status: 'PASS', detail: `${TUI_PLUGIN_ENTRY} registered` });
547
+ return 0;
548
+ }
549
+
550
+ checks.push({ name: 'TUI registration', status: 'FAIL', detail: 'Not registered' });
551
+ return 1;
552
+ }
553
+
554
+ /**
555
+ * Fix TUI registration: ensure @boyingliu01/opencode-plugin/tui is in tui.json.
556
+ * Uses atomic write (tmp + renameSync) for JSON safety.
557
+ * On corrupt JSON: backup to .corrupt-{timestamp}.bak then rebuild.
558
+ * Idempotent: skips if already registered.
559
+ * @returns {boolean} Whether a fix was applied
560
+ */
561
+ function fixTuiRegistration() {
562
+ const { data, error } = readTuiJson();
563
+
564
+ // Corrupt JSON: backup old file, rebuild from scratch
565
+ if (error && data === undefined) {
566
+ const ts = Date.now();
567
+ const backupPath = `${TUI_JSON_PATH}.corrupt-${ts}.bak`;
568
+ try {
569
+ fs.copyFileSync(TUI_JSON_PATH, backupPath);
570
+ console.log(` ⚠ TUI config corrupted — backed up to ${backupPath}`);
571
+ } catch { /* non-critical */ }
572
+ // Rebuild fresh
573
+ const dir = path.dirname(TUI_JSON_PATH);
574
+ fs.mkdirSync(dir, { recursive: true });
575
+ const newConfig = { plugin: [TUI_PLUGIN_ENTRY] };
576
+ const tmpPath = `${TUI_JSON_PATH}.tmp`;
577
+ fs.writeFileSync(tmpPath, JSON.stringify(newConfig, null, 2));
578
+ fs.renameSync(tmpPath, TUI_JSON_PATH);
579
+ console.log(` ✓ Registered ${TUI_PLUGIN_ENTRY} in TUI (rebuilt after corrupt backup)`);
580
+ return true;
581
+ }
582
+
583
+ // File doesn't exist: create it
584
+ if (data === null) {
585
+ const dir = path.dirname(TUI_JSON_PATH);
586
+ fs.mkdirSync(dir, { recursive: true });
587
+ const newConfig = { plugin: [TUI_PLUGIN_ENTRY] };
588
+ const tmpPath = `${TUI_JSON_PATH}.tmp`;
589
+ fs.writeFileSync(tmpPath, JSON.stringify(newConfig, null, 2));
590
+ fs.renameSync(tmpPath, TUI_JSON_PATH);
591
+ console.log(` ✓ Created TUI config with ${TUI_PLUGIN_ENTRY}`);
592
+ return true;
593
+ }
594
+
595
+ // File exists, check if plugin already registered (idempotent)
596
+ const plugins = Array.isArray(data.plugin) ? data.plugin : [];
597
+ if (plugins.includes(TUI_PLUGIN_ENTRY)) return false;
598
+
599
+ // Append plugin entry
600
+ data.plugin = plugins.concat([TUI_PLUGIN_ENTRY]);
601
+ const tmpPath = `${TUI_JSON_PATH}.tmp`;
602
+ fs.writeFileSync(tmpPath, JSON.stringify(data, null, 2));
603
+ fs.renameSync(tmpPath, TUI_JSON_PATH);
604
+ console.log(` ✓ Added ${TUI_PLUGIN_ENTRY} to TUI config`);
605
+ return true;
606
+ }
607
+
608
+ /**
609
+ * Wrapper for init.js: ensure TUI is registered without console output.
610
+ * Returns true if the plugin is already registered or was just added.
611
+ */
612
+ function ensureTuiRegistration() {
613
+ const { data } = readTuiJson();
614
+ if (data === null || data === undefined) {
615
+ fixTuiRegistration();
616
+ return;
617
+ }
618
+ const plugins = Array.isArray(data.plugin) ? data.plugin : [];
619
+ if (!plugins.includes(TUI_PLUGIN_ENTRY)) {
620
+ fixTuiRegistration();
621
+ }
622
+ }
623
+
502
624
  async function doctor(args) {
503
625
  const fixMode = args.includes('--fix');
504
626
 
@@ -551,4 +673,8 @@ module.exports = {
551
673
  fixMissingHooks,
552
674
  fixCoreHooksPath,
553
675
  fixMissingAdapters,
676
+ diagnoseTuiRegistration,
677
+ fixTuiRegistration,
678
+ ensureTuiRegistration,
679
+ readTuiJson,
554
680
  };
package/lib/init.js CHANGED
@@ -340,6 +340,12 @@ async function installLocal(args) {
340
340
  injectKarpathyPrinciples(projectRoot);
341
341
  configureOpenCodePlugin(srcDir, projectRoot);
342
342
 
343
+ // Auto-register TUI plugin globally (idempotent)
344
+ try {
345
+ const { ensureTuiRegistration } = require('../lib/doctor.js');
346
+ ensureTuiRegistration();
347
+ } catch { /* non-critical: TUI registration is best-effort */ }
348
+
343
349
  console.log('\nInstallation complete!');
344
350
  console.log('Run git commit to trigger quality gates');
345
351
  return 0;
@@ -390,6 +396,12 @@ async function setupGlobal(args) {
390
396
  const manifest = generateGlobalManifest(srcDir);
391
397
  updateConfig({ lastInit: new Date().toISOString(), mode: 'global', templateDir: TEMPLATE_DIR, manifest });
392
398
 
399
+ // Auto-register TUI plugin globally (idempotent)
400
+ try {
401
+ const { ensureTuiRegistration } = require('../lib/doctor.js');
402
+ ensureTuiRegistration();
403
+ } catch { /* non-critical: TUI registration is best-effort */ }
404
+
393
405
  console.log('\nGlobal setup complete!');
394
406
  console.log('All git repositories will now use xp-gate quality gates.');
395
407
  console.log('Per-project adapters can still override by creating <repo>/githooks/');
@@ -1,9 +1,9 @@
1
1
  # SRC/MOCK-POLICY KNOWLEDGE BASE
2
2
 
3
3
  **Generated:** 2026-06-24
4
- **Commit:** f8338d6
4
+ **Commit:** 5019773
5
5
  **Branch:** main
6
- **Version:** 0.10.12.0
6
+ **Version:** 0.10.13.0
7
7
 
8
8
  ## OVERVIEW
9
9
  Mock layering policy enforcement — Gate M3 of pre-push hook. Ensures integration tests use real implementations for internal dependencies, mock external dependencies, and annotate pending mocks with removal plans. Combines project scope scanning, mock decision engine, and per-file validation into a single pipeline.
@@ -1,9 +1,9 @@
1
1
  # SRC/MUTATION KNOWLEDGE BASE
2
2
 
3
3
  **Generated:** 2026-06-24
4
- **Commit:** f8338d6
4
+ **Commit:** 5019773
5
5
  **Branch:** main
6
- **Version:** 0.10.12.0
6
+ **Version:** 0.10.13.0
7
7
 
8
8
  ## OVERVIEW
9
9
  **Gate M** (incremental mutation testing) + **Gate M2** helpers (test-layer detection used by `src/mock-policy/`). Pre-push quality gate. TypeScript-only; uses Stryker.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@boyingliu01/xp-gate",
3
- "version": "0.10.12",
3
+ "version": "0.10.13",
4
4
  "description": "AI-driven development workflow: 6 quality gates + Delphi review + Sprint Flow",
5
5
  "bin": {
6
6
  "xp-gate": "./bin/xp-gate.js"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xp-gate",
3
- "version": "0.10.12",
3
+ "version": "0.10.13",
4
4
  "displayName": "XP-Gate",
5
5
  "description": "Extreme Programming quality gates + AI workflow skills for Claude Code. Includes 10 quality gates (Gate 0-9), Sprint Flow (11 phases), and Delphi multi-expert review (>=90% consensus).",
6
6
  "author": {
@@ -1,9 +1,9 @@
1
1
  # SKILLS/DELPHI-REVIEW KNOWLEDGE BASE
2
2
 
3
3
  **Generated:** 2026-06-24
4
- **Commit:** f8338d6
4
+ **Commit:** 5019773
5
5
  **Branch:** main
6
- **Version:** 0.10.12.0
6
+ **Version:** 0.10.13.0
7
7
 
8
8
  ## OVERVIEW
9
9
  Delphi Consensus Review — multi-round anonymous expert review (≥90% threshold, 3 experts from ≥2 providers, domestic models only). Supports design + code-walkthrough modes.
@@ -1,9 +1,9 @@
1
1
  # SKILLS/SPRINT-FLOW KNOWLEDGE BASE
2
2
 
3
3
  **Generated:** 2026-06-24
4
- **Commit:** f8338d6
4
+ **Commit:** 5019773
5
5
  **Branch:** main
6
- **Version:** 0.10.12.0
6
+ **Version:** 0.10.13.0
7
7
 
8
8
  ## OVERVIEW
9
9
  **11-phase** development pipeline: ISOLATE → AUTO-ESTIMATE → THINK → PLAN → BUILD → REVIEW → USER ACCEPTANCE → FEEDBACK → SHIP → LAND → CLEANUP. Phase 2 default build mode is **ralph-loop** (REQ-level iteration, 40-67% token savings vs parallel). HARD-GATE in Phase 1: design must pass Delphi review (≥90% consensus) before any coding.
@@ -1,9 +1,9 @@
1
1
  # SKILLS/TEST-SPECIFICATION-ALIGNMENT KNOWLEDGE BASE
2
2
 
3
3
  **Generated:** 2026-06-24
4
- **Commit:** f8338d6
4
+ **Commit:** 5019773
5
5
  **Branch:** main
6
- **Version:** 0.10.12.0
6
+ **Version:** 0.10.13.0
7
7
 
8
8
  ## OVERVIEW
9
9
  Test-Specification Alignment Engine — two-stage validation ensuring tests accurately reflect requirements and design specs.
@@ -13,7 +13,6 @@ import { describe, it, before, after } from "node:test"
13
13
  import assert from "node:assert/strict"
14
14
  import { randomUUID } from "node:crypto"
15
15
  import { mkdirSync, writeFileSync, rmSync } from "node:fs"
16
- import { join } from "node:path"
17
16
  import { tmpdir } from "node:os"
18
17
 
19
18
  // Import the functions under test
@@ -421,3 +420,124 @@ void describe("multi-sprint rendering", () => {
421
420
  assert.ok(output.includes("Minimal sprint"))
422
421
  })
423
422
  })
423
+
424
+ // ── Early-phase placeholder rendering ──
425
+
426
+ /**
427
+ * Renders panel content with early-phase placeholder fallback.
428
+ * Mirrors the planned renderContent(dir, sprints, upgradeNotice) from tui-plugin.ts.
429
+ * Expected to be the actual implementation after TDD cycle.
430
+ */
431
+ import { existsSync } from "node:fs"
432
+ import { dirname, resolve, parse, join } from "node:path"
433
+
434
+ function findGitRoot(startDir: string): string | null {
435
+ let current = resolve(startDir);
436
+ const root = parse(current).root;
437
+ const seen = new Set<string>();
438
+ while (current !== root) {
439
+ if (seen.has(current)) break;
440
+ seen.add(current);
441
+ if (existsSync(join(current, '.git'))) return current;
442
+ const parent = dirname(current);
443
+ if (parent === current) break;
444
+ current = parent;
445
+ }
446
+ if (existsSync(join(root, '.git'))) return root;
447
+ return null;
448
+ }
449
+
450
+ function renderContentWithPlaceholder(
451
+ dir: string,
452
+ sprints: { state: Record<string, unknown>; sourcePath: string; worktreeExists: boolean }[],
453
+ upgradeNotice: string | null,
454
+ ): string | null {
455
+ if (sprints.length > 0) {
456
+ return upgradeNotice || null
457
+ }
458
+
459
+ const hasStateDir = existsSync(join(dir, ".sprint-state"))
460
+ const gitRoot = findGitRoot(dir)
461
+ const hasWorktreesRoot = gitRoot ? existsSync(join(gitRoot, ".worktrees")) : false
462
+
463
+ if (hasStateDir) {
464
+ const placeholder = "SPRINT FLOW\n → 初始化中..."
465
+ return [upgradeNotice, placeholder].filter(Boolean).join("\n---\n")
466
+ }
467
+
468
+ if (hasWorktreesRoot) {
469
+ const placeholder = "SPRINT FLOW\n · 准备中..."
470
+ return [upgradeNotice, placeholder].filter(Boolean).join("\n---\n")
471
+ }
472
+
473
+ return upgradeNotice || null
474
+ }
475
+
476
+ void describe("early-phase placeholder rendering", () => {
477
+ const tmpDir = join(tmpdir(), "xp-gate-tui-placeholder-" + randomUUID())
478
+
479
+ before(() => {
480
+ mkdirSync(tmpDir, { recursive: true })
481
+ })
482
+
483
+ after(() => {
484
+ rmSync(tmpDir, { recursive: true, force: true })
485
+ })
486
+
487
+ void it("returns null when no sprints and no .sprint-state/ directory and no .worktrees/", () => {
488
+ const result = renderContentWithPlaceholder(tmpDir, [], null)
489
+ assert.equal(result, null, "Should return null when nothing exists")
490
+ })
491
+
492
+ void it("returns '初始化中...' placeholder when .sprint-state/ directory exists but no sprint data", () => {
493
+ mkdirSync(join(tmpDir, ".sprint-state"), { recursive: true })
494
+ const result = renderContentWithPlaceholder(tmpDir, [], null)
495
+ assert.ok(result !== null, "Should return a placeholder string")
496
+ assert.ok(result!.includes("SPRINT FLOW"), "Should include SPRINT FLOW header")
497
+ assert.ok(result!.includes("初始化中"), "Should include 初始化中...")
498
+ })
499
+
500
+ void it("returns '准备中...' placeholder when .worktrees/ directory exists but no sprint data", () => {
501
+ // Must create .git/ dir for findGitRoot() to succeed
502
+ // Note: test 2 (above) may have created .sprint-state/ — clean it first
503
+ rmSync(join(tmpDir, ".sprint-state"), { recursive: true, force: true })
504
+ mkdirSync(join(tmpDir, ".git"), { recursive: true })
505
+ mkdirSync(join(tmpDir, ".worktrees"), { recursive: true })
506
+
507
+ const result = renderContentWithPlaceholder(tmpDir, [], null)
508
+ assert.ok(result !== null, "Should return a placeholder string")
509
+ assert.ok(result!.includes("SPRINT FLOW"), "Should include SPRINT FLOW header")
510
+ assert.ok(result!.includes("准备中"), "Should include 准备中...")
511
+ })
512
+
513
+ void it("prefers .sprint-state/ placeholder over .worktrees/ when both exist", () => {
514
+ mkdirSync(join(tmpDir, ".sprint-state"), { recursive: true })
515
+ const result = renderContentWithPlaceholder(tmpDir, [], null)
516
+ assert.ok(result!.includes("初始化中"), "Should prefer 初始化中 when .sprint-state/ exists")
517
+ assert.ok(!result!.includes("准备中"), "Should NOT show 准备中 when .sprint-state/ exists")
518
+ })
519
+
520
+ void it("includes upgrade notice banner above placeholder when both present", () => {
521
+ const notice = "✓ Auto-upgraded from v0.10.12 to v0.10.13"
522
+ const result = renderContentWithPlaceholder(tmpDir, [], notice)
523
+ assert.ok(result !== null)
524
+ assert.ok(result!.includes("Auto-upgraded"), "Should include upgrade notice")
525
+ assert.ok(result!.includes("SPRINT FLOW"), "Should include placeholder below notice")
526
+ // Upgrade notice should appear before SPRINT FLOW
527
+ const noticePos = result!.indexOf("Auto-upgraded")
528
+ const sprintPos = result!.indexOf("SPRINT FLOW")
529
+ assert.ok(noticePos < sprintPos, "Upgrade notice should come before SPRINT FLOW")
530
+ })
531
+
532
+ void it("shows upgrade notice only when no sprints and no placeholder dirs", () => {
533
+ // Reset — remove .sprint-state/ and .worktrees/
534
+ rmSync(join(tmpDir, ".sprint-state"), { recursive: true, force: true })
535
+ rmSync(join(tmpDir, ".worktrees"), { recursive: true, force: true })
536
+
537
+ const notice = "↑ New version v1.0.0 available"
538
+ const result = renderContentWithPlaceholder(tmpDir, [], notice)
539
+ assert.ok(result !== null, "Should return upgrade notice even without sprint dirs")
540
+ assert.ok(result!.includes("New version"), "Should include upgrade notice text")
541
+ assert.ok(!result!.includes("SPRINT FLOW"), "Should NOT show placeholder when nothing exists")
542
+ })
543
+ })
@@ -322,6 +322,8 @@ export const XpGatePlugin = async (input: OpenCodePluginInput) => {
322
322
  if (!checked) {
323
323
  checked = true
324
324
  const msg = await runBackgroundUpdates(directory).catch(() => null)
325
+ // Primary notification: upgrade-notice.json → TUI sidebar banner
326
+ // Fallback: stderr for users without TUI panel registered
325
327
  if (msg) process.stderr.write(`${msg}\n`)
326
328
  }
327
329
  },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@boyingliu01/opencode-plugin",
3
- "version": "0.10.12",
3
+ "version": "0.10.13",
4
4
  "type": "module",
5
5
  "main": "index.ts",
6
6
  "description": "XP-Gate quality gates + AI workflow skills + Sprint Flow TUI sidebar for OpenCode",
@@ -1,9 +1,9 @@
1
1
  # SKILLS/DELPHI-REVIEW KNOWLEDGE BASE
2
2
 
3
3
  **Generated:** 2026-06-24
4
- **Commit:** f8338d6
4
+ **Commit:** 5019773
5
5
  **Branch:** main
6
- **Version:** 0.10.12.0
6
+ **Version:** 0.10.13.0
7
7
 
8
8
  ## OVERVIEW
9
9
  Delphi Consensus Review — multi-round anonymous expert review (≥90% threshold, 3 experts from ≥2 providers, domestic models only). Supports design + code-walkthrough modes.
@@ -1,9 +1,9 @@
1
1
  # SKILLS/SPRINT-FLOW KNOWLEDGE BASE
2
2
 
3
3
  **Generated:** 2026-06-24
4
- **Commit:** f8338d6
4
+ **Commit:** 5019773
5
5
  **Branch:** main
6
- **Version:** 0.10.12.0
6
+ **Version:** 0.10.13.0
7
7
 
8
8
  ## OVERVIEW
9
9
  **11-phase** development pipeline: ISOLATE → AUTO-ESTIMATE → THINK → PLAN → BUILD → REVIEW → USER ACCEPTANCE → FEEDBACK → SHIP → LAND → CLEANUP. Phase 2 default build mode is **ralph-loop** (REQ-level iteration, 40-67% token savings vs parallel). HARD-GATE in Phase 1: design must pass Delphi review (≥90% consensus) before any coding.
@@ -1,9 +1,9 @@
1
1
  # SKILLS/TEST-SPECIFICATION-ALIGNMENT KNOWLEDGE BASE
2
2
 
3
3
  **Generated:** 2026-06-24
4
- **Commit:** f8338d6
4
+ **Commit:** 5019773
5
5
  **Branch:** main
6
- **Version:** 0.10.12.0
6
+ **Version:** 0.10.13.0
7
7
 
8
8
  ## OVERVIEW
9
9
  Test-Specification Alignment Engine — two-stage validation ensuring tests accurately reflect requirements and design specs.
@@ -329,11 +329,29 @@ function renderMultiSprintSidebar(sprints: DiscoveredSprint[]): string | null {
329
329
  return blocks.join("\n---\n");
330
330
  }
331
331
 
332
- function renderContent(sprints: DiscoveredSprint[], upgradeNotice: string | null): string | null {
332
+ function renderContent(sprints: DiscoveredSprint[], upgradeNotice: string | null, dir: string): string | null {
333
333
  const sprintContent = renderMultiSprintSidebar(sprints)
334
- if (upgradeNotice && sprintContent) return `${upgradeNotice}\n---\n${sprintContent}`
335
- if (upgradeNotice) return upgradeNotice
336
- return sprintContent
334
+
335
+ if (sprintContent) {
336
+ return [upgradeNotice, sprintContent].filter(Boolean).join("\n---\n")
337
+ }
338
+
339
+ // Early-phase placeholders: when no sprint data yet, check for directory hints
340
+ const hasStateDir = existsSync(join(dir, ".sprint-state"))
341
+ const gitRoot = findGitRoot(dir)
342
+ const hasWorktreesRoot = gitRoot ? existsSync(join(gitRoot, ".worktrees")) : false
343
+
344
+ if (hasStateDir) {
345
+ const placeholder = "SPRINT FLOW\n → 初始化中..."
346
+ return [upgradeNotice, placeholder].filter(Boolean).join("\n---\n")
347
+ }
348
+
349
+ if (hasWorktreesRoot) {
350
+ const placeholder = "SPRINT FLOW\n · 准备中..."
351
+ return [upgradeNotice, placeholder].filter(Boolean).join("\n---\n")
352
+ }
353
+
354
+ return upgradeNotice || null
337
355
  }
338
356
 
339
357
  // ── Upgrade notice ──
@@ -376,15 +394,14 @@ const tuiPlugin: TuiSlotPlugin = {
376
394
  const dir = process.env.XP_GATE_PROJECT_DIR || process.cwd();
377
395
  const now = Date.now();
378
396
 
379
- // Use cache if still valid for current directory
380
397
  if (_cache && _cache.dir === dir && now - _cache.ts < CACHE_TTL_MS) {
381
- return renderContent(_cache.data, _cache.upgradeNotice);
398
+ return renderContent(_cache.data, _cache.upgradeNotice, dir);
382
399
  }
383
400
 
384
401
  const sprints = discoverActiveSprints(dir);
385
402
  const upgradeNotice = renderUpgradeNotice()
386
403
  _cache = { data: sprints, ts: now, dir, upgradeNotice };
387
- return renderContent(sprints, upgradeNotice);
404
+ return renderContent(sprints, upgradeNotice, dir);
388
405
  },
389
406
  },
390
407
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xp-gate",
3
- "version": "0.10.12",
3
+ "version": "0.10.13",
4
4
  "displayName": "XP-Gate",
5
5
  "description": "Extreme Programming quality gates + AI workflow skills for Qoder. Includes 10 quality gates (Gate 0-9), Sprint Flow (11 phases), and Delphi multi-expert review (>=90% consensus).",
6
6
  "author": {
@@ -1,9 +1,9 @@
1
1
  # SKILLS/DELPHI-REVIEW KNOWLEDGE BASE
2
2
 
3
3
  **Generated:** 2026-06-24
4
- **Commit:** f8338d6
4
+ **Commit:** 5019773
5
5
  **Branch:** main
6
- **Version:** 0.10.12.0
6
+ **Version:** 0.10.13.0
7
7
 
8
8
  ## OVERVIEW
9
9
  Delphi Consensus Review — multi-round anonymous expert review (≥90% threshold, 3 experts from ≥2 providers, domestic models only). Supports design + code-walkthrough modes.
@@ -1,9 +1,9 @@
1
1
  # SKILLS/SPRINT-FLOW KNOWLEDGE BASE
2
2
 
3
3
  **Generated:** 2026-06-24
4
- **Commit:** f8338d6
4
+ **Commit:** 5019773
5
5
  **Branch:** main
6
- **Version:** 0.10.12.0
6
+ **Version:** 0.10.13.0
7
7
 
8
8
  ## OVERVIEW
9
9
  **11-phase** development pipeline: ISOLATE → AUTO-ESTIMATE → THINK → PLAN → BUILD → REVIEW → USER ACCEPTANCE → FEEDBACK → SHIP → LAND → CLEANUP. Phase 2 default build mode is **ralph-loop** (REQ-level iteration, 40-67% token savings vs parallel). HARD-GATE in Phase 1: design must pass Delphi review (≥90% consensus) before any coding.
@@ -1,9 +1,9 @@
1
1
  # SKILLS/TEST-SPECIFICATION-ALIGNMENT KNOWLEDGE BASE
2
2
 
3
3
  **Generated:** 2026-06-24
4
- **Commit:** f8338d6
4
+ **Commit:** 5019773
5
5
  **Branch:** main
6
- **Version:** 0.10.12.0
6
+ **Version:** 0.10.13.0
7
7
 
8
8
  ## OVERVIEW
9
9
  Test-Specification Alignment Engine — two-stage validation ensuring tests accurately reflect requirements and design specs.
@@ -1,9 +1,9 @@
1
1
  # PRINCIPLES CHECKER MODULE
2
2
 
3
3
  **Generated:** 2026-06-24
4
- **Commit:** f8338d6
4
+ **Commit:** 5019773
5
5
  **Branch:** main
6
- **Version:** 0.10.12.0
6
+ **Version:** 0.10.13.0
7
7
 
8
8
  ## OVERVIEW
9
9
  Clean Code & SOLID principles checker — **Gate 4** of pre-commit. 14 rules × 9 language adapters, SARIF 2.1.0 output. Houses the **Boy Scout Rule** enforcement engine (Gate 6) and warning-baseline storage.
@@ -1,9 +1,9 @@
1
1
  # SKILLS/DELPHI-REVIEW KNOWLEDGE BASE
2
2
 
3
3
  **Generated:** 2026-06-24
4
- **Commit:** f8338d6
4
+ **Commit:** 5019773
5
5
  **Branch:** main
6
- **Version:** 0.10.12.0
6
+ **Version:** 0.10.13.0
7
7
 
8
8
  ## OVERVIEW
9
9
  Delphi Consensus Review — multi-round anonymous expert review (≥90% threshold, 3 experts from ≥2 providers, domestic models only). Supports design + code-walkthrough modes.
@@ -1,9 +1,9 @@
1
1
  # SKILLS/SPRINT-FLOW KNOWLEDGE BASE
2
2
 
3
3
  **Generated:** 2026-06-24
4
- **Commit:** f8338d6
4
+ **Commit:** 5019773
5
5
  **Branch:** main
6
- **Version:** 0.10.12.0
6
+ **Version:** 0.10.13.0
7
7
 
8
8
  ## OVERVIEW
9
9
  **11-phase** development pipeline: ISOLATE → AUTO-ESTIMATE → THINK → PLAN → BUILD → REVIEW → USER ACCEPTANCE → FEEDBACK → SHIP → LAND → CLEANUP. Phase 2 default build mode is **ralph-loop** (REQ-level iteration, 40-67% token savings vs parallel). HARD-GATE in Phase 1: design must pass Delphi review (≥90% consensus) before any coding.
@@ -1,9 +1,9 @@
1
1
  # SKILLS/TEST-SPECIFICATION-ALIGNMENT KNOWLEDGE BASE
2
2
 
3
3
  **Generated:** 2026-06-24
4
- **Commit:** f8338d6
4
+ **Commit:** 5019773
5
5
  **Branch:** main
6
- **Version:** 0.10.12.0
6
+ **Version:** 0.10.13.0
7
7
 
8
8
  ## OVERVIEW
9
9
  Test-Specification Alignment Engine — two-stage validation ensuring tests accurately reflect requirements and design specs.