whitesmith 0.0.2 → 0.0.4

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 (50) hide show
  1. package/README.md +286 -88
  2. package/dist/cli.d.ts.map +1 -1
  3. package/dist/cli.js +90 -2
  4. package/dist/cli.js.map +1 -1
  5. package/dist/comment.d.ts.map +1 -1
  6. package/dist/comment.js +18 -11
  7. package/dist/comment.js.map +1 -1
  8. package/dist/git.d.ts +5 -3
  9. package/dist/git.d.ts.map +1 -1
  10. package/dist/git.js +20 -29
  11. package/dist/git.js.map +1 -1
  12. package/dist/harnesses/pi.d.ts.map +1 -1
  13. package/dist/harnesses/pi.js +22 -6
  14. package/dist/harnesses/pi.js.map +1 -1
  15. package/dist/index.d.ts +3 -1
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +2 -1
  18. package/dist/index.js.map +1 -1
  19. package/dist/orchestrator.d.ts +31 -3
  20. package/dist/orchestrator.d.ts.map +1 -1
  21. package/dist/orchestrator.js +214 -10
  22. package/dist/orchestrator.js.map +1 -1
  23. package/dist/prompts.d.ts +52 -0
  24. package/dist/prompts.d.ts.map +1 -1
  25. package/dist/prompts.js +197 -0
  26. package/dist/prompts.js.map +1 -1
  27. package/dist/providers/github-ci.d.ts +40 -0
  28. package/dist/providers/github-ci.d.ts.map +1 -1
  29. package/dist/providers/github-ci.js +463 -213
  30. package/dist/providers/github-ci.js.map +1 -1
  31. package/dist/providers/index.d.ts +1 -1
  32. package/dist/providers/index.d.ts.map +1 -1
  33. package/dist/review.d.ts +48 -0
  34. package/dist/review.d.ts.map +1 -0
  35. package/dist/review.js +221 -0
  36. package/dist/review.js.map +1 -0
  37. package/dist/types.d.ts +4 -0
  38. package/dist/types.d.ts.map +1 -1
  39. package/package.json +1 -1
  40. package/src/cli.ts +116 -3
  41. package/src/comment.ts +20 -14
  42. package/src/git.ts +23 -30
  43. package/src/harnesses/pi.ts +27 -6
  44. package/src/index.ts +9 -1
  45. package/src/orchestrator.ts +253 -14
  46. package/src/prompts.ts +239 -0
  47. package/src/providers/github-ci.ts +513 -217
  48. package/src/providers/index.ts +1 -1
  49. package/src/review.ts +290 -0
  50. package/src/types.ts +4 -0
package/src/git.ts CHANGED
@@ -13,6 +13,7 @@ export class GitManager {
13
13
 
14
14
  constructor(workDir: string) {
15
15
  this.workDir = workDir;
16
+ this.ensureExcluded();
16
17
  }
17
18
 
18
19
  private async git(args: string): Promise<string> {
@@ -28,17 +29,28 @@ export class GitManager {
28
29
  }
29
30
 
30
31
  /**
31
- * Remove all .whitesmith-* temp files from the working directory.
32
+ * Ensure .whitesmith-* is excluded from git via .git/info/exclude.
33
+ * This prevents the agent from accidentally committing temp files
34
+ * without requiring changes to the user's .gitignore.
32
35
  */
33
- cleanupTempFiles(): void {
34
- for (const entry of fs.readdirSync(this.workDir)) {
35
- if (entry.startsWith('.whitesmith-')) {
36
- try {
37
- fs.unlinkSync(path.join(this.workDir, entry));
38
- } catch {
39
- // ignore
40
- }
41
- }
36
+ private ensureExcluded(): void {
37
+ const gitDir = path.join(this.workDir, '.git');
38
+ if (!fs.existsSync(gitDir)) return;
39
+
40
+ const excludeDir = path.join(gitDir, 'info');
41
+ const excludeFile = path.join(excludeDir, 'exclude');
42
+ const pattern = '.whitesmith-*';
43
+
44
+ fs.mkdirSync(excludeDir, {recursive: true});
45
+
46
+ let content = '';
47
+ if (fs.existsSync(excludeFile)) {
48
+ content = fs.readFileSync(excludeFile, 'utf-8');
49
+ }
50
+
51
+ if (!content.split('\n').some((line) => line.trim() === pattern)) {
52
+ const separator = content.length > 0 && !content.endsWith('\n') ? '\n' : '';
53
+ fs.appendFileSync(excludeFile, `${separator}${pattern}\n`);
42
54
  }
43
55
  }
44
56
 
@@ -72,31 +84,12 @@ export class GitManager {
72
84
  /**
73
85
  * Stage all changes and commit
74
86
  */
75
- async commitAll(message: string, exclude?: string[]): Promise<boolean> {
76
- // Always exclude whitesmith temp files
77
- const allExclude = ['.whitesmith-*', ...(exclude || [])];
78
-
79
- // Remove any whitesmith temp files from the working tree
80
- this.cleanupTempFiles();
81
-
87
+ async commitAll(message: string): Promise<boolean> {
82
88
  // Check if there are changes
83
89
  const status = await this.git('status --porcelain');
84
90
  if (!status) return false;
85
91
 
86
- // Add all then unstage excluded patterns
87
92
  await this.git('add -A');
88
- for (const pattern of allExclude) {
89
- try {
90
- await this.git(`reset HEAD -- ${pattern}`);
91
- } catch {
92
- // File might not be staged
93
- }
94
- }
95
-
96
- // Check if anything is still staged after exclusions
97
- const staged = await this.git('diff --cached --name-only');
98
- if (!staged) return false;
99
-
100
93
  await this.git(`commit -m "${message.replace(/"/g, '\\"')}"`);
101
94
  return true;
102
95
  }
@@ -49,10 +49,29 @@ export class PiHarness implements AgentHarness {
49
49
  );
50
50
  }
51
51
 
52
- // Check auth.json exists and has the expected provider
52
+ // Check for auth configuration (auth.json or models.json)
53
53
  const homeDir = process.env.HOME || homedir();
54
54
  const authJsonPath = path.join(homeDir, '.pi', 'agent', 'auth.json');
55
- if (fs.existsSync(authJsonPath)) {
55
+ const modelsJsonPath = path.join(homeDir, '.pi', 'agent', 'models.json');
56
+ const hasAuthJson = fs.existsSync(authJsonPath);
57
+ const hasModelsJson = fs.existsSync(modelsJsonPath);
58
+
59
+ if (hasModelsJson) {
60
+ try {
61
+ const modelsData = JSON.parse(fs.readFileSync(modelsJsonPath, 'utf-8'));
62
+ const providers = Object.keys(modelsData.providers || {});
63
+ console.log(
64
+ `Models config found at ${modelsJsonPath} with providers: ${providers.join(', ')}`,
65
+ );
66
+ if (!modelsData.providers?.[this.provider]) {
67
+ console.warn(
68
+ `WARNING: Provider '${this.provider}' not found in models.json (has: ${providers.join(', ')})`,
69
+ );
70
+ }
71
+ } catch (e: any) {
72
+ console.warn(`WARNING: Could not parse models.json: ${e.message}`);
73
+ }
74
+ } else if (hasAuthJson) {
56
75
  try {
57
76
  const authData = JSON.parse(fs.readFileSync(authJsonPath, 'utf-8'));
58
77
  const providers = Object.keys(authData);
@@ -66,7 +85,9 @@ export class PiHarness implements AgentHarness {
66
85
  console.warn(`WARNING: Could not parse auth.json: ${e.message}`);
67
86
  }
68
87
  } else {
69
- console.warn(`WARNING: No auth.json found at ${authJsonPath}`);
88
+ console.warn(
89
+ `WARNING: No auth configuration found (checked ${modelsJsonPath} and ${authJsonPath})`,
90
+ );
70
91
  }
71
92
 
72
93
  // Validate auth by making a minimal API call
@@ -87,9 +108,9 @@ export class PiHarness implements AgentHarness {
87
108
  [stderr, stdout].filter(Boolean).join('\n') || error.message || 'unknown error';
88
109
  throw new Error(
89
110
  `Agent auth validation failed. Ensure valid credentials are configured.\n` +
90
- `Set ANTHROPIC_API_KEY or configure OAuth via ~/.pi/agent/auth.json\n` +
91
- `Auth file path: ${authJsonPath}\n` +
92
- `Auth file exists: ${fs.existsSync(authJsonPath)}\n` +
111
+ `Configure providers via ~/.pi/agent/models.json or ~/.pi/agent/auth.json\n` +
112
+ `models.json exists: ${hasModelsJson}\n` +
113
+ `auth.json exists: ${hasAuthJson}\n` +
93
114
  `HOME: ${homeDir}\n` +
94
115
  `Details: ${details.slice(0, 800)}`,
95
116
  );
package/src/index.ts CHANGED
@@ -1,7 +1,15 @@
1
1
  export {Orchestrator} from './orchestrator.js';
2
2
  export {TaskManager} from './task-manager.js';
3
3
  export {GitManager} from './git.js';
4
- export {buildInvestigatePrompt, buildImplementPrompt} from './prompts.js';
4
+ export {
5
+ buildInvestigatePrompt,
6
+ buildImplementPrompt,
7
+ buildReviewTaskProposalPrompt,
8
+ buildReviewImplementationPRPrompt,
9
+ buildReviewTaskCompletionPrompt,
10
+ } from './prompts.js';
11
+ export {performReview, detectReviewTarget, parseReviewVerdict} from './review.js';
12
+ export type {ReviewConfig, ReviewTarget, ReviewResult, ReviewVerdict} from './review.js';
5
13
 
6
14
  export type {IssueProvider} from './providers/issue-provider.js';
7
15
  export {GitHubProvider} from './providers/github.js';
@@ -6,6 +6,8 @@ import {TaskManager} from './task-manager.js';
6
6
  import {GitManager} from './git.js';
7
7
  import {buildInvestigatePrompt, buildImplementPrompt} from './prompts.js';
8
8
  import {isAutoWorkEnabled} from './auto-work.js';
9
+ import {performReview} from './review.js';
10
+ import type {ReviewResult} from './review.js';
9
11
 
10
12
  /**
11
13
  * Main orchestrator for whitesmith.
@@ -44,6 +46,9 @@ export class Orchestrator {
44
46
  console.log(`Agent command: ${this.config.agentCmd}`);
45
47
  console.log(`Provider: ${this.config.provider}`);
46
48
  console.log(`Model: ${this.config.model}`);
49
+ if (this.config.issueNumber !== undefined) {
50
+ console.log(`Target issue: #${this.config.issueNumber}`);
51
+ }
47
52
  console.log('');
48
53
 
49
54
  // Skip agent validation and label creation in dry-run mode
@@ -57,6 +62,12 @@ export class Orchestrator {
57
62
  await this.issues.ensureLabels(Object.values(LABELS));
58
63
  }
59
64
 
65
+ // Delegate to single-issue mode if --issue is set
66
+ if (this.config.issueNumber !== undefined) {
67
+ await this.runForIssue(this.config.issueNumber);
68
+ return;
69
+ }
70
+
60
71
  for (let i = 1; i <= this.config.maxIterations; i++) {
61
72
  console.log('');
62
73
  console.log(`=== Iteration ${i}/${this.config.maxIterations} ===`);
@@ -122,6 +133,146 @@ export class Orchestrator {
122
133
  console.log('=== Iteration limit reached ===');
123
134
  }
124
135
 
136
+ /**
137
+ * Run the full pipeline for a single issue.
138
+ *
139
+ * Re-fetches the issue after each action to get updated labels, then decides
140
+ * the next action based on the current state. Loops until idle or the
141
+ * iteration limit is reached.
142
+ */
143
+ private async runForIssue(issueNumber: number): Promise<void> {
144
+ console.log(`Running single-issue mode for issue #${issueNumber}`);
145
+
146
+ for (let i = 1; i <= this.config.maxIterations; i++) {
147
+ console.log('');
148
+ console.log(`=== Iteration ${i}/${this.config.maxIterations} ===`);
149
+
150
+ // Make sure we're on main with latest
151
+ await this.git.fetch();
152
+ await this.git.checkoutMain();
153
+
154
+ // Re-fetch the issue to get current labels
155
+ const issue = await this.issues.getIssue(issueNumber);
156
+
157
+ // Determine the action based on the issue's current state
158
+ const action = await this.decideActionForIssue(issue);
159
+ console.log(`Action: ${action.type}`);
160
+
161
+ if (this.config.dryRun) {
162
+ switch (action.type) {
163
+ case 'reconcile':
164
+ console.log(`Would reconcile issue #${action.issue.number}: ${action.issue.title}`);
165
+ break;
166
+ case 'auto-approve':
167
+ console.log(
168
+ `Would auto-approve task PR for issue #${action.issue.number}: ${action.issue.title}`,
169
+ );
170
+ break;
171
+ case 'investigate':
172
+ console.log(`Would investigate issue #${action.issue.number}: ${action.issue.title}`);
173
+ break;
174
+ case 'implement':
175
+ console.log(
176
+ `Would implement task ${action.task.id}: ${action.task.title} (issue #${action.issue.number})`,
177
+ );
178
+ break;
179
+ case 'idle':
180
+ console.log('Nothing to do. Issue is either completed or no actions are applicable.');
181
+ break;
182
+ }
183
+ return;
184
+ }
185
+
186
+ switch (action.type) {
187
+ case 'reconcile':
188
+ await this.reconcile(action.issue);
189
+ break;
190
+ case 'auto-approve':
191
+ await this.autoApprove(action.issue);
192
+ break;
193
+ case 'investigate':
194
+ await this.investigate(action.issue);
195
+ break;
196
+ case 'implement':
197
+ await this.implement(action.task, action.issue);
198
+ break;
199
+ case 'idle':
200
+ console.log('Nothing to do. Issue is either completed or no actions are applicable.');
201
+ return;
202
+ }
203
+
204
+ if (!this.config.noSleep && i < this.config.maxIterations) {
205
+ console.log('Sleeping 5s...');
206
+ await new Promise((r) => setTimeout(r, 5000));
207
+ }
208
+ }
209
+
210
+ console.log('');
211
+ console.log('=== Iteration limit reached ===');
212
+ }
213
+
214
+ /**
215
+ * Decide the next action for a single issue based on its current labels.
216
+ */
217
+ private async decideActionForIssue(issue: Issue): Promise<Action> {
218
+ const labels = issue.labels;
219
+
220
+ // Handle stale investigating label (crashed previous run)
221
+ if (labels.includes(LABELS.INVESTIGATING)) {
222
+ console.log(`Issue #${issue.number} has stale '${LABELS.INVESTIGATING}' label, clearing it`);
223
+ await this.issues.removeLabel(issue.number, LABELS.INVESTIGATING);
224
+ // Treat as uninvestigated — re-investigate
225
+ return {type: 'investigate', issue};
226
+ }
227
+
228
+ // tasks-accepted: check if all tasks are done (reconcile) or implement next task
229
+ if (labels.includes(LABELS.TASKS_ACCEPTED)) {
230
+ const allDone = await this.allTasksCompletedOnBranch(issue.number);
231
+ if (allDone) {
232
+ return {type: 'reconcile', issue};
233
+ }
234
+
235
+ // Find an available task to implement
236
+ const implementAction = await this.findAvailableTask([issue]);
237
+ if (implementAction) {
238
+ return implementAction;
239
+ }
240
+
241
+ return {type: 'idle'};
242
+ }
243
+
244
+ // tasks-proposed: check if PR was already merged (tasks on main) → transition inline
245
+ if (labels.includes(LABELS.TASKS_PROPOSED)) {
246
+ if (this.tasks.hasRemainingTasks(issue.number)) {
247
+ // Tasks exist on main = investigate PR was merged
248
+ console.log(`Issue #${issue.number}: tasks PR merged, transitioning to tasks-accepted`);
249
+ await this.issues.removeLabel(issue.number, LABELS.TASKS_PROPOSED);
250
+ await this.issues.addLabel(issue.number, LABELS.TASKS_ACCEPTED);
251
+ // Find an available task to implement
252
+ const implementAction = await this.findAvailableTask([issue]);
253
+ if (implementAction) {
254
+ return implementAction;
255
+ }
256
+ return {type: 'idle'};
257
+ }
258
+
259
+ // PR not yet merged — auto-approve if auto-work is enabled
260
+ if (isAutoWorkEnabled(this.config, issue)) {
261
+ return {type: 'auto-approve', issue};
262
+ }
263
+
264
+ return {type: 'idle'};
265
+ }
266
+
267
+ // completed: nothing to do
268
+ if (labels.includes(LABELS.COMPLETED)) {
269
+ return {type: 'idle'};
270
+ }
271
+
272
+ // No whitesmith labels — investigate
273
+ return {type: 'investigate', issue};
274
+ }
275
+
125
276
  /**
126
277
  * Check whether all tasks for an issue have been completed on the issue branch.
127
278
  * Works without checking out the branch by inspecting the remote via git ls-tree.
@@ -141,11 +292,19 @@ export class Orchestrator {
141
292
  */
142
293
  private async decideAction(): Promise<Action> {
143
294
  // Priority 1: Reconcile — issues with tasks-accepted where all tasks are done
295
+ // but no PR exists yet (safety net for crash recovery).
144
296
  const acceptedIssues = await this.issues.listIssues({labels: [LABELS.TASKS_ACCEPTED]});
145
297
  for (const issue of acceptedIssues) {
146
298
  const allDone = await this.allTasksCompletedOnBranch(issue.number);
147
299
  if (allDone) {
148
- return {type: 'reconcile', issue};
300
+ // Only reconcile if no PR exists yet — if a PR is already open,
301
+ // the issue is waiting for merge and there's nothing more to do.
302
+ const branch = `issue/${issue.number}`;
303
+ const existingPR = await this.issues.getPRForBranch(branch);
304
+ if (!existingPR || existingPR.state === 'closed') {
305
+ return {type: 'reconcile', issue};
306
+ }
307
+ // PR exists and is open or merged — skip
149
308
  }
150
309
  }
151
310
 
@@ -226,13 +385,16 @@ export class Orchestrator {
226
385
  }
227
386
 
228
387
  /**
229
- * Phase 1: Reconcile — mark issue as completed, close it.
230
- * Also serves as safety net: creates PR if all tasks are done but no PR exists
388
+ * Phase 1: Reconcile — safety net for crash recovery.
389
+ * Creates PR if all tasks are done on the issue branch but no PR exists
231
390
  * (e.g. agent crashed after last task push but before PR creation).
391
+ *
392
+ * Does NOT close the issue — that happens when the PR is merged and the
393
+ * CLI `reconcile` command detects that task files are gone from main.
232
394
  */
233
395
  private async reconcile(issue: Issue): Promise<void> {
234
396
  console.log(`Reconciling issue #${issue.number}: ${issue.title}`);
235
- console.log('All tasks completed. Marking issue as done.');
397
+ console.log('All tasks completed on branch. Ensuring PR exists.');
236
398
 
237
399
  // Safety net: ensure a PR exists for the issue branch
238
400
  const branch = `issue/${issue.number}`;
@@ -253,22 +415,17 @@ export class Orchestrator {
253
415
  body: `## Implementation for #${issue.number}\n\n${taskSummary}\n\n---\n*Implemented by whitesmith*\n\nCloses #${issue.number}`,
254
416
  });
255
417
  console.log(`Safety net PR created: ${prUrl}`);
418
+ } else {
419
+ console.log(`PR already exists: ${existingPR.url}`);
256
420
  }
257
421
  }
258
422
 
259
- await this.issues.addLabel(issue.number, LABELS.COMPLETED);
260
- await this.issues.removeLabel(issue.number, LABELS.TASKS_ACCEPTED);
261
- await this.issues.comment(
262
- issue.number,
263
- `✅ All tasks for this issue have been implemented and merged. Closing.`,
264
- );
265
- await this.issues.closeIssue(issue.number);
266
-
267
- console.log(`Issue #${issue.number} closed.`);
423
+ console.log(`Issue #${issue.number} awaiting PR merge.`);
268
424
  }
269
425
 
270
426
  /**
271
- * Phase 1.5: Auto-approve — merge the task-proposal PR when auto-work is enabled
427
+ * Phase 1.5: Auto-approve — merge the task-proposal PR when auto-work is enabled.
428
+ * When review is enabled, runs a review first and only merges if approved.
272
429
  */
273
430
  private async autoApprove(issue: Issue): Promise<void> {
274
431
  console.log(`Auto-approving task PR for issue #${issue.number}: ${issue.title}`);
@@ -281,6 +438,27 @@ export class Orchestrator {
281
438
  return;
282
439
  }
283
440
 
441
+ // Run review before merging (if review is enabled)
442
+ if (this.config.review) {
443
+ let reviewResult: ReviewResult | null = null;
444
+ try {
445
+ reviewResult = await this.reviewTaskProposal(issue.number);
446
+ } catch (error) {
447
+ console.error('Review failed:', error instanceof Error ? error.message : error);
448
+ }
449
+
450
+ if (reviewResult && reviewResult.verdict === 'request_changes') {
451
+ console.log(`Review requested changes for task PR #${pr.number}. Skipping auto-merge.`);
452
+ await this.issues.comment(
453
+ issue.number,
454
+ `🔍 Review of task PR #${pr.number} requested changes. Auto-merge skipped — please review manually.`,
455
+ );
456
+ // Remove tasks-proposed so auto-approve doesn't retry every iteration
457
+ await this.issues.removeLabel(issue.number, LABELS.TASKS_PROPOSED);
458
+ return;
459
+ }
460
+ }
461
+
284
462
  await this.issues.mergePR(pr.number);
285
463
  console.log(`Merged PR #${pr.number}: ${pr.url}`);
286
464
 
@@ -396,6 +574,17 @@ export class Orchestrator {
396
574
  );
397
575
 
398
576
  console.log(`PR created: ${prUrl}`);
577
+
578
+ // Queue review of the task proposal (skip if auto-work — auto-approve will review)
579
+ if (this.config.review && !isAutoWorkEnabled(this.config, issue)) {
580
+ await this.git.checkoutMain();
581
+ try {
582
+ await this.reviewTaskProposal(issue.number);
583
+ } catch (error) {
584
+ console.error('Review failed:', error instanceof Error ? error.message : error);
585
+ }
586
+ return; // Already on main after review
587
+ }
399
588
  }
400
589
  } catch (error) {
401
590
  console.error('Investigation failed:', error instanceof Error ? error.message : error);
@@ -496,6 +685,16 @@ export class Orchestrator {
496
685
  }
497
686
 
498
687
  console.log(`PR created: ${prUrl}`);
688
+ // Queue review of the implementation PR
689
+ if (this.config.review) {
690
+ await this.git.checkoutMain();
691
+ try {
692
+ await this.reviewImplementationPR(issue.number);
693
+ } catch (error) {
694
+ console.error('Review failed:', error instanceof Error ? error.message : error);
695
+ }
696
+ return; // Already on main after review
697
+ }
499
698
  } else {
500
699
  console.log(
501
700
  `Task ${task.id} committed. ${remainingTasks.length} task(s) remaining for issue #${issue.number}.`,
@@ -509,4 +708,44 @@ export class Orchestrator {
509
708
  // Return to main
510
709
  await this.git.checkoutMain();
511
710
  }
711
+
712
+ /**
713
+ * Review a task proposal (investigate PR).
714
+ * Posts the review as a comment on the task proposal PR.
715
+ * Returns the review result so callers can check the verdict.
716
+ */
717
+ private async reviewTaskProposal(issueNumber: number): Promise<ReviewResult> {
718
+ console.log(`Reviewing task proposal for issue #${issueNumber}...`);
719
+ return performReview(
720
+ {type: 'issue-tasks', issueNumber},
721
+ {
722
+ workDir: this.config.workDir,
723
+ repo: this.config.repo,
724
+ logFile: this.config.logFile,
725
+ post: !this.config.noPush,
726
+ },
727
+ this.issues,
728
+ this.agent,
729
+ );
730
+ }
731
+
732
+ /**
733
+ * Review an implementation PR (all tasks completed for an issue).
734
+ * Posts the review as a comment on the implementation PR.
735
+ * Returns the review result so callers can check the verdict.
736
+ */
737
+ private async reviewImplementationPR(issueNumber: number): Promise<ReviewResult> {
738
+ console.log(`Reviewing implementation for issue #${issueNumber}...`);
739
+ return performReview(
740
+ {type: 'issue-tasks-completed', issueNumber},
741
+ {
742
+ workDir: this.config.workDir,
743
+ repo: this.config.repo,
744
+ logFile: this.config.logFile,
745
+ post: !this.config.noPush,
746
+ },
747
+ this.issues,
748
+ this.agent,
749
+ );
750
+ }
512
751
  }