devharness 0.9.4 → 0.9.6

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 (105) hide show
  1. package/build/command-recorder.d.ts +24 -0
  2. package/build/command-recorder.d.ts.map +1 -1
  3. package/build/command-recorder.js +55 -8
  4. package/build/command-recorder.js.map +1 -1
  5. package/build/config.d.ts +9 -0
  6. package/build/config.d.ts.map +1 -1
  7. package/build/config.js +10 -0
  8. package/build/config.js.map +1 -1
  9. package/build/dashboard/dashboard-hub.js +2 -2
  10. package/build/dashboard/dashboard-hub.js.map +1 -1
  11. package/build/github/gh-cli.d.ts +41 -0
  12. package/build/github/gh-cli.d.ts.map +1 -0
  13. package/build/github/gh-cli.js +147 -0
  14. package/build/github/gh-cli.js.map +1 -0
  15. package/build/github/gh-cli.test.d.ts +2 -0
  16. package/build/github/gh-cli.test.d.ts.map +1 -0
  17. package/build/github/gh-cli.test.js +145 -0
  18. package/build/github/gh-cli.test.js.map +1 -0
  19. package/build/github/gh-issues.d.ts +105 -0
  20. package/build/github/gh-issues.d.ts.map +1 -0
  21. package/build/github/gh-issues.js +199 -0
  22. package/build/github/gh-issues.js.map +1 -0
  23. package/build/github/gh-issues.test.d.ts +2 -0
  24. package/build/github/gh-issues.test.d.ts.map +1 -0
  25. package/build/github/gh-issues.test.js +118 -0
  26. package/build/github/gh-issues.test.js.map +1 -0
  27. package/build/github/issue-actions.d.ts +34 -0
  28. package/build/github/issue-actions.d.ts.map +1 -0
  29. package/build/github/issue-actions.js +530 -0
  30. package/build/github/issue-actions.js.map +1 -0
  31. package/build/github/issue-actions.test.d.ts +2 -0
  32. package/build/github/issue-actions.test.d.ts.map +1 -0
  33. package/build/github/issue-actions.test.js +555 -0
  34. package/build/github/issue-actions.test.js.map +1 -0
  35. package/build/github/issue-body.d.ts +179 -0
  36. package/build/github/issue-body.d.ts.map +1 -0
  37. package/build/github/issue-body.js +181 -0
  38. package/build/github/issue-body.js.map +1 -0
  39. package/build/github/issue-body.test.d.ts +2 -0
  40. package/build/github/issue-body.test.d.ts.map +1 -0
  41. package/build/github/issue-body.test.js +157 -0
  42. package/build/github/issue-body.test.js.map +1 -0
  43. package/build/helpers/paths.d.ts +7 -0
  44. package/build/helpers/paths.d.ts.map +1 -1
  45. package/build/helpers/paths.js +10 -1
  46. package/build/helpers/paths.js.map +1 -1
  47. package/build/index.js +7 -1
  48. package/build/index.js.map +1 -1
  49. package/build/issue-tracker.d.ts +30 -0
  50. package/build/issue-tracker.d.ts.map +1 -1
  51. package/build/issue-tracker.js +91 -1
  52. package/build/issue-tracker.js.map +1 -1
  53. package/build/issue-tracker.test.js +76 -1
  54. package/build/issue-tracker.test.js.map +1 -1
  55. package/build/tool-response.d.ts +34 -0
  56. package/build/tool-response.d.ts.map +1 -1
  57. package/build/tool-response.js.map +1 -1
  58. package/build/tools/assert-tools.d.ts +6 -6
  59. package/build/tools/breakpoint-tools.d.ts +3 -3
  60. package/build/tools/click-validation-console-noise.test.d.ts +2 -0
  61. package/build/tools/click-validation-console-noise.test.d.ts.map +1 -0
  62. package/build/tools/click-validation-console-noise.test.js +103 -0
  63. package/build/tools/click-validation-console-noise.test.js.map +1 -0
  64. package/build/tools/console-tools.d.ts +6 -6
  65. package/build/tools/console-tools.d.ts.map +1 -1
  66. package/build/tools/console-tools.js +1 -0
  67. package/build/tools/console-tools.js.map +1 -1
  68. package/build/tools/execution-tools.d.ts +3 -3
  69. package/build/tools/input-tools.d.ts +3 -3
  70. package/build/tools/inspection-tools.d.ts +15 -15
  71. package/build/tools/interpolation-reverse.d.ts +26 -0
  72. package/build/tools/interpolation-reverse.d.ts.map +1 -0
  73. package/build/tools/interpolation-reverse.js +130 -0
  74. package/build/tools/interpolation-reverse.js.map +1 -0
  75. package/build/tools/interpolation-reverse.test.d.ts +2 -0
  76. package/build/tools/interpolation-reverse.test.d.ts.map +1 -0
  77. package/build/tools/interpolation-reverse.test.js +76 -0
  78. package/build/tools/interpolation-reverse.test.js.map +1 -0
  79. package/build/tools/issues-tools.d.ts +29 -5
  80. package/build/tools/issues-tools.d.ts.map +1 -1
  81. package/build/tools/issues-tools.js +55 -5
  82. package/build/tools/issues-tools.js.map +1 -1
  83. package/build/tools/network-tools.d.ts +9 -9
  84. package/build/tools/replay-create-templatize.test.d.ts +2 -0
  85. package/build/tools/replay-create-templatize.test.d.ts.map +1 -0
  86. package/build/tools/replay-create-templatize.test.js +103 -0
  87. package/build/tools/replay-create-templatize.test.js.map +1 -0
  88. package/build/tools/replay-executor.d.ts +4 -0
  89. package/build/tools/replay-executor.d.ts.map +1 -1
  90. package/build/tools/replay-executor.js +39 -10
  91. package/build/tools/replay-executor.js.map +1 -1
  92. package/build/tools/replay-sequence-reload.test.js +23 -7
  93. package/build/tools/replay-sequence-reload.test.js.map +1 -1
  94. package/build/tools/replay-tools.d.ts +18 -18
  95. package/build/tools/replay-tools.d.ts.map +1 -1
  96. package/build/tools/replay-tools.js +6 -5
  97. package/build/tools/replay-tools.js.map +1 -1
  98. package/build/tools/request-tools.d.ts +1 -1
  99. package/build/tools/storage-tools.d.ts +6 -6
  100. package/docs/instructions.md +3 -1
  101. package/docs/messages.md +452 -1
  102. package/package.json +1 -1
  103. package/plugin/skills/devharness/SKILL.md +1 -1
  104. package/plugin/skills/devharness/references/sequences.md +6 -1
  105. package/plugin/skills/devharness/references/tool-categories.md +8 -1
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gh-cli.d.ts","sourceRoot":"","sources":["../../src/github/gh-cli.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAsB,KAAK,YAAY,EAAE,KAAK,YAAY,EAAE,MAAM,eAAe,CAAC;AAGzF,MAAM,MAAM,aAAa,GACrB,kBAAkB,GAClB,sBAAsB,GACtB,YAAY,GACZ,YAAY,GACZ,WAAW,CAAC;AAEhB,qBAAa,OAAQ,SAAQ,KAAK;IAG9B,QAAQ,CAAC,IAAI,EAAE,aAAa;IAE5B,QAAQ,CAAC,MAAM,EAAE,MAAM;IACvB,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE;IALzB,QAAQ,CAAC,IAAI,aAAa;gBAEf,IAAI,EAAE,aAAa,EAC5B,OAAO,EAAE,MAAM,EACN,MAAM,GAAE,MAAW,EACnB,IAAI,GAAE,MAAM,EAAO;CAI/B;AAED,eAAO,MAAM,qBAAqB,QAAS,CAAC;AAK5C,KAAK,OAAO,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,OAAO,EAAE,YAAY,KAAK,YAAY,CAAC;AAIxF,uDAAuD;AACvD,wBAAgB,kBAAkB,CAAC,EAAE,EAAE,OAAO,GAAG,IAAI,GAAG,IAAI,CAE3D;AAED,MAAM,WAAW,YAAY;IAC3B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,iEAAiE;IACjE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAYD;;;;;;;GAOG;AACH,wBAAgB,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,IAAI,GAAE,YAAiB,GAAG,OAAO,CAAC,MAAM,CAAC,CA0F9E;AAED,wBAAsB,SAAS,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,IAAI,GAAE,YAAiB,GAAG,OAAO,CAAC,CAAC,CAAC,CAOtF;AAED,kDAAkD;AAClD,wBAAgB,QAAQ,CAAC,GAAG,EAAE,OAAO,EAAE,KAAK,SAAM,GAAG,MAAM,CAG1D"}
@@ -0,0 +1,147 @@
1
+ /**
2
+ * The `gh` CLI as a bounded subprocess.
3
+ *
4
+ * Every call is time-boxed and every failure is a typed code. Bug 003 is the
5
+ * precedent: an unbounded wait blocked an autonomous agent for 25 minutes and
6
+ * destroyed its report. A clear immediate error is fine; a hang is not.
7
+ */
8
+ import { spawn as nodeSpawn } from 'child_process';
9
+ import { getProjectDir } from '../helpers/paths.js';
10
+ export class GhError extends Error {
11
+ code;
12
+ stderr;
13
+ args;
14
+ name = 'GhError';
15
+ constructor(code, message, stderr = '', args = []) {
16
+ super(message);
17
+ this.code = code;
18
+ this.stderr = stderr;
19
+ this.args = args;
20
+ }
21
+ }
22
+ export const DEFAULT_GH_TIMEOUT_MS = 20_000;
23
+ /** Grace between SIGTERM and SIGKILL. The promise has already rejected. */
24
+ const KILL_GRACE_MS = 2_000;
25
+ const MAX_OUTPUT_BYTES = 4 * 1024 * 1024;
26
+ let spawnImpl = nodeSpawn;
27
+ /** Test seam - pass null to restore the real spawn. */
28
+ export function setGhSpawnForTests(fn) {
29
+ spawnImpl = fn ?? nodeSpawn;
30
+ }
31
+ function classify(stderr, args) {
32
+ if (/gh auth login|not logged (in|into)|authentication token|HTTP 401/i.test(stderr)) {
33
+ return new GhError('GH_NOT_AUTHENTICATED', 'Not authenticated with GitHub', stderr, args);
34
+ }
35
+ if (/none of the git remotes|not a git repository|could not determine.*repository|no git remotes/i.test(stderr)) {
36
+ return new GhError('GH_NO_REPO', 'No GitHub repository for this directory', stderr, args);
37
+ }
38
+ return new GhError('GH_FAILED', `gh ${args[0] ?? ''} failed`, stderr, args);
39
+ }
40
+ /**
41
+ * Run `gh` and resolve its stdout.
42
+ *
43
+ * stdin is closed immediately and unconditionally: a `gh` that decides to
44
+ * prompt then reads EOF and exits, instead of waiting forever on a pipe
45
+ * nobody will write to. The env vars kill the other hang vectors - a pager
46
+ * attached to a pipe, and the update-check network call.
47
+ */
48
+ export function runGh(args, opts = {}) {
49
+ const timeoutMs = opts.timeoutMs ?? DEFAULT_GH_TIMEOUT_MS;
50
+ return new Promise((resolve, reject) => {
51
+ let child;
52
+ try {
53
+ child = spawnImpl('gh', args, {
54
+ cwd: opts.cwd ?? getProjectDir(),
55
+ stdio: ['pipe', 'pipe', 'pipe'],
56
+ env: {
57
+ ...process.env,
58
+ GH_PROMPT_DISABLED: '1',
59
+ GH_NO_UPDATE_NOTIFIER: '1',
60
+ GH_PAGER: 'cat',
61
+ PAGER: 'cat',
62
+ NO_COLOR: '1',
63
+ CLICOLOR: '0',
64
+ },
65
+ });
66
+ }
67
+ catch (err) {
68
+ reject(new GhError('GH_FAILED', `Could not start gh: ${err.message}`, '', args));
69
+ return;
70
+ }
71
+ let stdout = '';
72
+ let stderr = '';
73
+ let settled = false;
74
+ let killTimer;
75
+ const cleanup = () => {
76
+ clearTimeout(timer);
77
+ if (killTimer)
78
+ clearTimeout(killTimer);
79
+ opts.signal?.removeEventListener('abort', onAbort);
80
+ };
81
+ /** Reject now; escalate to SIGKILL in the background. We never wait on it. */
82
+ const abandon = (err) => {
83
+ if (settled)
84
+ return;
85
+ settled = true;
86
+ cleanup();
87
+ child.kill('SIGTERM');
88
+ killTimer = setTimeout(() => child.kill('SIGKILL'), KILL_GRACE_MS);
89
+ killTimer.unref?.();
90
+ reject(err);
91
+ };
92
+ const timer = setTimeout(() => {
93
+ abandon(new GhError('GH_TIMEOUT', `gh ${args[0] ?? ''} exceeded ${Math.round(timeoutMs / 1000)}s and was cancelled`, stderr, args));
94
+ }, timeoutMs);
95
+ const onAbort = () => abandon(new GhError('GH_FAILED', 'Cancelled', stderr, args));
96
+ opts.signal?.addEventListener('abort', onAbort, { once: true });
97
+ // Before any await point, so a prompting gh always sees EOF.
98
+ if (opts.stdin)
99
+ child.stdin?.write(opts.stdin);
100
+ child.stdin?.end();
101
+ child.stdout?.on('data', (chunk) => {
102
+ stdout += chunk.toString();
103
+ if (stdout.length > MAX_OUTPUT_BYTES) {
104
+ abandon(new GhError('GH_FAILED', 'gh produced more output than expected', stderr, args));
105
+ }
106
+ });
107
+ child.stderr?.on('data', (chunk) => {
108
+ stderr += chunk.toString();
109
+ if (stderr.length > MAX_OUTPUT_BYTES)
110
+ stderr = stderr.slice(0, MAX_OUTPUT_BYTES);
111
+ });
112
+ child.on('error', (err) => {
113
+ if (settled)
114
+ return;
115
+ settled = true;
116
+ cleanup();
117
+ reject(err.code === 'ENOENT'
118
+ ? new GhError('GH_NOT_INSTALLED', 'The gh CLI is not installed or not on PATH', '', args)
119
+ : new GhError('GH_FAILED', `Could not run gh: ${err.message}`, stderr, args));
120
+ });
121
+ child.on('close', (code) => {
122
+ if (settled)
123
+ return;
124
+ settled = true;
125
+ cleanup();
126
+ if (code === 0)
127
+ resolve(stdout);
128
+ else
129
+ reject(classify(stderr, args));
130
+ });
131
+ });
132
+ }
133
+ export async function runGhJson(args, opts = {}) {
134
+ const stdout = await runGh(args, opts);
135
+ try {
136
+ return JSON.parse(stdout);
137
+ }
138
+ catch {
139
+ throw new GhError('GH_FAILED', `gh ${args[0] ?? ''} returned output that is not JSON`, stdout.slice(0, 500), args);
140
+ }
141
+ }
142
+ /** Truncated stderr for a user-facing message. */
143
+ export function ghDetail(err, limit = 500) {
144
+ const text = (err.stderr || err.message).trim();
145
+ return text.length > limit ? `${text.slice(0, limit)}…` : text;
146
+ }
147
+ //# sourceMappingURL=gh-cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gh-cli.js","sourceRoot":"","sources":["../../src/github/gh-cli.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=gh-cli.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gh-cli.test.d.ts","sourceRoot":"","sources":["../../src/github/gh-cli.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,145 @@
1
+ // @vitest-environment node
2
+ import { describe, it, expect, afterEach, vi } from 'vitest';
3
+ import { EventEmitter } from 'events';
4
+ import { promises as fsp } from 'fs';
5
+ import { tmpdir } from 'os';
6
+ import { join } from 'path';
7
+ import { runGh, runGhJson, setGhSpawnForTests, GhError, DEFAULT_GH_TIMEOUT_MS } from './gh-cli.js';
8
+ /** A child that behaves however the test needs, recording what was done to it. */
9
+ function fakeChild() {
10
+ const child = new EventEmitter();
11
+ child.stdout = new EventEmitter();
12
+ child.stderr = new EventEmitter();
13
+ child.stdinEnded = false;
14
+ child.written = '';
15
+ child.signals = [];
16
+ child.stdin = {
17
+ write: (s) => { child.written += s; },
18
+ end: () => { child.stdinEnded = true; },
19
+ };
20
+ child.kill = (sig) => { child.signals.push(sig); return true; };
21
+ return child;
22
+ }
23
+ function useChild(child, capture) {
24
+ setGhSpawnForTests(((_cmd, args, opts) => {
25
+ capture?.(args, opts);
26
+ return child;
27
+ }));
28
+ return child;
29
+ }
30
+ afterEach(() => {
31
+ setGhSpawnForTests(null);
32
+ vi.useRealTimers();
33
+ });
34
+ describe('runGh', () => {
35
+ it('resolves stdout on a clean exit', async () => {
36
+ const child = useChild(fakeChild());
37
+ const promise = runGh(['issue', 'list']);
38
+ child.stdout.emit('data', '[]');
39
+ child.emit('close', 0);
40
+ await expect(promise).resolves.toBe('[]');
41
+ });
42
+ it('closes stdin immediately, before anything can block', async () => {
43
+ const child = useChild(fakeChild());
44
+ const promise = runGh(['issue', 'create'], { stdin: 'body text' });
45
+ // Asserted before the process has exited: a prompting gh must already
46
+ // have seen EOF by now, not after we finish reading its output.
47
+ expect(child.stdinEnded).toBe(true);
48
+ expect(child.written).toBe('body text');
49
+ child.emit('close', 0);
50
+ await promise;
51
+ });
52
+ it('disables the pager and prompts, the two hang vectors', async () => {
53
+ let seen;
54
+ const child = useChild(fakeChild(), (_args, opts) => { seen = opts; });
55
+ const promise = runGh(['repo', 'view']);
56
+ expect(seen.env.GH_PROMPT_DISABLED).toBe('1');
57
+ expect(seen.env.GH_PAGER).toBe('cat');
58
+ expect(seen.env.GH_NO_UPDATE_NOTIFIER).toBe('1');
59
+ child.emit('close', 0);
60
+ await promise;
61
+ });
62
+ it('rejects with GH_TIMEOUT when the child never exits, then escalates to SIGKILL', async () => {
63
+ vi.useFakeTimers();
64
+ const child = useChild(fakeChild());
65
+ const promise = runGh(['issue', 'list']);
66
+ const assertion = expect(promise).rejects.toMatchObject({ code: 'GH_TIMEOUT' });
67
+ // The child emits nothing, ever - the bug-003 shape.
68
+ await vi.advanceTimersByTimeAsync(DEFAULT_GH_TIMEOUT_MS + 1);
69
+ await assertion;
70
+ expect(child.signals).toEqual(['SIGTERM']);
71
+ await vi.advanceTimersByTimeAsync(2_001);
72
+ expect(child.signals).toEqual(['SIGTERM', 'SIGKILL']);
73
+ });
74
+ it('reports GH_NOT_INSTALLED when gh is missing', async () => {
75
+ const child = useChild(fakeChild());
76
+ const promise = runGh(['issue', 'list']);
77
+ const err = Object.assign(new Error('spawn gh ENOENT'), { code: 'ENOENT' });
78
+ child.emit('error', err);
79
+ await expect(promise).rejects.toMatchObject({ code: 'GH_NOT_INSTALLED' });
80
+ });
81
+ it('reports GH_NOT_AUTHENTICATED from stderr', async () => {
82
+ const child = useChild(fakeChild());
83
+ const promise = runGh(['issue', 'list']);
84
+ child.stderr.emit('data', 'gh: To get started with GitHub CLI, please run: gh auth login\n');
85
+ child.emit('close', 4);
86
+ await expect(promise).rejects.toMatchObject({ code: 'GH_NOT_AUTHENTICATED' });
87
+ });
88
+ it('reports GH_NO_REPO when the directory has no GitHub remote', async () => {
89
+ const child = useChild(fakeChild());
90
+ const promise = runGh(['issue', 'list']);
91
+ child.stderr.emit('data', 'none of the git remotes configured for this repository point to a known GitHub host\n');
92
+ child.emit('close', 1);
93
+ await expect(promise).rejects.toMatchObject({ code: 'GH_NO_REPO' });
94
+ });
95
+ it('falls back to GH_FAILED and keeps stderr for the message', async () => {
96
+ const child = useChild(fakeChild());
97
+ const promise = runGh(['issue', 'view', '9999']);
98
+ child.stderr.emit('data', 'GraphQL: Could not resolve to an Issue with the number of 9999.\n');
99
+ child.emit('close', 1);
100
+ await expect(promise).rejects.toMatchObject({
101
+ code: 'GH_FAILED',
102
+ stderr: expect.stringContaining('Could not resolve'),
103
+ });
104
+ });
105
+ it('rejects rather than throwing when stdout is not JSON', async () => {
106
+ const child = useChild(fakeChild());
107
+ const promise = runGhJson(['issue', 'list']);
108
+ child.stdout.emit('data', 'not json at all');
109
+ child.emit('close', 0);
110
+ await expect(promise).rejects.toBeInstanceOf(GhError);
111
+ });
112
+ it('ignores a late close after the timeout already rejected', async () => {
113
+ vi.useFakeTimers();
114
+ const child = useChild(fakeChild());
115
+ const promise = runGh(['issue', 'list']);
116
+ const assertion = expect(promise).rejects.toMatchObject({ code: 'GH_TIMEOUT' });
117
+ await vi.advanceTimersByTimeAsync(DEFAULT_GH_TIMEOUT_MS + 1);
118
+ await assertion;
119
+ // A SIGTERMed gh still emits close; it must not produce an unhandled
120
+ // resolution of an already-rejected promise.
121
+ expect(() => child.emit('close', 0)).not.toThrow();
122
+ });
123
+ });
124
+ describe('runGh against a real process', () => {
125
+ // The injected tests prove classification; only this one proves the actual
126
+ // spawn path cannot hang.
127
+ it('kills a real gh that never exits', async () => {
128
+ const dir = await fsp.mkdtemp(join(tmpdir(), 'gh-stub-'));
129
+ const stub = join(dir, 'gh');
130
+ await fsp.writeFile(stub, '#!/bin/sh\nsleep 300\n', { mode: 0o755 });
131
+ const originalPath = process.env.PATH;
132
+ process.env.PATH = `${dir}:${originalPath}`;
133
+ try {
134
+ const started = Date.now();
135
+ await expect(runGh(['issue', 'list'], { timeoutMs: 750 }))
136
+ .rejects.toMatchObject({ code: 'GH_TIMEOUT' });
137
+ expect(Date.now() - started).toBeLessThan(5_000);
138
+ }
139
+ finally {
140
+ process.env.PATH = originalPath;
141
+ await fsp.rm(dir, { recursive: true, force: true });
142
+ }
143
+ }, 10_000);
144
+ });
145
+ //# sourceMappingURL=gh-cli.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gh-cli.test.js","sourceRoot":"","sources":["../../src/github/gh-cli.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Typed `gh` operations for issues, and the pure policy that decides what a
3
+ * sync should do. The policy is separated so the truth table is testable
4
+ * without a subprocess.
5
+ */
6
+ import { type RunGhOptions } from './gh-cli.js';
7
+ import { type IssueClosedReason, type IssueStatus, type TrackedIssue } from '../issue-tracker.js';
8
+ export interface RemoteComment {
9
+ id: string;
10
+ body: string;
11
+ author: string;
12
+ createdAt: string;
13
+ }
14
+ export interface RemoteIssue {
15
+ number: number;
16
+ title: string;
17
+ body: string;
18
+ author: string;
19
+ state: 'OPEN' | 'CLOSED';
20
+ stateReason?: string | null;
21
+ labels: string[];
22
+ updatedAt: string;
23
+ url: string;
24
+ comments: RemoteComment[];
25
+ }
26
+ /** The subset `gh issue list` returns - no body, no comments. */
27
+ export type RemoteIssueSummary = Omit<RemoteIssue, 'body' | 'title' | 'comments' | 'author'>;
28
+ export declare function ghRepoName(repo?: string, opts?: RunGhOptions): Promise<string>;
29
+ export declare function ghExistingLabels(repo?: string, opts?: RunGhOptions): Promise<Set<string>>;
30
+ export declare function ghCreateLabel(name: string, repo?: string, opts?: RunGhOptions): Promise<void>;
31
+ /** `gh issue create` has no --json; it prints the URL. */
32
+ export declare function ghCreateIssue(params: {
33
+ title: string;
34
+ body: string;
35
+ labels: string[];
36
+ repo?: string;
37
+ }, opts?: RunGhOptions): Promise<{
38
+ number: number;
39
+ url: string;
40
+ } | {
41
+ number: null;
42
+ url: string;
43
+ raw: string;
44
+ }>;
45
+ export declare function ghViewIssue(number: number, repo?: string, opts?: RunGhOptions): Promise<RemoteIssue>;
46
+ /** The login gh is authenticated as - the yardstick for "did someone else
47
+ * write this". */
48
+ export declare function ghCurrentUser(opts?: RunGhOptions): Promise<string>;
49
+ export declare function ghListIssues(repo?: string, opts?: RunGhOptions): Promise<RemoteIssueSummary[]>;
50
+ export declare function ghEditBody(number: number, body: string, repo?: string, opts?: RunGhOptions): Promise<void>;
51
+ export declare function ghAddComment(number: number, body: string, repo?: string, opts?: RunGhOptions): Promise<void>;
52
+ export declare function ghCloseIssue(number: number, reason: IssueClosedReason, repo?: string, opts?: RunGhOptions): Promise<void>;
53
+ export declare function markComment(id: string, body: string): string;
54
+ export declare function commentGithubId(text: string): string | null;
55
+ export declare function stripCommentMarker(text: string): string;
56
+ export declare function stripLocalMarker(body: string): string;
57
+ export declare function bodyHash(body: string): string;
58
+ export declare function closedReasonFrom(stateReason: string | null | undefined): IssueClosedReason;
59
+ export type SyncAction = {
60
+ kind: 'none';
61
+ } | {
62
+ kind: 'pull';
63
+ } | {
64
+ kind: 'push';
65
+ } | {
66
+ kind: 'conflict';
67
+ } | {
68
+ kind: 'missing-upstream';
69
+ };
70
+ export interface SyncClassification {
71
+ action: SyncAction['kind'];
72
+ localChanged: boolean;
73
+ remoteChanged: boolean;
74
+ /** First sync for this issue - adopt rather than claim a conflict. */
75
+ firstSync: boolean;
76
+ }
77
+ /**
78
+ * Which side moved since the two last agreed.
79
+ *
80
+ * With no baseline this is a first sync: adopt whichever side has content
81
+ * rather than reporting a conflict on an issue nobody has actually edited.
82
+ */
83
+ export declare function classifySync(local: Pick<TrackedIssue, 'body' | 'githubBodyHash' | 'githubSyncedAt'>, remote: {
84
+ updatedAt: string;
85
+ } | undefined, strippedLocalBody: string): SyncClassification;
86
+ export interface StatusResolution {
87
+ status?: IssueStatus;
88
+ closedReason?: IssueClosedReason;
89
+ resolvedAt?: Date;
90
+ /** Upstream is open while we think it is closed - close it up there. */
91
+ closeUpstream?: IssueClosedReason;
92
+ reopened?: boolean;
93
+ }
94
+ /**
95
+ * How local status and upstream state reconcile.
96
+ *
97
+ * devharness closes issues upstream but never reopens them; GitHub can do
98
+ * both locally. That asymmetry is what keeps an automated sync from undoing
99
+ * a human's decision on a public issue.
100
+ */
101
+ export declare function resolveStatus(local: Pick<TrackedIssue, 'type' | 'status' | 'closedReason' | 'startedAt' | 'resolvedAt'>, remote: Pick<RemoteIssueSummary, 'state' | 'stateReason'>, now: Date): StatusResolution;
102
+ /** Upstream labels are added, never removed - a replace would delete exactly
103
+ * the local-only tags that carry the triage meaning. */
104
+ export declare function unionLabels(local: string[], remote: string[]): string[];
105
+ //# sourceMappingURL=gh-issues.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gh-issues.d.ts","sourceRoot":"","sources":["../../src/github/gh-issues.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,199 @@
1
+ /**
2
+ * Typed `gh` operations for issues, and the pure policy that decides what a
3
+ * sync should do. The policy is separated so the truth table is testable
4
+ * without a subprocess.
5
+ */
6
+ import { createHash } from 'crypto';
7
+ import { runGh, runGhJson } from './gh-cli.js';
8
+ import { completedStatusFor, isCompletedStatus, } from '../issue-tracker.js';
9
+ function repoArgs(repo) {
10
+ return repo ? ['--repo', repo] : [];
11
+ }
12
+ function normaliseLabels(raw) {
13
+ if (!Array.isArray(raw))
14
+ return [];
15
+ return raw
16
+ .map(l => (typeof l === 'string' ? l : l?.name))
17
+ .filter((l) => typeof l === 'string' && l.length > 0);
18
+ }
19
+ export async function ghRepoName(repo, opts) {
20
+ if (repo)
21
+ return repo;
22
+ const result = await runGhJson(['repo', 'view', '--json', 'nameWithOwner'], opts);
23
+ return result.nameWithOwner;
24
+ }
25
+ export async function ghExistingLabels(repo, opts) {
26
+ const raw = await runGhJson(['label', 'list', '--limit', '200', '--json', 'name', ...repoArgs(repo)], opts);
27
+ return new Set(raw.map(l => l.name));
28
+ }
29
+ export async function ghCreateLabel(name, repo, opts) {
30
+ await runGh(['label', 'create', name, ...repoArgs(repo)], opts);
31
+ }
32
+ /** `gh issue create` has no --json; it prints the URL. */
33
+ export async function ghCreateIssue(params, opts) {
34
+ const args = ['issue', 'create', '--title', params.title, '--body-file', '-', ...repoArgs(params.repo)];
35
+ for (const label of params.labels)
36
+ args.push('--label', label);
37
+ const stdout = await runGh(args, { ...opts, stdin: params.body });
38
+ const match = stdout.match(/\/issues\/(\d+)\s*$/m);
39
+ const url = stdout.trim().split(/\s+/).pop() ?? stdout.trim();
40
+ // The issue exists either way - an unparseable number is a stamping
41
+ // problem, never a create failure.
42
+ return match ? { number: parseInt(match[1], 10), url } : { number: null, url, raw: stdout };
43
+ }
44
+ export async function ghViewIssue(number, repo, opts) {
45
+ const raw = await runGhJson([
46
+ 'issue', 'view', String(number),
47
+ '--json', 'number,title,body,author,state,stateReason,labels,updatedAt,url,comments',
48
+ ...repoArgs(repo),
49
+ ], opts);
50
+ return {
51
+ number: raw.number,
52
+ title: raw.title ?? '',
53
+ body: raw.body ?? '',
54
+ author: raw.author?.login ?? '',
55
+ state: raw.state === 'CLOSED' ? 'CLOSED' : 'OPEN',
56
+ stateReason: raw.stateReason ?? null,
57
+ labels: normaliseLabels(raw.labels),
58
+ updatedAt: raw.updatedAt,
59
+ url: raw.url,
60
+ comments: (raw.comments ?? []).map((c) => ({
61
+ id: String(c.id ?? c.url ?? ''),
62
+ body: c.body ?? '',
63
+ author: c.author?.login ?? '',
64
+ createdAt: c.createdAt ?? '',
65
+ })),
66
+ };
67
+ }
68
+ /** The login gh is authenticated as - the yardstick for "did someone else
69
+ * write this". */
70
+ export async function ghCurrentUser(opts) {
71
+ const out = await runGh(['api', 'user', '--jq', '.login'], opts);
72
+ return out.trim();
73
+ }
74
+ export async function ghListIssues(repo, opts) {
75
+ const raw = await runGhJson([
76
+ 'issue', 'list', '--state', 'all', '--limit', '200',
77
+ '--json', 'number,state,stateReason,labels,updatedAt,url',
78
+ ...repoArgs(repo),
79
+ ], opts);
80
+ return raw.map(r => ({
81
+ number: r.number,
82
+ state: r.state === 'CLOSED' ? 'CLOSED' : 'OPEN',
83
+ stateReason: r.stateReason ?? null,
84
+ labels: normaliseLabels(r.labels),
85
+ updatedAt: r.updatedAt,
86
+ url: r.url,
87
+ }));
88
+ }
89
+ export async function ghEditBody(number, body, repo, opts) {
90
+ await runGh(['issue', 'edit', String(number), '--body-file', '-', ...repoArgs(repo)], { ...opts, stdin: body });
91
+ }
92
+ export async function ghAddComment(number, body, repo, opts) {
93
+ await runGh(['issue', 'comment', String(number), '--body-file', '-', ...repoArgs(repo)], { ...opts, stdin: body });
94
+ }
95
+ export async function ghCloseIssue(number, reason, repo, opts) {
96
+ const ghReason = reason === 'not_planned' ? 'not planned' : reason;
97
+ await runGh(['issue', 'close', String(number), '--reason', ghReason, ...repoArgs(repo)], opts);
98
+ }
99
+ // =============================================================================
100
+ // Comment identity
101
+ // =============================================================================
102
+ /** Hidden marker naming the upstream comment a local comment came from. It
103
+ * lives inside the comment text, so it needs no schema change and survives
104
+ * the existing parse/serialize round trip. */
105
+ const GH_COMMENT_MARKER_RE = /^<!-- gh: (.+?) -->\r?\n?/;
106
+ export function markComment(id, body) {
107
+ return `<!-- gh: ${id} -->\n${body}`;
108
+ }
109
+ export function commentGithubId(text) {
110
+ const match = text.match(GH_COMMENT_MARKER_RE);
111
+ return match ? match[1] : null;
112
+ }
113
+ export function stripCommentMarker(text) {
114
+ return text.replace(GH_COMMENT_MARKER_RE, '');
115
+ }
116
+ /** The `<!-- devharness: local #N -->` marker publish appends to the GitHub
117
+ * body. It is upstream-only metadata: strip it from anything stored or
118
+ * hashed locally, or a pull-then-push cycle accretes one marker per sync. */
119
+ const LOCAL_MARKER_RE = /\n*<!--\s*devharness: local #\d+\s*-->/g;
120
+ export function stripLocalMarker(body) {
121
+ return body.replace(LOCAL_MARKER_RE, '');
122
+ }
123
+ // =============================================================================
124
+ // Sync policy (pure)
125
+ // =============================================================================
126
+ export function bodyHash(body) {
127
+ return createHash('sha256').update(body, 'utf8').digest('hex').slice(0, 16);
128
+ }
129
+ export function closedReasonFrom(stateReason) {
130
+ const value = (stateReason ?? '').toLowerCase().replace(/\s+/g, '_');
131
+ if (value === 'not_planned' || value === 'duplicate')
132
+ return value;
133
+ return 'completed';
134
+ }
135
+ /**
136
+ * Which side moved since the two last agreed.
137
+ *
138
+ * With no baseline this is a first sync: adopt whichever side has content
139
+ * rather than reporting a conflict on an issue nobody has actually edited.
140
+ */
141
+ export function classifySync(local, remote, strippedLocalBody) {
142
+ if (!remote) {
143
+ return { action: 'missing-upstream', localChanged: false, remoteChanged: false, firstSync: false };
144
+ }
145
+ const firstSync = !local.githubSyncedAt || !local.githubBodyHash;
146
+ if (firstSync) {
147
+ return { action: 'pull', localChanged: false, remoteChanged: true, firstSync: true };
148
+ }
149
+ const localChanged = bodyHash(strippedLocalBody) !== local.githubBodyHash;
150
+ const remoteChanged = new Date(remote.updatedAt).getTime() > local.githubSyncedAt.getTime();
151
+ let action = 'none';
152
+ if (localChanged && remoteChanged)
153
+ action = 'conflict';
154
+ else if (remoteChanged)
155
+ action = 'pull';
156
+ else if (localChanged)
157
+ action = 'push';
158
+ return { action, localChanged, remoteChanged, firstSync: false };
159
+ }
160
+ /**
161
+ * How local status and upstream state reconcile.
162
+ *
163
+ * devharness closes issues upstream but never reopens them; GitHub can do
164
+ * both locally. That asymmetry is what keeps an automated sync from undoing
165
+ * a human's decision on a public issue.
166
+ */
167
+ export function resolveStatus(local, remote, now) {
168
+ const localClosed = isCompletedStatus(local.status);
169
+ if (remote.state === 'CLOSED') {
170
+ const closedReason = closedReasonFrom(remote.stateReason);
171
+ if (localClosed)
172
+ return { closedReason };
173
+ return { status: completedStatusFor(local.type), closedReason, resolvedAt: now };
174
+ }
175
+ // Upstream is open.
176
+ if (!localClosed)
177
+ return {};
178
+ // Closed here and open there. If we had recorded a close, GitHub reopened
179
+ // it - follow. Otherwise this close has not reached GitHub yet.
180
+ if (local.closedReason) {
181
+ return {
182
+ status: local.startedAt ? 'in_progress' : 'acknowledged',
183
+ closedReason: undefined,
184
+ resolvedAt: undefined,
185
+ reopened: true,
186
+ };
187
+ }
188
+ return { closeUpstream: 'completed' };
189
+ }
190
+ /** Upstream labels are added, never removed - a replace would delete exactly
191
+ * the local-only tags that carry the triage meaning. */
192
+ export function unionLabels(local, remote) {
193
+ const merged = [...local];
194
+ for (const label of remote)
195
+ if (!merged.includes(label))
196
+ merged.push(label);
197
+ return merged;
198
+ }
199
+ //# sourceMappingURL=gh-issues.js.map