revspec 0.5.0 → 0.7.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.
Files changed (47) hide show
  1. package/README.md +84 -67
  2. package/bin/revspec.ts +4 -38
  3. package/package.json +20 -3
  4. package/skills/revspec/SKILL.md +38 -31
  5. package/src/cli/reply.ts +1 -1
  6. package/src/cli/watch.ts +69 -41
  7. package/src/protocol/live-events.ts +6 -16
  8. package/src/state/review-state.ts +37 -24
  9. package/src/tui/app.ts +168 -107
  10. package/src/tui/comment-input.ts +21 -14
  11. package/src/tui/confirm.ts +4 -6
  12. package/src/tui/help.ts +77 -20
  13. package/src/tui/pager.ts +4 -2
  14. package/src/tui/search.ts +9 -4
  15. package/src/tui/spinner.ts +81 -0
  16. package/src/tui/status-bar.ts +9 -8
  17. package/src/tui/thread-list.ts +62 -22
  18. package/src/tui/ui/keymap.ts +55 -0
  19. package/.github/workflows/ci.yml +0 -18
  20. package/CLAUDE.md +0 -27
  21. package/bun.lock +0 -213
  22. package/docs/superpowers/plans/2026-03-14-live-ai-integration.md +0 -1877
  23. package/docs/superpowers/plans/2026-03-14-spectral-v1-implementation.md +0 -2139
  24. package/docs/superpowers/plans/2026-03-15-ui-refactor.md +0 -1025
  25. package/docs/superpowers/specs/2026-03-14-live-ai-integration-design.md +0 -518
  26. package/docs/superpowers/specs/2026-03-14-live-ai-integration-design.review.json +0 -65
  27. package/docs/superpowers/specs/2026-03-14-spec-review-tool-design.md +0 -331
  28. package/docs/superpowers/specs/2026-03-14-spec-review-tool-design.review.json +0 -141
  29. package/docs/superpowers/specs/claude-code-integration-notes.md +0 -26
  30. package/scripts/install-skill.sh +0 -20
  31. package/scripts/release.sh +0 -52
  32. package/test/cli-reply.test.ts +0 -140
  33. package/test/cli-watch.test.ts +0 -216
  34. package/test/cli.test.ts +0 -160
  35. package/test/e2e-live.test.ts +0 -171
  36. package/test/live-interaction.test.ts +0 -398
  37. package/test/opentui-smoke.test.ts +0 -12
  38. package/test/protocol/live-events.test.ts +0 -509
  39. package/test/protocol/live-merge.test.ts +0 -167
  40. package/test/protocol/merge.test.ts +0 -100
  41. package/test/protocol/read.test.ts +0 -92
  42. package/test/protocol/types.test.ts +0 -95
  43. package/test/protocol/write.test.ts +0 -72
  44. package/test/state/review-state.test.ts +0 -399
  45. package/test/tui/pager.test.ts +0 -159
  46. package/test/tui/ui/keybinds.test.ts +0 -71
  47. package/tsconfig.json +0 -14
@@ -1,100 +0,0 @@
1
- import { describe, expect, it } from "bun:test";
2
- import { mergeDraftIntoReview } from "../../src/protocol/merge";
3
- import type { ReviewFile, DraftFile, Thread } from "../../src/protocol/types";
4
-
5
- const baseReview: ReviewFile = {
6
- file: "spec.md",
7
- threads: [],
8
- };
9
-
10
- function makeThread(id: string, messages: string[] = ["hello"]): Thread {
11
- return {
12
- id,
13
- line: 1,
14
- status: "open",
15
- messages: messages.map((text) => ({ author: "reviewer", text })),
16
- };
17
- }
18
-
19
- describe("mergeDraftIntoReview", () => {
20
- it("adds new threads from draft to an empty review", () => {
21
- const draft: DraftFile = {
22
- threads: [makeThread("t1"), makeThread("t2")],
23
- };
24
- const result = mergeDraftIntoReview({ ...baseReview }, draft);
25
- expect(result.threads).toHaveLength(2);
26
- expect(result.threads.map((t) => t.id)).toEqual(["t1", "t2"]);
27
- });
28
-
29
- it("appends only new messages to an existing thread (draft has full history)", () => {
30
- const existingThread: Thread = makeThread("t1", ["first message"]);
31
- const review: ReviewFile = { file: "spec.md", threads: [existingThread] };
32
-
33
- // Draft contains full history (existing + new)
34
- const draftThread: Thread = {
35
- ...existingThread,
36
- messages: [
37
- { author: "reviewer", text: "first message" },
38
- { author: "owner", text: "second message" },
39
- ],
40
- };
41
- const draft: DraftFile = { threads: [draftThread] };
42
-
43
- const result = mergeDraftIntoReview(review, draft);
44
- expect(result.threads).toHaveLength(1);
45
- expect(result.threads[0].messages).toHaveLength(2);
46
- expect(result.threads[0].messages[1].text).toBe("second message");
47
- });
48
-
49
- it("handles a mix of new and existing threads", () => {
50
- const existing: Thread = makeThread("t1", ["msg1"]);
51
- const review: ReviewFile = { file: "spec.md", threads: [existing] };
52
-
53
- const draftExisting: Thread = {
54
- ...existing,
55
- messages: [
56
- { author: "reviewer", text: "msg1" },
57
- { author: "owner", text: "reply" },
58
- ],
59
- };
60
- const draftNew: Thread = makeThread("t2", ["new thread"]);
61
- const draft: DraftFile = { threads: [draftExisting, draftNew] };
62
-
63
- const result = mergeDraftIntoReview(review, draft);
64
- expect(result.threads).toHaveLength(2);
65
- expect(result.threads[0].messages).toHaveLength(2);
66
- expect(result.threads[1].id).toBe("t2");
67
- });
68
-
69
- it("updates thread status from draft", () => {
70
- const existing: Thread = { ...makeThread("t1"), status: "open" };
71
- const review: ReviewFile = { file: "spec.md", threads: [existing] };
72
-
73
- const draftThread: Thread = {
74
- ...existing,
75
- status: "resolved",
76
- messages: existing.messages,
77
- };
78
- const draft: DraftFile = { threads: [draftThread] };
79
-
80
- const result = mergeDraftIntoReview(review, draft);
81
- expect(result.threads[0].status).toBe("resolved");
82
- });
83
-
84
- it("returns review unchanged when draft has no threads", () => {
85
- const thread: Thread = makeThread("t1");
86
- const review: ReviewFile = { file: "spec.md", threads: [thread] };
87
- const draft: DraftFile = { approved: true };
88
-
89
- const result = mergeDraftIntoReview(review, draft);
90
- expect(result.threads).toHaveLength(1);
91
- expect(result.threads[0].id).toBe("t1");
92
- });
93
-
94
- it("creates a new ReviewFile with specFile when review is null", () => {
95
- const draft: DraftFile = { threads: [makeThread("t1")] };
96
- const result = mergeDraftIntoReview(null, draft, "new-spec.md");
97
- expect(result.file).toBe("new-spec.md");
98
- expect(result.threads).toHaveLength(1);
99
- });
100
- });
@@ -1,92 +0,0 @@
1
- import { describe, expect, it, afterEach } from "bun:test";
2
- import { mkdtempSync, rmSync, writeFileSync } from "fs";
3
- import { join } from "path";
4
- import { readReviewFile, readDraftFile } from "../../src/protocol/read";
5
- import type { ReviewFile, DraftFile } from "../../src/protocol/types";
6
-
7
- function tmpDir() {
8
- return mkdtempSync("/tmp/revspec-test-");
9
- }
10
-
11
- describe("readReviewFile", () => {
12
- it("returns null for a missing file", () => {
13
- expect(readReviewFile("/nonexistent/path/review.json")).toBeNull();
14
- });
15
-
16
- it("returns parsed ReviewFile for a valid file", () => {
17
- const dir = tmpDir();
18
- const filePath = join(dir, "review.json");
19
- const data: ReviewFile = {
20
- file: "spec.md",
21
- threads: [
22
- {
23
- id: "t1",
24
- line: 10,
25
- status: "open",
26
- messages: [{ author: "reviewer", text: "Looks good?" }],
27
- },
28
- ],
29
- };
30
- writeFileSync(filePath, JSON.stringify(data));
31
- const result = readReviewFile(filePath);
32
- expect(result).toEqual(data);
33
- rmSync(dir, { recursive: true });
34
- });
35
-
36
- it("returns null for invalid JSON", () => {
37
- const dir = tmpDir();
38
- const filePath = join(dir, "review.json");
39
- writeFileSync(filePath, "not valid json {{");
40
- expect(readReviewFile(filePath)).toBeNull();
41
- rmSync(dir, { recursive: true });
42
- });
43
-
44
- it("returns null when JSON doesn't match ReviewFile schema", () => {
45
- const dir = tmpDir();
46
- const filePath = join(dir, "review.json");
47
- writeFileSync(filePath, JSON.stringify({ threads: [] })); // missing 'file'
48
- expect(readReviewFile(filePath)).toBeNull();
49
- rmSync(dir, { recursive: true });
50
- });
51
- });
52
-
53
- describe("readDraftFile", () => {
54
- it("returns null for a missing file", () => {
55
- expect(readDraftFile("/nonexistent/path/draft.json")).toBeNull();
56
- });
57
-
58
- it("returns parsed DraftFile for an approval draft", () => {
59
- const dir = tmpDir();
60
- const filePath = join(dir, "draft.json");
61
- const data: DraftFile = { approved: true };
62
- writeFileSync(filePath, JSON.stringify(data));
63
- expect(readDraftFile(filePath)).toEqual(data);
64
- rmSync(dir, { recursive: true });
65
- });
66
-
67
- it("returns parsed DraftFile for a threads draft", () => {
68
- const dir = tmpDir();
69
- const filePath = join(dir, "draft.json");
70
- const data: DraftFile = {
71
- threads: [
72
- {
73
- id: "t2",
74
- line: 5,
75
- status: "pending",
76
- messages: [{ author: "owner", text: "Response here" }],
77
- },
78
- ],
79
- };
80
- writeFileSync(filePath, JSON.stringify(data));
81
- expect(readDraftFile(filePath)).toEqual(data);
82
- rmSync(dir, { recursive: true });
83
- });
84
-
85
- it("returns null for corrupted JSON", () => {
86
- const dir = tmpDir();
87
- const filePath = join(dir, "draft.json");
88
- writeFileSync(filePath, "{bad json");
89
- expect(readDraftFile(filePath)).toBeNull();
90
- rmSync(dir, { recursive: true });
91
- });
92
- });
@@ -1,95 +0,0 @@
1
- import { describe, expect, it } from "bun:test";
2
- import {
3
- isValidStatus,
4
- isValidThread,
5
- isValidReviewFile,
6
- } from "../../src/protocol/types";
7
-
8
- describe("isValidStatus", () => {
9
- it("accepts open", () => expect(isValidStatus("open")).toBe(true));
10
- it("accepts pending", () => expect(isValidStatus("pending")).toBe(true));
11
- it("accepts resolved", () => expect(isValidStatus("resolved")).toBe(true));
12
- it("accepts outdated", () => expect(isValidStatus("outdated")).toBe(true));
13
- it("rejects addressed", () => expect(isValidStatus("addressed")).toBe(false));
14
- it("rejects empty string", () => expect(isValidStatus("")).toBe(false));
15
- it("rejects OPEN (uppercase)", () => expect(isValidStatus("OPEN")).toBe(false));
16
- });
17
-
18
- describe("isValidThread", () => {
19
- it("accepts a minimal valid thread", () => {
20
- expect(
21
- isValidThread({
22
- id: "t1",
23
- line: 5,
24
- status: "open",
25
- messages: [{ author: "reviewer", text: "hello" }],
26
- })
27
- ).toBe(true);
28
- });
29
-
30
- it("rejects thread missing line", () => {
31
- expect(
32
- isValidThread({
33
- id: "t1",
34
- status: "open",
35
- messages: [],
36
- })
37
- ).toBe(false);
38
- });
39
-
40
- it("rejects thread missing messages", () => {
41
- expect(
42
- isValidThread({
43
- id: "t1",
44
- line: 3,
45
- status: "open",
46
- })
47
- ).toBe(false);
48
- });
49
-
50
- it("rejects thread with invalid status", () => {
51
- expect(
52
- isValidThread({
53
- id: "t1",
54
- line: 3,
55
- status: "addressed",
56
- messages: [],
57
- })
58
- ).toBe(false);
59
- });
60
- });
61
-
62
- describe("isValidReviewFile", () => {
63
- it("accepts a valid review file", () => {
64
- expect(
65
- isValidReviewFile({
66
- file: "spec.md",
67
- threads: [
68
- {
69
- id: "t1",
70
- line: 1,
71
- status: "open",
72
- messages: [{ author: "reviewer", text: "comment" }],
73
- },
74
- ],
75
- })
76
- ).toBe(true);
77
- });
78
-
79
- it("accepts a review file with empty threads array", () => {
80
- expect(
81
- isValidReviewFile({
82
- file: "spec.md",
83
- threads: [],
84
- })
85
- ).toBe(true);
86
- });
87
-
88
- it("rejects a review file missing the file field", () => {
89
- expect(
90
- isValidReviewFile({
91
- threads: [],
92
- })
93
- ).toBe(false);
94
- });
95
- });
@@ -1,72 +0,0 @@
1
- import { describe, expect, it } from "bun:test";
2
- import { mkdtempSync, rmSync, readFileSync } from "fs";
3
- import { join } from "path";
4
- import { writeReviewFile, writeDraftFile } from "../../src/protocol/write";
5
- import type { ReviewFile, DraftFile } from "../../src/protocol/types";
6
-
7
- function tmpDir() {
8
- return mkdtempSync("/tmp/revspec-test-");
9
- }
10
-
11
- describe("writeReviewFile", () => {
12
- it("writes valid JSON that can be parsed back to ReviewFile", () => {
13
- const dir = tmpDir();
14
- const filePath = join(dir, "review.json");
15
- const data: ReviewFile = {
16
- file: "spec.md",
17
- threads: [
18
- {
19
- id: "t1",
20
- line: 3,
21
- status: "open",
22
- messages: [{ author: "reviewer", text: "Is this right?" }],
23
- },
24
- ],
25
- };
26
- writeReviewFile(filePath, data);
27
- const raw = readFileSync(filePath, "utf8");
28
- expect(JSON.parse(raw)).toEqual(data);
29
- rmSync(dir, { recursive: true });
30
- });
31
-
32
- it("writes pretty-printed JSON (contains newlines)", () => {
33
- const dir = tmpDir();
34
- const filePath = join(dir, "review.json");
35
- const data: ReviewFile = { file: "spec.md", threads: [] };
36
- writeReviewFile(filePath, data);
37
- const raw = readFileSync(filePath, "utf8");
38
- expect(raw).toContain("\n");
39
- rmSync(dir, { recursive: true });
40
- });
41
- });
42
-
43
- describe("writeDraftFile", () => {
44
- it("writes an approval draft", () => {
45
- const dir = tmpDir();
46
- const filePath = join(dir, "draft.json");
47
- const data: DraftFile = { approved: true };
48
- writeDraftFile(filePath, data);
49
- const raw = readFileSync(filePath, "utf8");
50
- expect(JSON.parse(raw)).toEqual(data);
51
- rmSync(dir, { recursive: true });
52
- });
53
-
54
- it("writes a threads draft", () => {
55
- const dir = tmpDir();
56
- const filePath = join(dir, "draft.json");
57
- const data: DraftFile = {
58
- threads: [
59
- {
60
- id: "t2",
61
- line: 7,
62
- status: "resolved",
63
- messages: [{ author: "owner", text: "Fixed." }],
64
- },
65
- ],
66
- };
67
- writeDraftFile(filePath, data);
68
- const raw = readFileSync(filePath, "utf8");
69
- expect(JSON.parse(raw)).toEqual(data);
70
- rmSync(dir, { recursive: true });
71
- });
72
- });