chief-clancy 0.5.6 → 0.5.8

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 (78) hide show
  1. package/README.md +1 -1
  2. package/dist/bundle/clancy-once.js +72 -66
  3. package/dist/schemas/bitbucket-pr.d.ts +15 -0
  4. package/dist/schemas/bitbucket-pr.d.ts.map +1 -1
  5. package/dist/schemas/bitbucket-pr.js +6 -0
  6. package/dist/schemas/bitbucket-pr.js.map +1 -1
  7. package/dist/schemas/github.d.ts +12 -0
  8. package/dist/schemas/github.d.ts.map +1 -1
  9. package/dist/schemas/github.js +2 -0
  10. package/dist/schemas/github.js.map +1 -1
  11. package/dist/schemas/gitlab-mr.d.ts +11 -0
  12. package/dist/schemas/gitlab-mr.d.ts.map +1 -1
  13. package/dist/schemas/gitlab-mr.js +2 -0
  14. package/dist/schemas/gitlab-mr.js.map +1 -1
  15. package/dist/scripts/board/github/github.d.ts +1 -1
  16. package/dist/scripts/board/github/github.d.ts.map +1 -1
  17. package/dist/scripts/board/github/github.js +2 -2
  18. package/dist/scripts/board/github/github.js.map +1 -1
  19. package/dist/scripts/once/board-ops.d.ts +11 -0
  20. package/dist/scripts/once/board-ops.d.ts.map +1 -0
  21. package/dist/scripts/once/board-ops.js +81 -0
  22. package/dist/scripts/once/board-ops.js.map +1 -0
  23. package/dist/scripts/once/deliver.d.ts +13 -0
  24. package/dist/scripts/once/deliver.d.ts.map +1 -0
  25. package/dist/scripts/once/deliver.js +134 -0
  26. package/dist/scripts/once/deliver.js.map +1 -0
  27. package/dist/scripts/once/fetch-ticket.d.ts +4 -0
  28. package/dist/scripts/once/fetch-ticket.d.ts.map +1 -0
  29. package/dist/scripts/once/fetch-ticket.js +59 -0
  30. package/dist/scripts/once/fetch-ticket.js.map +1 -0
  31. package/dist/scripts/once/git-token.d.ts +13 -0
  32. package/dist/scripts/once/git-token.d.ts.map +1 -0
  33. package/dist/scripts/once/git-token.js +30 -0
  34. package/dist/scripts/once/git-token.js.map +1 -0
  35. package/dist/scripts/once/once.d.ts.map +1 -1
  36. package/dist/scripts/once/once.js +23 -421
  37. package/dist/scripts/once/once.js.map +1 -1
  38. package/dist/scripts/once/pr-creation.d.ts +11 -0
  39. package/dist/scripts/once/pr-creation.d.ts.map +1 -0
  40. package/dist/scripts/once/pr-creation.js +50 -0
  41. package/dist/scripts/once/pr-creation.js.map +1 -0
  42. package/dist/scripts/once/rework.d.ts +32 -0
  43. package/dist/scripts/once/rework.d.ts.map +1 -0
  44. package/dist/scripts/once/rework.js +208 -0
  45. package/dist/scripts/once/rework.js.map +1 -0
  46. package/dist/scripts/once/types.d.ts +13 -0
  47. package/dist/scripts/once/types.d.ts.map +1 -0
  48. package/dist/scripts/once/types.js +2 -0
  49. package/dist/scripts/once/types.js.map +1 -0
  50. package/dist/scripts/shared/git-ops/git-ops.d.ts +11 -0
  51. package/dist/scripts/shared/git-ops/git-ops.d.ts.map +1 -1
  52. package/dist/scripts/shared/git-ops/git-ops.js +26 -0
  53. package/dist/scripts/shared/git-ops/git-ops.js.map +1 -1
  54. package/dist/scripts/shared/preflight/preflight.d.ts.map +1 -1
  55. package/dist/scripts/shared/preflight/preflight.js +15 -3
  56. package/dist/scripts/shared/preflight/preflight.js.map +1 -1
  57. package/dist/scripts/shared/progress/progress.d.ts +2 -1
  58. package/dist/scripts/shared/progress/progress.d.ts.map +1 -1
  59. package/dist/scripts/shared/progress/progress.js +29 -13
  60. package/dist/scripts/shared/progress/progress.js.map +1 -1
  61. package/dist/scripts/shared/pull-request/bitbucket/bitbucket.d.ts +12 -0
  62. package/dist/scripts/shared/pull-request/bitbucket/bitbucket.d.ts.map +1 -1
  63. package/dist/scripts/shared/pull-request/bitbucket/bitbucket.js +51 -1
  64. package/dist/scripts/shared/pull-request/bitbucket/bitbucket.js.map +1 -1
  65. package/dist/scripts/shared/pull-request/github/github.d.ts +26 -2
  66. package/dist/scripts/shared/pull-request/github/github.d.ts.map +1 -1
  67. package/dist/scripts/shared/pull-request/github/github.js +102 -9
  68. package/dist/scripts/shared/pull-request/github/github.js.map +1 -1
  69. package/dist/scripts/shared/pull-request/gitlab/gitlab.d.ts +30 -2
  70. package/dist/scripts/shared/pull-request/gitlab/gitlab.d.ts.map +1 -1
  71. package/dist/scripts/shared/pull-request/gitlab/gitlab.js +70 -4
  72. package/dist/scripts/shared/pull-request/gitlab/gitlab.js.map +1 -1
  73. package/dist/scripts/shared/pull-request/pr-body/pr-body.d.ts.map +1 -1
  74. package/dist/scripts/shared/pull-request/pr-body/pr-body.js +6 -1
  75. package/dist/scripts/shared/pull-request/pr-body/pr-body.js.map +1 -1
  76. package/dist/types/remote.d.ts +2 -0
  77. package/dist/types/remote.d.ts.map +1 -1
  78. package/package.json +1 -1
@@ -0,0 +1,59 @@
1
+ import { fetchIssue as fetchGitHubIssue, resolveUsername, } from '../../scripts/board/github/github.js';
2
+ import { buildAuthHeader, fetchTicket as fetchJiraTicket, } from '../../scripts/board/jira/jira.js';
3
+ import { fetchIssue as fetchLinearIssue } from '../../scripts/board/linear/linear.js';
4
+ // ─── Board-specific fetch ────────────────────────────────────────────────────
5
+ export async function fetchTicket(config) {
6
+ switch (config.provider) {
7
+ case 'jira': {
8
+ const { env } = config;
9
+ const auth = buildAuthHeader(env.JIRA_USER, env.JIRA_API_TOKEN);
10
+ const ticket = await fetchJiraTicket(env.JIRA_BASE_URL, auth, env.JIRA_PROJECT_KEY, env.CLANCY_JQL_STATUS ?? 'To Do', env.CLANCY_JQL_SPRINT, env.CLANCY_LABEL);
11
+ if (!ticket)
12
+ return undefined;
13
+ const blockerStr = ticket.blockers.length
14
+ ? `Blocked by: ${ticket.blockers.join(', ')}`
15
+ : 'None';
16
+ return {
17
+ key: ticket.key,
18
+ title: ticket.title,
19
+ description: ticket.description,
20
+ parentInfo: ticket.epicKey ?? 'none',
21
+ blockers: blockerStr,
22
+ };
23
+ }
24
+ case 'github': {
25
+ const { env } = config;
26
+ const username = await resolveUsername(env.GITHUB_TOKEN);
27
+ const ticket = await fetchGitHubIssue(env.GITHUB_TOKEN, env.GITHUB_REPO, env.CLANCY_LABEL, username);
28
+ if (!ticket)
29
+ return undefined;
30
+ return {
31
+ key: ticket.key,
32
+ title: ticket.title,
33
+ description: ticket.description,
34
+ parentInfo: ticket.milestone ?? 'none',
35
+ blockers: 'None',
36
+ };
37
+ }
38
+ case 'linear': {
39
+ const { env } = config;
40
+ const ticket = await fetchLinearIssue({
41
+ LINEAR_API_KEY: env.LINEAR_API_KEY,
42
+ LINEAR_TEAM_ID: env.LINEAR_TEAM_ID,
43
+ CLANCY_LABEL: env.CLANCY_LABEL,
44
+ });
45
+ if (!ticket)
46
+ return undefined;
47
+ return {
48
+ key: ticket.key,
49
+ title: ticket.title,
50
+ description: ticket.description,
51
+ parentInfo: ticket.parentIdentifier ?? 'none',
52
+ blockers: 'None',
53
+ linearIssueId: ticket.issueId,
54
+ issueId: ticket.issueId,
55
+ };
56
+ }
57
+ }
58
+ }
59
+ //# sourceMappingURL=fetch-ticket.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fetch-ticket.js","sourceRoot":"","sources":["../../../src/scripts/once/fetch-ticket.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,UAAU,IAAI,gBAAgB,EAC9B,eAAe,GAChB,MAAM,kCAAkC,CAAC;AAC1C,OAAO,EACL,eAAe,EACf,WAAW,IAAI,eAAe,GAC/B,MAAM,8BAA8B,CAAC;AACtC,OAAO,EAAE,UAAU,IAAI,gBAAgB,EAAE,MAAM,kCAAkC,CAAC;AAKlF,gFAAgF;AAEhF,MAAM,CAAC,KAAK,UAAU,WAAW,CAC/B,MAAmB;IAEnB,QAAQ,MAAM,CAAC,QAAQ,EAAE,CAAC;QACxB,KAAK,MAAM,CAAC,CAAC,CAAC;YACZ,MAAM,EAAE,GAAG,EAAE,GAAG,MAAM,CAAC;YACvB,MAAM,IAAI,GAAG,eAAe,CAAC,GAAG,CAAC,SAAS,EAAE,GAAG,CAAC,cAAc,CAAC,CAAC;YAChE,MAAM,MAAM,GAAG,MAAM,eAAe,CAClC,GAAG,CAAC,aAAa,EACjB,IAAI,EACJ,GAAG,CAAC,gBAAgB,EACpB,GAAG,CAAC,iBAAiB,IAAI,OAAO,EAChC,GAAG,CAAC,iBAAiB,EACrB,GAAG,CAAC,YAAY,CACjB,CAAC;YAEF,IAAI,CAAC,MAAM;gBAAE,OAAO,SAAS,CAAC;YAE9B,MAAM,UAAU,GAAG,MAAM,CAAC,QAAQ,CAAC,MAAM;gBACvC,CAAC,CAAC,eAAe,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE;gBAC7C,CAAC,CAAC,MAAM,CAAC;YAEX,OAAO;gBACL,GAAG,EAAE,MAAM,CAAC,GAAG;gBACf,KAAK,EAAE,MAAM,CAAC,KAAK;gBACnB,WAAW,EAAE,MAAM,CAAC,WAAW;gBAC/B,UAAU,EAAE,MAAM,CAAC,OAAO,IAAI,MAAM;gBACpC,QAAQ,EAAE,UAAU;aACrB,CAAC;QACJ,CAAC;QAED,KAAK,QAAQ,CAAC,CAAC,CAAC;YACd,MAAM,EAAE,GAAG,EAAE,GAAG,MAAM,CAAC;YACvB,MAAM,QAAQ,GAAG,MAAM,eAAe,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC;YACzD,MAAM,MAAM,GAAG,MAAM,gBAAgB,CACnC,GAAG,CAAC,YAAY,EAChB,GAAG,CAAC,WAAW,EACf,GAAG,CAAC,YAAY,EAChB,QAAQ,CACT,CAAC;YAEF,IAAI,CAAC,MAAM;gBAAE,OAAO,SAAS,CAAC;YAE9B,OAAO;gBACL,GAAG,EAAE,MAAM,CAAC,GAAG;gBACf,KAAK,EAAE,MAAM,CAAC,KAAK;gBACnB,WAAW,EAAE,MAAM,CAAC,WAAW;gBAC/B,UAAU,EAAE,MAAM,CAAC,SAAS,IAAI,MAAM;gBACtC,QAAQ,EAAE,MAAM;aACjB,CAAC;QACJ,CAAC;QAED,KAAK,QAAQ,CAAC,CAAC,CAAC;YACd,MAAM,EAAE,GAAG,EAAE,GAAG,MAAM,CAAC;YACvB,MAAM,MAAM,GAAG,MAAM,gBAAgB,CAAC;gBACpC,cAAc,EAAE,GAAG,CAAC,cAAc;gBAClC,cAAc,EAAE,GAAG,CAAC,cAAc;gBAClC,YAAY,EAAE,GAAG,CAAC,YAAY;aAC/B,CAAC,CAAC;YAEH,IAAI,CAAC,MAAM;gBAAE,OAAO,SAAS,CAAC;YAE9B,OAAO;gBACL,GAAG,EAAE,MAAM,CAAC,GAAG;gBACf,KAAK,EAAE,MAAM,CAAC,KAAK;gBACnB,WAAW,EAAE,MAAM,CAAC,WAAW;gBAC/B,UAAU,EAAE,MAAM,CAAC,gBAAgB,IAAI,MAAM;gBAC7C,QAAQ,EAAE,MAAM;gBAChB,aAAa,EAAE,MAAM,CAAC,OAAO;gBAC7B,OAAO,EAAE,MAAM,CAAC,OAAO;aACxB,CAAC;QACJ,CAAC;IACH,CAAC;AACH,CAAC"}
@@ -0,0 +1,13 @@
1
+ import type { BoardConfig } from '../../scripts/shared/env-schema/env-schema.js';
2
+ import type { RemoteInfo } from '../../types/index.js';
3
+ /**
4
+ * Resolve a git host token from the board config's env.
5
+ *
6
+ * For GitHub boards, `GITHUB_TOKEN` is always present.
7
+ * For Jira/Linear boards, check the shared optional vars.
8
+ */
9
+ export declare function resolveGitToken(config: BoardConfig, remote: RemoteInfo): {
10
+ token: string;
11
+ username?: string;
12
+ } | undefined;
13
+ //# sourceMappingURL=git-token.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"git-token.d.ts","sourceRoot":"","sources":["../../../src/scripts/once/git-token.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,2CAA2C,CAAC;AAC7E,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAInD;;;;;GAKG;AACH,wBAAgB,eAAe,CAC7B,MAAM,EAAE,WAAW,EACnB,MAAM,EAAE,UAAU,GACjB;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,CAmBlD"}
@@ -0,0 +1,30 @@
1
+ import { sharedEnv } from './board-ops.js';
2
+ /**
3
+ * Resolve a git host token from the board config's env.
4
+ *
5
+ * For GitHub boards, `GITHUB_TOKEN` is always present.
6
+ * For Jira/Linear boards, check the shared optional vars.
7
+ */
8
+ export function resolveGitToken(config, remote) {
9
+ const env = sharedEnv(config);
10
+ switch (remote.host) {
11
+ case 'github':
12
+ if (env.GITHUB_TOKEN)
13
+ return { token: env.GITHUB_TOKEN };
14
+ break;
15
+ case 'gitlab':
16
+ if (env.GITLAB_TOKEN)
17
+ return { token: env.GITLAB_TOKEN };
18
+ break;
19
+ case 'bitbucket':
20
+ if (env.BITBUCKET_USER && env.BITBUCKET_TOKEN)
21
+ return { token: env.BITBUCKET_TOKEN, username: env.BITBUCKET_USER };
22
+ break;
23
+ case 'bitbucket-server':
24
+ if (env.BITBUCKET_TOKEN)
25
+ return { token: env.BITBUCKET_TOKEN };
26
+ break;
27
+ }
28
+ return undefined;
29
+ }
30
+ //# sourceMappingURL=git-token.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"git-token.js","sourceRoot":"","sources":["../../../src/scripts/once/git-token.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAE3C;;;;;GAKG;AACH,MAAM,UAAU,eAAe,CAC7B,MAAmB,EACnB,MAAkB;IAElB,MAAM,GAAG,GAAG,SAAS,CAAC,MAAM,CAAC,CAAC;IAE9B,QAAQ,MAAM,CAAC,IAAI,EAAE,CAAC;QACpB,KAAK,QAAQ;YACX,IAAI,GAAG,CAAC,YAAY;gBAAE,OAAO,EAAE,KAAK,EAAE,GAAG,CAAC,YAAY,EAAE,CAAC;YACzD,MAAM;QACR,KAAK,QAAQ;YACX,IAAI,GAAG,CAAC,YAAY;gBAAE,OAAO,EAAE,KAAK,EAAE,GAAG,CAAC,YAAY,EAAE,CAAC;YACzD,MAAM;QACR,KAAK,WAAW;YACd,IAAI,GAAG,CAAC,cAAc,IAAI,GAAG,CAAC,eAAe;gBAC3C,OAAO,EAAE,KAAK,EAAE,GAAG,CAAC,eAAe,EAAE,QAAQ,EAAE,GAAG,CAAC,cAAc,EAAE,CAAC;YACtE,MAAM;QACR,KAAK,kBAAkB;YACrB,IAAI,GAAG,CAAC,eAAe;gBAAE,OAAO,EAAE,KAAK,EAAE,GAAG,CAAC,eAAe,EAAE,CAAC;YAC/D,MAAM;IACV,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"once.d.ts","sourceRoot":"","sources":["../../../src/scripts/once/once.ts"],"names":[],"mappings":"AAsuBA;;;;;;;;;GASG;AACH,wBAAsB,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CA2SvD"}
1
+ {"version":3,"file":"once.d.ts","sourceRoot":"","sources":["../../../src/scripts/once/once.ts"],"names":[],"mappings":"AAqDA;;;;;;;;;GASG;AACH,wBAAsB,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAsUvD"}
@@ -10,431 +10,21 @@
10
10
  */
11
11
  import { resolve } from 'node:path';
12
12
  import { fileURLToPath } from 'node:url';
13
- import { closeIssue, fetchIssue as fetchGitHubIssue, isValidRepo, pingGitHub, resolveUsername, } from '../../scripts/board/github/github.js';
14
- import { buildAuthHeader, fetchTicket as fetchJiraTicket, isSafeJqlValue, pingJira, transitionIssue as transitionJiraIssue, } from '../../scripts/board/jira/jira.js';
15
- import { fetchIssue as fetchLinearIssue, isValidTeamId, pingLinear, transitionIssue as transitionLinearIssue, } from '../../scripts/board/linear/linear.js';
16
13
  import { computeTargetBranch, computeTicketBranch, } from '../../scripts/shared/branch/branch.js';
17
14
  import { invokeClaudeSession } from '../../scripts/shared/claude-cli/claude-cli.js';
18
15
  import { detectBoard } from '../../scripts/shared/env-schema/env-schema.js';
19
16
  import { checkFeasibility } from '../../scripts/shared/feasibility/feasibility.js';
20
17
  import { formatDuration } from '../../scripts/shared/format/format.js';
21
- import { checkout, currentBranch, deleteBranch, ensureBranch, fetchRemoteBranch, pushBranch, squashMerge, } from '../../scripts/shared/git-ops/git-ops.js';
18
+ import { checkout, currentBranch, diffAgainstBranch, ensureBranch, fetchRemoteBranch, } from '../../scripts/shared/git-ops/git-ops.js';
22
19
  import { sendNotification } from '../../scripts/shared/notify/notify.js';
23
20
  import { runPreflight } from '../../scripts/shared/preflight/preflight.js';
24
- import { appendProgress, countReworkCycles, findEntriesWithStatus, } from '../../scripts/shared/progress/progress.js';
21
+ import { appendProgress, countReworkCycles, } from '../../scripts/shared/progress/progress.js';
25
22
  import { buildPrompt, buildReworkPrompt, } from '../../scripts/shared/prompt/prompt.js';
26
- import { checkPrReviewState as checkBitbucketPrReviewState, checkServerPrReviewState as checkBitbucketServerPrReviewState, createPullRequest as createBitbucketPr, createServerPullRequest as createBitbucketServerPr, fetchPrReviewComments as fetchBitbucketPrReviewComments, fetchServerPrReviewComments as fetchBitbucketServerPrReviewComments, } from '../../scripts/shared/pull-request/bitbucket/bitbucket.js';
27
- import { checkPrReviewState as checkGitHubPrReviewState, createPullRequest as createGitHubPr, fetchPrReviewComments as fetchGitHubPrReviewComments, } from '../../scripts/shared/pull-request/github/github.js';
28
- import { checkMrReviewState as checkGitLabMrReviewState, createMergeRequest as createGitLabMr, fetchMrReviewComments as fetchGitLabMrReviewComments, } from '../../scripts/shared/pull-request/gitlab/gitlab.js';
29
- import { buildPrBody } from '../../scripts/shared/pull-request/pr-body/pr-body.js';
30
- import { buildApiBaseUrl, detectRemote, } from '../../scripts/shared/remote/remote.js';
31
23
  import { bold, dim, green, red, yellow } from '../../utils/ansi/ansi.js';
32
- // ─── Helpers ──────────────────────────────────────────────────────────────────
33
- /** Type-safe access to shared env vars across all board configs. */
34
- function sharedEnv(config) {
35
- return config.env;
36
- }
37
- // ─── Board-specific fetch ────────────────────────────────────────────────────
38
- async function fetchTicket(config) {
39
- switch (config.provider) {
40
- case 'jira': {
41
- const { env } = config;
42
- const auth = buildAuthHeader(env.JIRA_USER, env.JIRA_API_TOKEN);
43
- const ticket = await fetchJiraTicket(env.JIRA_BASE_URL, auth, env.JIRA_PROJECT_KEY, env.CLANCY_JQL_STATUS ?? 'To Do', env.CLANCY_JQL_SPRINT, env.CLANCY_LABEL);
44
- if (!ticket)
45
- return undefined;
46
- const blockerStr = ticket.blockers.length
47
- ? `Blocked by: ${ticket.blockers.join(', ')}`
48
- : 'None';
49
- return {
50
- key: ticket.key,
51
- title: ticket.title,
52
- description: ticket.description,
53
- parentInfo: ticket.epicKey ?? 'none',
54
- blockers: blockerStr,
55
- };
56
- }
57
- case 'github': {
58
- const { env } = config;
59
- const username = await resolveUsername(env.GITHUB_TOKEN);
60
- const ticket = await fetchGitHubIssue(env.GITHUB_TOKEN, env.GITHUB_REPO, env.CLANCY_LABEL, username);
61
- if (!ticket)
62
- return undefined;
63
- return {
64
- key: ticket.key,
65
- title: ticket.title,
66
- description: ticket.description,
67
- parentInfo: ticket.milestone ?? 'none',
68
- blockers: 'None',
69
- };
70
- }
71
- case 'linear': {
72
- const { env } = config;
73
- const ticket = await fetchLinearIssue({
74
- LINEAR_API_KEY: env.LINEAR_API_KEY,
75
- LINEAR_TEAM_ID: env.LINEAR_TEAM_ID,
76
- CLANCY_LABEL: env.CLANCY_LABEL,
77
- });
78
- if (!ticket)
79
- return undefined;
80
- return {
81
- key: ticket.key,
82
- title: ticket.title,
83
- description: ticket.description,
84
- parentInfo: ticket.parentIdentifier ?? 'none',
85
- blockers: 'None',
86
- linearIssueId: ticket.issueId,
87
- issueId: ticket.issueId,
88
- };
89
- }
90
- }
91
- }
92
- // ─── PR-based rework detection ────────────────────────────────────────────────
93
- /**
94
- * Check open PRs for review feedback requesting changes.
95
- *
96
- * Scans progress.txt for tickets with status PR_CREATED, then checks
97
- * the corresponding PR's review state on the detected remote platform.
98
- * If a reviewer has requested changes, returns the ticket and feedback.
99
- *
100
- * This is best-effort — errors are swallowed so the orchestrator
101
- * can fall through to fresh ticket fetch.
102
- */
103
- async function fetchReworkFromPrReview(config) {
104
- const prCreated = findEntriesWithStatus(process.cwd(), 'PR_CREATED');
105
- const reworked = findEntriesWithStatus(process.cwd(), 'REWORK');
106
- const candidates = [...prCreated, ...reworked];
107
- if (candidates.length === 0)
108
- return undefined;
109
- const platformOverride = sharedEnv(config).CLANCY_GIT_PLATFORM;
110
- const remote = detectRemote(platformOverride);
111
- if (remote.host === 'none' ||
112
- remote.host === 'unknown' ||
113
- remote.host === 'azure') {
114
- return undefined;
115
- }
116
- const creds = resolveGitToken(config, remote);
117
- if (!creds)
118
- return undefined;
119
- const apiBase = buildApiBaseUrl(remote, sharedEnv(config).CLANCY_GIT_API_URL);
120
- if (!apiBase)
121
- return undefined;
122
- // Limit to first 5 candidates to avoid rate limits
123
- const toCheck = candidates.slice(0, 5);
124
- for (const entry of toCheck) {
125
- const branch = computeTicketBranch(config.provider, entry.key);
126
- // Convert progress timestamp (YYYY-MM-DD HH:MM) to ISO 8601 for API filtering.
127
- // Only comments created AFTER this timestamp should trigger rework,
128
- // preventing stale inline comments from causing infinite rework loops.
129
- // Parse local timestamp (YYYY-MM-DD HH:MM) and convert to ISO 8601.
130
- // Falls back to undefined if timestamp is invalid (skips filtering).
131
- let since;
132
- if (entry.timestamp) {
133
- const date = new Date(entry.timestamp.replace(' ', 'T'));
134
- since = Number.isNaN(date.getTime()) ? undefined : date.toISOString();
135
- }
136
- let reviewState;
137
- switch (remote.host) {
138
- case 'github':
139
- reviewState = await checkGitHubPrReviewState(creds.token, `${remote.owner}/${remote.repo}`, branch, remote.owner, apiBase, since);
140
- break;
141
- case 'gitlab':
142
- reviewState = await checkGitLabMrReviewState(creds.token, apiBase, remote.projectPath, branch, since);
143
- break;
144
- case 'bitbucket':
145
- reviewState = await checkBitbucketPrReviewState(creds.username, creds.token, remote.workspace, remote.repoSlug, branch, since);
146
- break;
147
- case 'bitbucket-server':
148
- reviewState = await checkBitbucketServerPrReviewState(creds.token, apiBase, remote.projectKey, remote.repoSlug, branch, since);
149
- break;
150
- }
151
- if (reviewState?.changesRequested) {
152
- // Fetch review comments for the PR
153
- let feedback = [];
154
- switch (remote.host) {
155
- case 'github':
156
- feedback = await fetchGitHubPrReviewComments(creds.token, `${remote.owner}/${remote.repo}`, reviewState.prNumber, apiBase, since);
157
- break;
158
- case 'gitlab':
159
- feedback = await fetchGitLabMrReviewComments(creds.token, apiBase, remote.projectPath, reviewState.prNumber, since);
160
- break;
161
- case 'bitbucket':
162
- feedback = await fetchBitbucketPrReviewComments(creds.username, creds.token, remote.workspace, remote.repoSlug, reviewState.prNumber, since);
163
- break;
164
- case 'bitbucket-server':
165
- feedback = await fetchBitbucketServerPrReviewComments(creds.token, apiBase, remote.projectKey, remote.repoSlug, reviewState.prNumber, since);
166
- break;
167
- }
168
- const ticket = {
169
- key: entry.key,
170
- title: entry.summary,
171
- description: '',
172
- parentInfo: 'none',
173
- blockers: 'None',
174
- };
175
- return { ticket, feedback, prNumber: reviewState.prNumber };
176
- }
177
- }
178
- return undefined;
179
- }
180
- // ─── Board-specific ping ─────────────────────────────────────────────────────
181
- async function pingBoard(config) {
182
- switch (config.provider) {
183
- case 'jira': {
184
- const { env } = config;
185
- const auth = buildAuthHeader(env.JIRA_USER, env.JIRA_API_TOKEN);
186
- return pingJira(env.JIRA_BASE_URL, env.JIRA_PROJECT_KEY, auth);
187
- }
188
- case 'github':
189
- return pingGitHub(config.env.GITHUB_TOKEN, config.env.GITHUB_REPO);
190
- case 'linear':
191
- return pingLinear(config.env.LINEAR_API_KEY);
192
- }
193
- }
194
- // ─── Board-specific validation ───────────────────────────────────────────────
195
- function validateInputs(config) {
196
- switch (config.provider) {
197
- case 'jira': {
198
- const { env } = config;
199
- if (!isSafeJqlValue(env.JIRA_PROJECT_KEY)) {
200
- return '✗ JIRA_PROJECT_KEY contains invalid characters';
201
- }
202
- if (env.CLANCY_LABEL && !isSafeJqlValue(env.CLANCY_LABEL)) {
203
- return '✗ CLANCY_LABEL contains invalid characters';
204
- }
205
- if (env.CLANCY_JQL_STATUS && !isSafeJqlValue(env.CLANCY_JQL_STATUS)) {
206
- return '✗ CLANCY_JQL_STATUS contains invalid characters';
207
- }
208
- return undefined;
209
- }
210
- case 'github': {
211
- if (!isValidRepo(config.env.GITHUB_REPO)) {
212
- return '✗ GITHUB_REPO format is invalid — expected owner/repo';
213
- }
214
- return undefined;
215
- }
216
- case 'linear': {
217
- if (!isValidTeamId(config.env.LINEAR_TEAM_ID)) {
218
- return '✗ LINEAR_TEAM_ID contains invalid characters';
219
- }
220
- return undefined;
221
- }
222
- }
223
- }
224
- // ─── Board-specific transitions ──────────────────────────────────────────────
225
- async function transitionToStatus(config, ticket, statusName) {
226
- switch (config.provider) {
227
- case 'jira': {
228
- const { env } = config;
229
- const auth = buildAuthHeader(env.JIRA_USER, env.JIRA_API_TOKEN);
230
- const issueKey = ticket.key;
231
- const ok = await transitionJiraIssue(env.JIRA_BASE_URL, auth, issueKey, statusName);
232
- if (ok)
233
- console.log(` → Transitioned to ${statusName}`);
234
- break;
235
- }
236
- case 'github': {
237
- // GitHub Issues only has open/closed — status transitions not applicable
238
- // closeIssue is called separately after merge
239
- break;
240
- }
241
- case 'linear': {
242
- const { env } = config;
243
- if (!ticket.linearIssueId)
244
- break;
245
- const ok = await transitionLinearIssue(env.LINEAR_API_KEY, env.LINEAR_TEAM_ID, ticket.linearIssueId, statusName);
246
- if (ok)
247
- console.log(` → Transitioned to ${statusName}`);
248
- break;
249
- }
250
- }
251
- }
252
- // ─── PR/MR creation ──────────────────────────────────────────────────────────
253
- /**
254
- * Resolve a git host token from the board config's env.
255
- *
256
- * For GitHub boards, `GITHUB_TOKEN` is always present.
257
- * For Jira/Linear boards, check the shared optional vars.
258
- */
259
- function resolveGitToken(config, remote) {
260
- const env = sharedEnv(config);
261
- switch (remote.host) {
262
- case 'github':
263
- if (env.GITHUB_TOKEN)
264
- return { token: env.GITHUB_TOKEN };
265
- break;
266
- case 'gitlab':
267
- if (env.GITLAB_TOKEN)
268
- return { token: env.GITLAB_TOKEN };
269
- break;
270
- case 'bitbucket':
271
- if (env.BITBUCKET_USER && env.BITBUCKET_TOKEN)
272
- return { token: env.BITBUCKET_TOKEN, username: env.BITBUCKET_USER };
273
- break;
274
- case 'bitbucket-server':
275
- if (env.BITBUCKET_TOKEN)
276
- return { token: env.BITBUCKET_TOKEN };
277
- break;
278
- }
279
- return undefined;
280
- }
281
- /**
282
- * Attempt to create a PR/MR on the detected remote platform.
283
- */
284
- async function attemptPrCreation(config, remote, ticketBranch, targetBranch, title, body) {
285
- const creds = resolveGitToken(config, remote);
286
- if (!creds)
287
- return undefined;
288
- const apiBase = buildApiBaseUrl(remote, sharedEnv(config).CLANCY_GIT_API_URL);
289
- if (!apiBase)
290
- return undefined;
291
- switch (remote.host) {
292
- case 'github':
293
- return createGitHubPr(creds.token, `${remote.owner}/${remote.repo}`, ticketBranch, targetBranch, title, body, apiBase);
294
- case 'gitlab':
295
- return createGitLabMr(creds.token, apiBase, remote.projectPath, ticketBranch, targetBranch, title, body);
296
- case 'bitbucket':
297
- return createBitbucketPr(creds.username, creds.token, remote.workspace, remote.repoSlug, ticketBranch, targetBranch, title, body);
298
- case 'bitbucket-server':
299
- return createBitbucketServerPr(creds.token, apiBase, remote.projectKey, remote.repoSlug, ticketBranch, targetBranch, title, body);
300
- default:
301
- return undefined;
302
- }
303
- }
304
- /**
305
- * Build a manual PR/MR URL for the user to click.
306
- */
307
- function buildManualPrUrl(remote, ticketBranch, targetBranch) {
308
- const encodedTicket = encodeURIComponent(ticketBranch);
309
- const encodedTarget = encodeURIComponent(targetBranch);
310
- if (remote.host === 'github') {
311
- return `https://${remote.hostname}/${remote.owner}/${remote.repo}/compare/${encodedTarget}...${encodedTicket}`;
312
- }
313
- if (remote.host === 'gitlab') {
314
- return `https://${remote.hostname}/${remote.projectPath}/-/merge_requests/new?merge_request[source_branch]=${encodedTicket}&merge_request[target_branch]=${encodedTarget}`;
315
- }
316
- if (remote.host === 'bitbucket') {
317
- return `https://${remote.hostname}/${remote.workspace}/${remote.repoSlug}/pull-requests/new?source=${encodedTicket}&dest=${encodedTarget}`;
318
- }
319
- return undefined;
320
- }
321
- // ─── Delivery paths ──────────────────────────────────────────────────────────
322
- /**
323
- * Epic/parent flow: squash merge locally, delete branch, transition to Done.
324
- */
325
- async function deliverViaEpicMerge(config, ticket, ticketBranch, targetBranch) {
326
- checkout(targetBranch);
327
- const commitMsg = `feat(${ticket.key}): ${ticket.title}`;
328
- const hadChanges = squashMerge(ticketBranch, commitMsg);
329
- if (!hadChanges) {
330
- console.log(yellow('⚠ No changes staged after squash merge. Claude may not have committed any work.'));
331
- }
332
- deleteBranch(ticketBranch);
333
- // Transition to Done / close issue (best-effort)
334
- const statusDone = config.env.CLANCY_STATUS_DONE;
335
- if (config.provider === 'github') {
336
- const issueNumber = parseInt(ticket.key.replace('#', ''), 10);
337
- if (Number.isNaN(issueNumber)) {
338
- console.log(`⚠ Could not parse issue number from ${ticket.key}. Close it manually on GitHub.`);
339
- }
340
- else {
341
- const closed = await closeIssue(config.env.GITHUB_TOKEN, config.env.GITHUB_REPO, issueNumber);
342
- if (!closed) {
343
- console.log(`⚠ Could not close issue ${ticket.key}. Close it manually on GitHub.`);
344
- }
345
- }
346
- }
347
- else if (statusDone) {
348
- await transitionToStatus(config, ticket, statusDone);
349
- }
350
- appendProgress(process.cwd(), ticket.key, ticket.title, 'DONE');
351
- }
352
- /**
353
- * PR flow: push branch to remote, create PR/MR, transition to In Review.
354
- *
355
- * @returns `false` if the push failed (caller should handle early return).
356
- */
357
- async function deliverViaPullRequest(config, ticket, ticketBranch, targetBranch, startTime) {
358
- const pushed = pushBranch(ticketBranch);
359
- if (!pushed) {
360
- console.log(yellow(`⚠ Could not push ${ticketBranch} to origin.`));
361
- console.log(dim(' The branch is still available locally. Push manually:'));
362
- console.log(dim(` git push -u origin ${ticketBranch}`));
363
- appendProgress(process.cwd(), ticket.key, ticket.title, 'PUSH_FAILED');
364
- checkout(targetBranch);
365
- const elapsed = formatDuration(Date.now() - startTime);
366
- console.log('');
367
- console.log(yellow(`⚠ ${ticket.key} implemented but push failed`) +
368
- dim(` (${elapsed})`));
369
- return false;
370
- }
371
- console.log(green(` ✓ Pushed ${ticketBranch}`));
372
- // Attempt PR/MR creation
373
- const platformOverride = sharedEnv(config).CLANCY_GIT_PLATFORM;
374
- const remote = detectRemote(platformOverride);
375
- const prTitle = `feat(${ticket.key}): ${ticket.title}`;
376
- const prBody = buildPrBody(config, {
377
- key: ticket.key,
378
- title: ticket.title,
379
- description: ticket.description,
380
- provider: config.provider,
381
- });
382
- if (remote.host !== 'none' &&
383
- remote.host !== 'unknown' &&
384
- remote.host !== 'azure') {
385
- const pr = await attemptPrCreation(config, remote, ticketBranch, targetBranch, prTitle, prBody);
386
- if (pr?.ok) {
387
- console.log(green(` ✓ PR created: ${pr.url}`));
388
- appendProgress(process.cwd(), ticket.key, ticket.title, 'PR_CREATED');
389
- }
390
- else if (pr && !pr.ok && pr.alreadyExists) {
391
- console.log(yellow(` ⚠ A PR/MR already exists for ${ticketBranch}. Branch pushed.`));
392
- appendProgress(process.cwd(), ticket.key, ticket.title, 'PUSHED');
393
- }
394
- else if (pr && !pr.ok) {
395
- console.log(yellow(` ⚠ PR/MR creation failed: ${pr.error}`));
396
- const manualUrl = buildManualPrUrl(remote, ticketBranch, targetBranch);
397
- if (manualUrl) {
398
- console.log(dim(` Create one manually: ${manualUrl}`));
399
- }
400
- else {
401
- console.log(dim(' Branch pushed — create a PR/MR manually.'));
402
- }
403
- appendProgress(process.cwd(), ticket.key, ticket.title, 'PUSHED');
404
- }
405
- else {
406
- // No token available for this platform
407
- const manualUrl = buildManualPrUrl(remote, ticketBranch, targetBranch);
408
- if (manualUrl) {
409
- console.log(dim(` Create a PR: ${manualUrl}`));
410
- }
411
- else {
412
- console.log(dim(' Branch pushed to remote. Create a PR/MR manually.'));
413
- }
414
- appendProgress(process.cwd(), ticket.key, ticket.title, 'PUSHED');
415
- }
416
- }
417
- else if (remote.host === 'none') {
418
- console.log(yellow(`⚠ No git remote configured. Branch available locally: ${ticketBranch}`));
419
- appendProgress(process.cwd(), ticket.key, ticket.title, 'LOCAL');
420
- }
421
- else {
422
- // Unknown or Azure remote — just note the push
423
- console.log(dim(' Branch pushed to remote. Create a PR/MR manually.'));
424
- appendProgress(process.cwd(), ticket.key, ticket.title, 'PUSHED');
425
- }
426
- // Transition to In Review (not Done — PR hasn't been merged yet)
427
- // For GitHub Issues: do NOT close — PR body has "Closes #N" for auto-close on merge
428
- if (config.provider !== 'github') {
429
- const statusReview = config.env.CLANCY_STATUS_REVIEW ?? config.env.CLANCY_STATUS_DONE;
430
- if (statusReview) {
431
- await transitionToStatus(config, ticket, statusReview);
432
- }
433
- }
434
- // Switch back to target branch
435
- checkout(targetBranch);
436
- return true;
437
- }
24
+ import { pingBoard, sharedEnv, transitionToStatus, validateInputs, } from './board-ops.js';
25
+ import { deliverViaEpicMerge, deliverViaPullRequest } from './deliver.js';
26
+ import { fetchTicket } from './fetch-ticket.js';
27
+ import { fetchReworkFromPrReview, postReworkActions } from './rework.js';
438
28
  // ─── Main orchestrator ───────────────────────────────────────────────────────
439
29
  /**
440
30
  * Run the once orchestrator — full ticket lifecycle.
@@ -489,6 +79,9 @@ export async function run(argv) {
489
79
  // 5. Check rework — PR-based detection, then fresh ticket
490
80
  let isRework = false;
491
81
  let prFeedback;
82
+ let reworkPrNumber;
83
+ let reworkDiscussionIds;
84
+ let reworkReviewers;
492
85
  let ticket;
493
86
  // PR-based rework (automatic, no config needed)
494
87
  try {
@@ -497,6 +90,9 @@ export async function run(argv) {
497
90
  isRework = true;
498
91
  ticket = prRework.ticket;
499
92
  prFeedback = prRework.feedback;
93
+ reworkPrNumber = prRework.prNumber;
94
+ reworkDiscussionIds = prRework.discussionIds;
95
+ reworkReviewers = prRework.reviewers;
500
96
  console.log(yellow(` ↻ PR rework: [${ticket.key}] ${ticket.title}`));
501
97
  }
502
98
  }
@@ -609,7 +205,7 @@ export async function run(argv) {
609
205
  description: ticket.description,
610
206
  provider: config.provider,
611
207
  feedbackComments: prFeedback ?? [],
612
- previousContext: undefined, // V1: no diff context yet
208
+ previousContext: diffAgainstBranch(targetBranch),
613
209
  });
614
210
  }
615
211
  else {
@@ -631,11 +227,17 @@ export async function run(argv) {
631
227
  const hasParent = ticket.parentInfo !== 'none';
632
228
  if (isRework) {
633
229
  // PR-flow rework: push to existing branch, PR updates automatically
634
- const delivered = await deliverViaPullRequest(config, ticket, ticketBranch, targetBranch, startTime);
635
- if (!delivered)
230
+ const delivered = await deliverViaPullRequest(config, ticket, ticketBranch, targetBranch, startTime, true);
231
+ if (!delivered) {
232
+ appendProgress(process.cwd(), ticket.key, ticket.title, 'PUSH_FAILED');
636
233
  return;
637
- // Override progress to REWORK (deliverViaPullRequest logs PR_CREATED/PUSHED)
638
- appendProgress(process.cwd(), ticket.key, ticket.title, 'REWORK');
234
+ }
235
+ // Log single REWORK entry (skipLog prevents deliverViaPullRequest from double-logging)
236
+ appendProgress(process.cwd(), ticket.key, ticket.title, 'REWORK', reworkPrNumber);
237
+ // Post-rework actions (all best-effort)
238
+ if (reworkPrNumber != null) {
239
+ await postReworkActions(config, reworkPrNumber, prFeedback ?? [], reworkDiscussionIds, reworkReviewers);
240
+ }
639
241
  }
640
242
  else if (hasParent) {
641
243
  await deliverViaEpicMerge(config, ticket, ticketBranch, targetBranch);