loreli 0.0.0 → 2.0.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 (104) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +710 -97
  3. package/bin/loreli.js +89 -0
  4. package/package.json +77 -14
  5. package/packages/README.md +101 -0
  6. package/packages/action/README.md +98 -0
  7. package/packages/action/prompts/action.md +172 -0
  8. package/packages/action/src/index.js +684 -0
  9. package/packages/agent/README.md +606 -0
  10. package/packages/agent/src/backends/claude.js +387 -0
  11. package/packages/agent/src/backends/codex.js +351 -0
  12. package/packages/agent/src/backends/cursor.js +371 -0
  13. package/packages/agent/src/backends/index.js +486 -0
  14. package/packages/agent/src/base.js +138 -0
  15. package/packages/agent/src/cli.js +275 -0
  16. package/packages/agent/src/discover.js +396 -0
  17. package/packages/agent/src/factory.js +124 -0
  18. package/packages/agent/src/index.js +12 -0
  19. package/packages/agent/src/models.js +159 -0
  20. package/packages/agent/src/output.js +62 -0
  21. package/packages/agent/src/session.js +162 -0
  22. package/packages/agent/src/trace.js +186 -0
  23. package/packages/classify/README.md +136 -0
  24. package/packages/classify/prompts/blocker.md +12 -0
  25. package/packages/classify/prompts/feedback.md +14 -0
  26. package/packages/classify/prompts/pane-state.md +20 -0
  27. package/packages/classify/src/index.js +81 -0
  28. package/packages/config/README.md +898 -0
  29. package/packages/config/src/defaults.js +145 -0
  30. package/packages/config/src/index.js +223 -0
  31. package/packages/config/src/schema.js +291 -0
  32. package/packages/config/src/validate.js +160 -0
  33. package/packages/context/README.md +165 -0
  34. package/packages/context/src/index.js +198 -0
  35. package/packages/hub/README.md +338 -0
  36. package/packages/hub/src/base.js +154 -0
  37. package/packages/hub/src/github.js +1597 -0
  38. package/packages/hub/src/index.js +79 -0
  39. package/packages/hub/src/labels.js +48 -0
  40. package/packages/identity/README.md +288 -0
  41. package/packages/identity/src/index.js +620 -0
  42. package/packages/identity/src/themes/avatar.js +217 -0
  43. package/packages/identity/src/themes/digimon.js +217 -0
  44. package/packages/identity/src/themes/dragonball.js +217 -0
  45. package/packages/identity/src/themes/lotr.js +217 -0
  46. package/packages/identity/src/themes/marvel.js +217 -0
  47. package/packages/identity/src/themes/pokemon.js +217 -0
  48. package/packages/identity/src/themes/starwars.js +217 -0
  49. package/packages/identity/src/themes/transformers.js +217 -0
  50. package/packages/identity/src/themes/zelda.js +217 -0
  51. package/packages/knowledge/README.md +217 -0
  52. package/packages/knowledge/src/index.js +243 -0
  53. package/packages/log/README.md +93 -0
  54. package/packages/log/src/index.js +252 -0
  55. package/packages/marker/README.md +200 -0
  56. package/packages/marker/src/index.js +184 -0
  57. package/packages/mcp/README.md +323 -0
  58. package/packages/mcp/instructions.md +126 -0
  59. package/packages/mcp/scaffolding/.agents/skills/loreli-context/SKILL.md +89 -0
  60. package/packages/mcp/scaffolding/ISSUE_TEMPLATE/config.yml +2 -0
  61. package/packages/mcp/scaffolding/ISSUE_TEMPLATE/loreli.yml +83 -0
  62. package/packages/mcp/scaffolding/loreli.yml +491 -0
  63. package/packages/mcp/scaffolding/mcp-configs/.codex/config.toml +4 -0
  64. package/packages/mcp/scaffolding/mcp-configs/.cursor/mcp.json +14 -0
  65. package/packages/mcp/scaffolding/mcp-configs/.mcp.json +14 -0
  66. package/packages/mcp/scaffolding/pull-request.md +23 -0
  67. package/packages/mcp/src/index.js +600 -0
  68. package/packages/mcp/src/tools/agent-context.js +44 -0
  69. package/packages/mcp/src/tools/agents.js +450 -0
  70. package/packages/mcp/src/tools/context.js +200 -0
  71. package/packages/mcp/src/tools/github.js +1163 -0
  72. package/packages/mcp/src/tools/hitl.js +162 -0
  73. package/packages/mcp/src/tools/index.js +18 -0
  74. package/packages/mcp/src/tools/refactor.js +227 -0
  75. package/packages/mcp/src/tools/repo.js +44 -0
  76. package/packages/mcp/src/tools/start.js +904 -0
  77. package/packages/mcp/src/tools/status.js +149 -0
  78. package/packages/mcp/src/tools/work.js +134 -0
  79. package/packages/orchestrator/README.md +192 -0
  80. package/packages/orchestrator/src/index.js +1492 -0
  81. package/packages/planner/README.md +251 -0
  82. package/packages/planner/prompts/plan-reviewer.md +109 -0
  83. package/packages/planner/prompts/planner.md +191 -0
  84. package/packages/planner/prompts/tiebreaker-reviewer.md +71 -0
  85. package/packages/planner/src/index.js +1381 -0
  86. package/packages/review/README.md +129 -0
  87. package/packages/review/prompts/reviewer.md +158 -0
  88. package/packages/review/src/index.js +1403 -0
  89. package/packages/risk/README.md +178 -0
  90. package/packages/risk/prompts/risk.md +272 -0
  91. package/packages/risk/src/index.js +439 -0
  92. package/packages/session/README.md +165 -0
  93. package/packages/session/src/index.js +215 -0
  94. package/packages/test-utils/README.md +96 -0
  95. package/packages/test-utils/src/index.js +354 -0
  96. package/packages/tmux/README.md +261 -0
  97. package/packages/tmux/src/index.js +501 -0
  98. package/packages/workflow/README.md +317 -0
  99. package/packages/workflow/prompts/preamble.md +14 -0
  100. package/packages/workflow/src/index.js +660 -0
  101. package/packages/workflow/src/proof-of-life.js +74 -0
  102. package/packages/workspace/README.md +143 -0
  103. package/packages/workspace/src/index.js +1127 -0
  104. package/index.js +0 -8
@@ -0,0 +1,160 @@
1
+ /**
2
+ * Input validation utilities for tool arguments.
3
+ *
4
+ * Provides strict validation for user-facing inputs before any side effects
5
+ * occur. Enforces name regex, length limits, reserved names, and enum checks.
6
+ *
7
+ * All validators throw descriptive errors on invalid input.
8
+ */
9
+
10
+ /**
11
+ * Regex for valid agent/team names. Alphanumeric, hyphens, underscores only.
12
+ *
13
+ * @type {RegExp}
14
+ */
15
+ const NAME_RE = /^[A-Za-z0-9_-]+$/;
16
+
17
+ /**
18
+ * Maximum length for agent names.
19
+ *
20
+ * @type {number}
21
+ */
22
+ const MAX_NAME_LENGTH = 64;
23
+
24
+ /**
25
+ * Regex for valid repo format: "owner/name".
26
+ *
27
+ * @type {RegExp}
28
+ */
29
+ const REPO_RE = /^[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/;
30
+
31
+ /**
32
+ * Valid agent roles.
33
+ *
34
+ * @type {Set<string>}
35
+ */
36
+ const ROLES = new Set(['planner', 'action', 'reviewer']);
37
+
38
+ /**
39
+ * Valid theme names.
40
+ *
41
+ * @type {Set<string>}
42
+ */
43
+ const THEMES = new Set(['transformers', 'pokemon', 'marvel', 'digimon', 'starwars', 'lotr', 'dragonball', 'avatar', 'zelda']);
44
+
45
+ /**
46
+ * Valid AI providers.
47
+ *
48
+ * @type {Set<string>}
49
+ */
50
+ const PROVIDERS = new Set(['openai', 'anthropic', 'cursor-openai', 'cursor-anthropic']);
51
+
52
+ /**
53
+ * Validate a repository string is in "owner/name" format.
54
+ *
55
+ * @param {string} repo - Repository identifier to validate.
56
+ * @returns {string} The validated repo string.
57
+ * @throws {Error} If the repo format is invalid.
58
+ */
59
+ export function repo(repo) {
60
+ if (!repo || typeof repo !== 'string') {
61
+ throw new Error('Repository is required.');
62
+ }
63
+ if (!REPO_RE.test(repo)) {
64
+ throw new Error(`Invalid repository format: "${repo}". Expected "owner/name".`);
65
+ }
66
+ return repo;
67
+ }
68
+
69
+ /**
70
+ * Validate an agent name meets naming requirements.
71
+ *
72
+ * @param {string} name - Agent name to validate.
73
+ * @returns {string} The validated name.
74
+ * @throws {Error} If the name is invalid.
75
+ */
76
+ export function name(name) {
77
+ if (!name || typeof name !== 'string') {
78
+ throw new Error('Agent name is required.');
79
+ }
80
+ if (name.length > MAX_NAME_LENGTH) {
81
+ throw new Error(`Agent name too long (${name.length} chars, max ${MAX_NAME_LENGTH}).`);
82
+ }
83
+ if (!NAME_RE.test(name)) {
84
+ throw new Error(`Invalid agent name: "${name}". Use only letters, numbers, hyphens, underscores.`);
85
+ }
86
+ return name;
87
+ }
88
+
89
+ /**
90
+ * Validate an agent role is one of the known roles.
91
+ *
92
+ * @param {string} role - Role to validate.
93
+ * @returns {string} The validated role.
94
+ * @throws {Error} If the role is invalid.
95
+ */
96
+ export function role(role) {
97
+ if (!role || typeof role !== 'string') {
98
+ throw new Error('Role is required.');
99
+ }
100
+ if (!ROLES.has(role)) {
101
+ throw new Error(`Invalid role: "${role}". Valid: ${[...ROLES].join(', ')}.`);
102
+ }
103
+ return role;
104
+ }
105
+
106
+ /**
107
+ * Validate a theme name or list of theme names.
108
+ * Accepts a single string or an array of strings, each checked
109
+ * against the known theme set.
110
+ *
111
+ * @param {string|string[]} value - Theme or themes to validate.
112
+ * @returns {string|string[]} The validated value (same shape as input).
113
+ * @throws {Error} If any theme is invalid or the list is empty.
114
+ */
115
+ export function theme(value) {
116
+ if (Array.isArray(value)) {
117
+ if (!value.length) throw new Error('Theme list must not be empty.');
118
+ for (const t of value) theme(t);
119
+ return value;
120
+ }
121
+ if (!value || typeof value !== 'string') {
122
+ throw new Error('Theme is required.');
123
+ }
124
+ if (!THEMES.has(value)) {
125
+ throw new Error(`Invalid theme: "${value}". Valid: ${[...THEMES].join(', ')}.`);
126
+ }
127
+ return value;
128
+ }
129
+
130
+ /**
131
+ * Validate a provider name is one of the known providers.
132
+ *
133
+ * @param {string} provider - Provider to validate.
134
+ * @returns {string} The validated provider.
135
+ * @throws {Error} If the provider is invalid.
136
+ */
137
+ export function provider(provider) {
138
+ if (!provider || typeof provider !== 'string') {
139
+ throw new Error('Provider is required.');
140
+ }
141
+ if (!PROVIDERS.has(provider)) {
142
+ throw new Error(`Invalid provider: "${provider}". Valid: ${[...PROVIDERS].join(', ')}.`);
143
+ }
144
+ return provider;
145
+ }
146
+
147
+ /**
148
+ * Validate a positive number (used for timeouts, PR numbers, etc.).
149
+ *
150
+ * @param {number} value - Number to validate.
151
+ * @param {string} label - Label for error messages.
152
+ * @returns {number} The validated number.
153
+ * @throws {Error} If the value is not a positive number.
154
+ */
155
+ export function positive(value, label) {
156
+ if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0) {
157
+ throw new Error(`${label} must be a positive number, got: ${value}.`);
158
+ }
159
+ return value;
160
+ }
@@ -0,0 +1,165 @@
1
+ # loreli/context
2
+
3
+ Context resolution engine for Loreli's read path.
4
+
5
+ This package resolves decision context from code and GitHub artifacts without using an LLM. It links file lines to commits, commits to PRs, PRs to issues, and issues to discussions so agents can understand why code exists.
6
+
7
+ ## API Reference
8
+
9
+ ### `blame(workspace, hub, repo, cwd, file, line)`
10
+
11
+ Resolve a file line to commit metadata and associated pull requests.
12
+
13
+ #### Parameters
14
+
15
+ | Name | Type | Description |
16
+ |------|------|-------------|
17
+ | `workspace` | `object` | Workspace module that provides `blame(cwd, file, line)`. |
18
+ | `hub` | `object` | Hub instance that provides `associatedPulls(repo, sha)`. |
19
+ | `repo` | `string` | Repository in `owner/name` format. |
20
+ | `cwd` | `string` | Local repository path used for git commands. |
21
+ | `file` | `string` | Repository-relative file path. |
22
+ | `line` | `number` | 1-based line number. |
23
+
24
+ #### Returns
25
+
26
+ `Promise<{ sha: string, author: string, date: string, summary: string, prs: Array<{ number: number, title: string, state: string }> }>`
27
+
28
+ #### Errors
29
+
30
+ Throws when the workspace blame operation fails (for example, file missing or invalid line).
31
+
32
+ If fetching associated PRs fails, the function returns successfully with `prs: []`.
33
+
34
+ ### `history(workspace, hub, repo, cwd, file, opts?)`
35
+
36
+ Return recent commit history for a file with PR associations.
37
+
38
+ #### Parameters
39
+
40
+ | Name | Type | Description |
41
+ |------|------|-------------|
42
+ | `workspace` | `object` | Workspace module that provides `gitlog(cwd, file, opts)`. |
43
+ | `hub` | `object` | Hub instance that provides `associatedPulls(repo, sha)`. |
44
+ | `repo` | `string` | Repository in `owner/name` format. |
45
+ | `cwd` | `string` | Local repository path used for git commands. |
46
+ | `file` | `string` | Repository-relative file path. |
47
+ | `opts` | `object` | Optional query options. |
48
+ | `opts.limit` | `number` | Maximum commits to return. Default is `10`. |
49
+
50
+ #### Returns
51
+
52
+ `Promise<Array<{ sha: string, date: string, message: string, prs: Array<{ number: number, title: string, state: string }> }>>`
53
+
54
+ #### Errors
55
+
56
+ Throws when the workspace log operation fails unexpectedly.
57
+
58
+ If PR association lookup fails for a commit, that commit is still returned with `prs: []`.
59
+
60
+ ### `resolve(hub, repo, pr)`
61
+
62
+ Walk from a pull request to linked issues and referenced discussions.
63
+
64
+ #### Parameters
65
+
66
+ | Name | Type | Description |
67
+ |------|------|-------------|
68
+ | `hub` | `object` | Hub instance with PR, issue, comment, and discussion APIs. |
69
+ | `repo` | `string` | Repository in `owner/name` format. |
70
+ | `pr` | `number` | Pull request number to resolve. |
71
+
72
+ #### Returns
73
+
74
+ `Promise<{ pr: object, issues: Array<object>, discussions: Array<object> }>`
75
+
76
+ The returned `pr` contains PR fields plus `comments`. Each returned issue/discussion includes its comments.
77
+
78
+ #### Errors
79
+
80
+ Throws when the root PR cannot be fetched.
81
+
82
+ Linked issue/discussion fetch failures are tolerated and skipped so partial context can still be returned.
83
+
84
+ ### `search(hub, repo, query)`
85
+
86
+ Search issues, pull requests, and discussions through the hub search API.
87
+
88
+ #### Parameters
89
+
90
+ | Name | Type | Description |
91
+ |------|------|-------------|
92
+ | `hub` | `object` | Hub instance that provides `searchIssues(repo, query)`. |
93
+ | `repo` | `string` | Repository in `owner/name` format. |
94
+ | `query` | `string` | Search query string. |
95
+
96
+ #### Returns
97
+
98
+ `Promise<Array<{ number: number, title: string, type: string, url: string }>>`
99
+
100
+ #### Errors
101
+
102
+ Throws when the hub search call fails.
103
+
104
+ ### `chain(hub, repo, items)`
105
+
106
+ Resolve and normalize a list of PRs, issues, and discussions into a context chain.
107
+
108
+ #### Parameters
109
+
110
+ | Name | Type | Description |
111
+ |------|------|-------------|
112
+ | `hub` | `object` | Hub instance with content fetch APIs. |
113
+ | `repo` | `string` | Repository in `owner/name` format. |
114
+ | `items` | `Array<{ type: string, number: number }>` | Sequence of items where `type` is `pr`, `issue`, or `discussion`. |
115
+
116
+ #### Returns
117
+
118
+ `Promise<Array<{ type: string, number: number, title: string, body: string, comments: Array<object>, reviews?: Array<object>, trace: object | null }>>`
119
+
120
+ PR and discussion entries include parsed trace marker data in `trace` when present. Issue entries return `trace: null`.
121
+
122
+ #### Errors
123
+
124
+ Item-level failures are tolerated and skipped. The function still resolves with successful entries.
125
+
126
+ ## Usage
127
+
128
+ This example demonstrates how to resolve file-line context for agent prompts. This matters because reviewer and planner prompts need traceable provenance from code to prior decisions. The call returns commit metadata plus any linked pull requests.
129
+
130
+ ```js
131
+ import * as workspace from 'loreli/workspace';
132
+ import { Hub } from 'loreli/hub';
133
+ import { blame } from 'loreli/context';
134
+
135
+ const hub = new Hub({ token: process.env.GITHUB_TOKEN });
136
+
137
+ const result = await blame(
138
+ workspace,
139
+ hub,
140
+ 'owner/repo',
141
+ '/path/to/local/clone',
142
+ 'src/server.js',
143
+ 42,
144
+ );
145
+
146
+ console.log(result.sha, result.prs);
147
+ ```
148
+
149
+ This example demonstrates how to assemble a mixed context chain across PRs, issues, and discussions. This matters when an agent needs complete decision history before proposing changes. The result preserves trace markers on PRs and discussions.
150
+
151
+ ```js
152
+ import { Hub } from 'loreli/hub';
153
+ import { chain } from 'loreli/context';
154
+
155
+ const hub = new Hub({ token: process.env.GITHUB_TOKEN });
156
+
157
+ const items = [
158
+ { type: 'pr', number: 123 },
159
+ { type: 'issue', number: 88 },
160
+ { type: 'discussion', number: 14 },
161
+ ];
162
+
163
+ const resolved = await chain(hub, 'owner/repo', items);
164
+ console.log(resolved.length);
165
+ ```
@@ -0,0 +1,198 @@
1
+ /**
2
+ * Context resolution engine — the read path.
3
+ *
4
+ * Traverses the decision chain from code to commits to PRs to issues
5
+ * to discussions, assembling the full context of why code exists.
6
+ * No LLM dependency — pure Git + GitHub API resolution.
7
+ *
8
+ * @module loreli/context
9
+ */
10
+
11
+ import { parse } from 'loreli/marker';
12
+
13
+ /**
14
+ * Regex to detect `Closes #N`, `Fixes #N`, or `Resolves #N` patterns
15
+ * in PR/issue bodies (GitHub auto-linking keywords).
16
+ *
17
+ * @type {RegExp}
18
+ */
19
+ const CLOSE_RE = /(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\s+#(\d+)/gi;
20
+
21
+ /**
22
+ * Resolve a file line to its originating commit and associated PRs.
23
+ * Runs git blame locally, then correlates the commit SHA to PRs via
24
+ * the GitHub API.
25
+ *
26
+ * @param {object} workspace - Workspace module with blame() function.
27
+ * @param {object} hub - Hub instance for GitHub API calls.
28
+ * @param {string} repo - "owner/name" repository.
29
+ * @param {string} cwd - Local clone directory.
30
+ * @param {string} file - Relative file path.
31
+ * @param {number} line - 1-based line number.
32
+ * @returns {Promise<{sha: string, author: string, date: string, summary: string, prs: Array<{number: number, title: string, state: string}>}>}
33
+ */
34
+ export async function blame(workspace, hub, repo, cwd, file, line) {
35
+ const info = await workspace.blame(cwd, file, line);
36
+ let prs = [];
37
+ try {
38
+ prs = await hub.associatedPulls(repo, info.sha);
39
+ } catch { /* commit may predate the repo or have no associated PRs */ }
40
+ return { ...info, prs };
41
+ }
42
+
43
+ /**
44
+ * Retrieve recent commit history for a file with associated PRs.
45
+ *
46
+ * @param {object} workspace - Workspace module with gitlog() function.
47
+ * @param {object} hub - Hub instance for GitHub API calls.
48
+ * @param {string} repo - "owner/name" repository.
49
+ * @param {string} cwd - Local clone directory.
50
+ * @param {string} file - Relative file path.
51
+ * @param {object} [opts] - Options.
52
+ * @param {number} [opts.limit=10] - Maximum commits to return.
53
+ * @returns {Promise<Array<{sha: string, date: string, message: string, prs: Array<{number: number, title: string, state: string}>}>>}
54
+ */
55
+ export async function history(workspace, hub, repo, cwd, file, opts = {}) {
56
+ const commits = await workspace.gitlog(cwd, file, opts);
57
+ const results = [];
58
+ for (const commit of commits) {
59
+ let prs = [];
60
+ try {
61
+ prs = await hub.associatedPulls(repo, commit.sha);
62
+ } catch { /* non-fatal */ }
63
+ results.push({ ...commit, prs });
64
+ }
65
+ return results;
66
+ }
67
+
68
+ /**
69
+ * Walk the decision chain from a PR number to its linked issues
70
+ * and discussions, collecting all comments and traces.
71
+ *
72
+ * @param {object} hub - Hub instance.
73
+ * @param {string} repo - "owner/name" repository.
74
+ * @param {number} pr - Pull request number.
75
+ * @returns {Promise<{pr: object, issues: Array<object>, discussions: Array<object>}>}
76
+ */
77
+ export async function resolve(hub, repo, pr) {
78
+ const pull = await hub.pull(repo, pr);
79
+ const prComments = await hub.comments(repo, pr);
80
+
81
+ // Extract linked issue numbers from PR body
82
+ const linked = new Set();
83
+ let match;
84
+ while ((match = CLOSE_RE.exec(pull.body)) !== null) {
85
+ linked.add(parseInt(match[1], 10));
86
+ }
87
+
88
+ // Also check sub-issues
89
+ try {
90
+ const subs = await hub.subs(repo, pr);
91
+ for (const sub of subs) linked.add(sub.number);
92
+ } catch { /* sub-issues API may not be available */ }
93
+
94
+ const issues = [];
95
+ const discussions = [];
96
+ const seen = new Set();
97
+
98
+ for (const num of linked) {
99
+ if (seen.has(num)) continue;
100
+ seen.add(num);
101
+
102
+ try {
103
+ const issue = await hub.issue(repo, num);
104
+ const comments = await hub.comments(repo, num);
105
+ issues.push({ ...issue, comments });
106
+
107
+ // Look for discussion references in issue body/comments
108
+ const bodies = [issue.body, ...comments.map(function body(c) { return c.body; })];
109
+ for (const body of bodies) {
110
+ const discRef = body?.match(/#(\d+)/g);
111
+ if (!discRef) continue;
112
+ for (const ref of discRef) {
113
+ const discNum = parseInt(ref.slice(1), 10);
114
+ if (seen.has(discNum)) continue;
115
+ seen.add(discNum);
116
+ try {
117
+ const disc = await hub.discussion(repo, discNum);
118
+ const discComments = await hub.discussionComments(disc.id);
119
+ discussions.push({ ...disc, comments: discComments });
120
+ } catch { /* not a discussion number */ }
121
+ }
122
+ }
123
+ } catch { /* issue may not exist */ }
124
+ }
125
+
126
+ return { pr: { ...pull, comments: prComments }, issues, discussions };
127
+ }
128
+
129
+ /**
130
+ * Search across issues, PRs, and discussions using GitHub Search API.
131
+ *
132
+ * @param {object} hub - Hub instance.
133
+ * @param {string} repo - "owner/name" repository.
134
+ * @param {string} query - Search keywords.
135
+ * @returns {Promise<Array<{number: number, title: string, type: string, url: string}>>}
136
+ */
137
+ export async function search(hub, repo, query) {
138
+ return hub.searchIssues(repo, query);
139
+ }
140
+
141
+ /**
142
+ * Build a full context chain from resolved items, including trace
143
+ * blocks. Unlike the `read` tool which strips traces via
144
+ * `excise(body, 'trace')`, this function preserves them.
145
+ *
146
+ * @param {object} hub - Hub instance.
147
+ * @param {string} repo - "owner/name" repository.
148
+ * @param {Array<{type: string, number: number}>} items - Items to chain.
149
+ * @returns {Promise<Array<{type: string, number: number, title: string, body: string, comments: Array<object>, trace: object|null}>>}
150
+ */
151
+ export async function chain(hub, repo, items) {
152
+ const results = [];
153
+
154
+ for (const item of items) {
155
+ try {
156
+ if (item.type === 'pr') {
157
+ const pull = await hub.pull(repo, item.number);
158
+ const comments = await hub.comments(repo, item.number);
159
+ const reviews = await hub.reviews(repo, item.number);
160
+ const trace = parse(pull.body, 'trace');
161
+ results.push({
162
+ type: 'pr',
163
+ number: pull.number,
164
+ title: pull.title,
165
+ body: pull.body,
166
+ comments,
167
+ reviews,
168
+ trace
169
+ });
170
+ } else if (item.type === 'issue') {
171
+ const issue = await hub.issue(repo, item.number);
172
+ const comments = await hub.comments(repo, item.number);
173
+ results.push({
174
+ type: 'issue',
175
+ number: issue.number,
176
+ title: issue.title,
177
+ body: issue.body,
178
+ comments,
179
+ trace: null
180
+ });
181
+ } else if (item.type === 'discussion') {
182
+ const disc = await hub.discussion(repo, item.number);
183
+ const comments = await hub.discussionComments(disc.id);
184
+ const trace = parse(disc.body, 'trace');
185
+ results.push({
186
+ type: 'discussion',
187
+ number: disc.number,
188
+ title: disc.title,
189
+ body: disc.body,
190
+ comments,
191
+ trace
192
+ });
193
+ }
194
+ } catch { /* skip items that can't be resolved */ }
195
+ }
196
+
197
+ return results;
198
+ }