agentme 0.35.1 → 0.36.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 (19) hide show
  1. package/.xdrs/agentme/bdrs/operations/401-epic-feature-story-planning.md +1 -1
  2. package/.xdrs/agentme/edrs/application/skills/250-github-connector/SKILL.md +187 -0
  3. package/.xdrs/agentme/edrs/application/skills/250-github-connector/SKILL.test.md +118 -0
  4. package/.xdrs/agentme/edrs/application/skills/251-azure-devops-connector/SKILL.md +205 -0
  5. package/.xdrs/agentme/edrs/application/skills/251-azure-devops-connector/SKILL.test.md +114 -0
  6. package/.xdrs/agentme/edrs/index.md +3 -0
  7. package/.xdrs/agentme/edrs/principles/017-skill-testing.md +3 -0
  8. package/.xdrs/agentme/edrs/principles/skills/150-refine-plan-mode/SKILL.md +25 -8
  9. package/.xdrs/agentme/edrs/principles/skills/150-refine-plan-mode/SKILL.test.md +27 -3
  10. package/.xdrs/agentme/edrs/principles/skills/400-pr-owner-assistant/Makefile +8 -0
  11. package/.xdrs/agentme/edrs/principles/skills/400-pr-owner-assistant/SKILL.md +633 -0
  12. package/.xdrs/agentme/edrs/principles/skills/400-pr-owner-assistant/SKILL.test.md +174 -0
  13. package/.xdrs/agentme/edrs/principles/skills/400-pr-owner-assistant/scripts/post-replies-azure-devops.js +219 -0
  14. package/.xdrs/agentme/edrs/principles/skills/400-pr-owner-assistant/scripts/post-replies-azure-devops.test.js +253 -0
  15. package/.xdrs/agentme/edrs/principles/skills/400-pr-owner-assistant/scripts/post-replies-github.js +237 -0
  16. package/.xdrs/agentme/edrs/principles/skills/400-pr-owner-assistant/scripts/post-replies-github.test.js +272 -0
  17. package/.xdrs/agentme/edrs/principles/skills/400-pr-owner-assistant/scripts/update-section.js +246 -0
  18. package/.xdrs/agentme/edrs/principles/skills/400-pr-owner-assistant/scripts/update-section.test.js +199 -0
  19. package/package.json +1 -1
@@ -0,0 +1,246 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * Safe read/update helper for a pr-owner-assistant tracking file
6
+ * (.tmp/review-pr-<N>.md), keyed by each section's stable `id:` value
7
+ * instead of its title text. See SKILL.md's "Editing the tracking file" note.
8
+ *
9
+ * Usage:
10
+ * update-section.js list <file>
11
+ * update-section.js get <file> <id> <field>
12
+ * update-section.js set <file> <id> <field> <value...>
13
+ * update-section.js set-block <file> <id> <field> (new value read from stdin)
14
+ * update-section.js set-list <file> <id> <field> (new items read from stdin, one per line)
15
+ *
16
+ * <id> is the section's "id:" value (e.g. "review-comment/1935286030"), not the title.
17
+ */
18
+
19
+ const fs = require('fs');
20
+
21
+ const SCALAR_FIELDS = [
22
+ 'id',
23
+ 'status',
24
+ 'source',
25
+ 'author-raw',
26
+ 'comment-url',
27
+ 'type',
28
+ 'possible-user-intention',
29
+ 'suggested-fix-assessment',
30
+ 'criticality',
31
+ 'action',
32
+ 'resolve-on-apply',
33
+ 'pending-reply',
34
+ ];
35
+ const BLOCK_FIELDS = ['source-lines', 'diff-hunk-raw', 'suggested-fix', 'comment-raw', 'reply-draft'];
36
+ const LIST_FIELDS = ['replies-raw', 'possible-follow-ups'];
37
+ const ALL_FIELDS = [...SCALAR_FIELDS, ...BLOCK_FIELDS, ...LIST_FIELDS];
38
+ const NEXT_FIELD_RE = new RegExp(`^(?:${ALL_FIELDS.join('|')}):`);
39
+
40
+ function usage() {
41
+ return `Usage:
42
+ update-section.js list <file>
43
+ update-section.js get <file> <id> <field>
44
+ update-section.js set <file> <id> <field> <value...>
45
+ update-section.js set-block <file> <id> <field> (new value read from stdin)
46
+ update-section.js set-list <file> <id> <field> (new items read from stdin, one per line)
47
+
48
+ <id> is the section's "id:" value (e.g. "review-comment/1935286030"), not the title.
49
+ Scalar fields: ${SCALAR_FIELDS.join(', ')}
50
+ Block fields (use set-block): ${BLOCK_FIELDS.join(', ')}
51
+ List fields (use set-list): ${LIST_FIELDS.join(', ')}`;
52
+ }
53
+
54
+ function headerIndices(lines) {
55
+ const idx = [];
56
+ lines.forEach((line, i) => {
57
+ if (line.startsWith('### ')) idx.push(i);
58
+ });
59
+ return idx;
60
+ }
61
+
62
+ function findSectionById(lines, id) {
63
+ const idx = headerIndices(lines);
64
+ const matches = [];
65
+ for (let s = 0; s < idx.length; s++) {
66
+ const start = idx[s];
67
+ const end = s + 1 < idx.length ? idx[s + 1] : lines.length;
68
+ for (let i = start; i < end; i++) {
69
+ const m = /^id:\s*(.*)$/.exec(lines[i]);
70
+ if (m && m[1].trim() === id) {
71
+ matches.push({ start, end, title: lines[start].slice(4).trim() });
72
+ break;
73
+ }
74
+ }
75
+ }
76
+ if (matches.length === 0) throw new Error(`no section found with id: ${id}`);
77
+ if (matches.length > 1) {
78
+ throw new Error(`ambiguous: ${matches.length} sections found with id: ${id}`);
79
+ }
80
+ return matches[0];
81
+ }
82
+
83
+ // Locates `field:`'s own line plus the line range of any indented content that follows it
84
+ // (block-scalar `|` content or a `- ` list), stopping at the next known top-level field or
85
+ // the next section header -- never at a bare blank line, since block content legitimately
86
+ // contains blank lines.
87
+ function findField(lines, start, end, field) {
88
+ const headRe = new RegExp(`^${field}:(.*)$`);
89
+ const occurrences = [];
90
+ for (let i = start; i < end; i++) {
91
+ if (headRe.test(lines[i])) occurrences.push(i);
92
+ }
93
+ if (occurrences.length === 0) throw new Error(`field "${field}" not found in matched section`);
94
+ if (occurrences.length > 1) {
95
+ throw new Error(
96
+ `field "${field}" appears ${occurrences.length} times in this section -- ambiguous, edit manually`,
97
+ );
98
+ }
99
+ const lineIdx = occurrences[0];
100
+ const suffix = headRe.exec(lines[lineIdx])[1].trim();
101
+ let contentEnd = lineIdx + 1;
102
+ let kind;
103
+ if (suffix === '|') {
104
+ kind = 'block';
105
+ while (contentEnd < end && !NEXT_FIELD_RE.test(lines[contentEnd]) && !lines[contentEnd].startsWith('### ')) {
106
+ contentEnd++;
107
+ }
108
+ } else if (suffix === '') {
109
+ if (lineIdx + 1 < end && /^ {2}- /.test(lines[lineIdx + 1])) {
110
+ kind = 'list';
111
+ while (contentEnd < end && /^ {2}- /.test(lines[contentEnd])) contentEnd++;
112
+ } else {
113
+ kind = 'scalar';
114
+ }
115
+ } else {
116
+ kind = 'scalar';
117
+ }
118
+ return { lineIdx, kind, suffix, contentStart: lineIdx + 1, contentEnd };
119
+ }
120
+
121
+ function requireKind(field, expectedKind, fieldSet, cmdHint) {
122
+ if (!fieldSet.includes(field)) {
123
+ throw new Error(`field "${field}" is not a ${expectedKind} field -- use ${cmdHint}`);
124
+ }
125
+ }
126
+
127
+ function readStdin() {
128
+ return fs.readFileSync(0, 'utf8');
129
+ }
130
+
131
+ function writeBack(file, lines, hadTrailingNewline) {
132
+ let text = lines.join('\n');
133
+ if (hadTrailingNewline && !text.endsWith('\n')) text += '\n';
134
+ fs.writeFileSync(file, text);
135
+ }
136
+
137
+ function main(argv) {
138
+ const [cmd, file, ...rest] = argv;
139
+ if (!cmd || cmd === '--help' || cmd === '-h' || !file) {
140
+ console.error(usage());
141
+ process.exit(cmd === '--help' || cmd === '-h' ? 0 : 1);
142
+ return;
143
+ }
144
+
145
+ const original = fs.readFileSync(file, 'utf8');
146
+ const hadTrailingNewline = original.endsWith('\n');
147
+ const lines = original.split('\n');
148
+
149
+ if (cmd === 'list') {
150
+ const idx = headerIndices(lines);
151
+ for (let s = 0; s < idx.length; s++) {
152
+ const start = idx[s];
153
+ const end = s + 1 < idx.length ? idx[s + 1] : lines.length;
154
+ const row = { id: '', status: '', action: '', 'pending-reply': '', 'resolve-on-apply': '' };
155
+ for (const key of Object.keys(row)) {
156
+ try {
157
+ const f = findField(lines, start, end, key);
158
+ row[key] = f.suffix;
159
+ } catch {
160
+ // field absent in a malformed section -- leave blank, never fail `list`
161
+ }
162
+ }
163
+ // Bullets avoid relying on column/tab alignment, which breaks once field values vary in width.
164
+ console.log(`- id: ${row.id}`);
165
+ console.log(` title: ${lines[start].slice(4).trim()}`);
166
+ console.log(` status: ${row.status}`);
167
+ console.log(` action: ${row.action}`);
168
+ console.log(` pending-reply: ${row['pending-reply']}`);
169
+ console.log(` resolve-on-apply: ${row['resolve-on-apply']}`);
170
+ }
171
+ return;
172
+ }
173
+
174
+ const [id, field, ...valueParts] = rest;
175
+ if (!id || !field) {
176
+ console.error(usage());
177
+ process.exit(1);
178
+ return;
179
+ }
180
+ const section = findSectionById(lines, id);
181
+
182
+ if (cmd === 'get') {
183
+ const f = findField(lines, section.start, section.end, field);
184
+ if (f.kind === 'scalar') {
185
+ console.log(f.suffix);
186
+ } else if (f.kind === 'block') {
187
+ console.log(
188
+ lines
189
+ .slice(f.contentStart, f.contentEnd)
190
+ .map((l) => (l.startsWith(' ') ? l.slice(2) : l))
191
+ .join('\n'),
192
+ );
193
+ } else {
194
+ console.log(
195
+ lines
196
+ .slice(f.contentStart, f.contentEnd)
197
+ .map((l) => l.replace(/^ {2}- /, ''))
198
+ .join('\n'),
199
+ );
200
+ }
201
+ return;
202
+ }
203
+
204
+ if (cmd === 'set') {
205
+ requireKind(field, 'scalar', SCALAR_FIELDS, 'set-block/set-list');
206
+ const f = findField(lines, section.start, section.end, field);
207
+ const value = valueParts.join(' ');
208
+ lines[f.lineIdx] = value === '' ? `${field}:` : `${field}: ${value}`;
209
+ writeBack(file, lines, hadTrailingNewline);
210
+ console.log('OK');
211
+ return;
212
+ }
213
+
214
+ if (cmd === 'set-block') {
215
+ requireKind(field, 'block', BLOCK_FIELDS, 'set (scalar) or set-list');
216
+ const f = findField(lines, section.start, section.end, field);
217
+ const stdin = readStdin().replace(/\n$/, '');
218
+ const newContentLines = stdin.split('\n').map((l) => (l === '' ? '' : ` ${l}`));
219
+ lines.splice(f.lineIdx, f.contentEnd - f.lineIdx, `${field}: |`, ...newContentLines);
220
+ writeBack(file, lines, hadTrailingNewline);
221
+ console.log('OK');
222
+ return;
223
+ }
224
+
225
+ if (cmd === 'set-list') {
226
+ requireKind(field, 'list', LIST_FIELDS, 'set (scalar) or set-block');
227
+ const f = findField(lines, section.start, section.end, field);
228
+ const stdin = readStdin().replace(/\n$/, '');
229
+ const items = stdin.split('\n').filter((l) => l.trim() !== '');
230
+ const newContentLines = items.map((l) => ` - ${l}`);
231
+ lines.splice(f.lineIdx, f.contentEnd - f.lineIdx, `${field}:`, ...newContentLines);
232
+ writeBack(file, lines, hadTrailingNewline);
233
+ console.log('OK');
234
+ return;
235
+ }
236
+
237
+ console.error(usage());
238
+ process.exit(1);
239
+ }
240
+
241
+ try {
242
+ main(process.argv.slice(2));
243
+ } catch (err) {
244
+ console.error(`Error: ${err.message}`);
245
+ process.exit(1);
246
+ }
@@ -0,0 +1,199 @@
1
+ 'use strict';
2
+
3
+ const test = require('node:test');
4
+ const assert = require('node:assert/strict');
5
+ const fs = require('node:fs');
6
+ const os = require('node:os');
7
+ const path = require('node:path');
8
+ const { execFileSync } = require('node:child_process');
9
+
10
+ const SCRIPT = path.join(__dirname, 'update-section.js');
11
+
12
+ const FIXTURE = `# PR #482
13
+
14
+ Some raw PR summary text.
15
+
16
+ ### Contributor offers to help
17
+ id: issue-comment/111
18
+ status: open
19
+ source: (PR conversation)
20
+ comment-raw: |
21
+ Thanks for this PR!
22
+
23
+ Happy to help test it.
24
+ replies-raw:
25
+ possible-follow-ups:
26
+ - reply thanking them
27
+ action:
28
+ resolve-on-apply: false
29
+ pending-reply: none
30
+ reply-draft: |
31
+
32
+ ### Naming taxonomy question
33
+ id: review-comment/222
34
+ status: open
35
+ source: [lib/src/foo.ts:10-10](../lib/src/foo.ts#L10-L10)
36
+ suggested-fix: |
37
+ const value = compute();
38
+ suggested-fix-assessment: accept-as-is
39
+ comment-raw: |
40
+ Shall we rename this?
41
+ replies-raw:
42
+ possible-follow-ups:
43
+ - "fix: rename for consistency"
44
+ - "mark won't-fix: keep current naming"
45
+ action: fix
46
+ resolve-on-apply: true
47
+ pending-reply: drafted
48
+ reply-draft: |
49
+ Renamed as suggested. (pr-owner-assistant skill - using defaults)
50
+ `;
51
+
52
+ function writeFixture(content = FIXTURE) {
53
+ const file = path.join(fs.mkdtempSync(path.join(os.tmpdir(), 'update-section-test-')), 'review-pr-482.md');
54
+ fs.writeFileSync(file, content);
55
+ return file;
56
+ }
57
+
58
+ function run(args, input) {
59
+ try {
60
+ const stdout = execFileSync('node', [SCRIPT, ...args], { input, encoding: 'utf8' });
61
+ return { status: 0, stdout };
62
+ } catch (err) {
63
+ return { status: err.status, stdout: err.stdout, stderr: err.stderr };
64
+ }
65
+ }
66
+
67
+ test('list renders one bullet block per section, not tab-aligned columns', () => {
68
+ const file = writeFixture();
69
+ const { status, stdout } = run(['list', file]);
70
+ assert.equal(status, 0);
71
+ assert.equal(stdout.includes('\t'), false, 'list output must not rely on tab alignment');
72
+ assert.match(stdout, /- id: issue-comment\/111/);
73
+ assert.match(stdout, /^ {2}title: Contributor offers to help$/m);
74
+ assert.match(stdout, /^ {2}status: open$/m);
75
+ assert.match(stdout, /- id: review-comment\/222/);
76
+ assert.match(stdout, /^ {2}action: fix$/m);
77
+ assert.match(stdout, /^ {2}pending-reply: drafted$/m);
78
+ });
79
+
80
+ test('get returns a scalar field value', () => {
81
+ const file = writeFixture();
82
+ const { status, stdout } = run(['get', file, 'review-comment/222', 'status']);
83
+ assert.equal(status, 0);
84
+ assert.equal(stdout.trim(), 'open');
85
+ });
86
+
87
+ test('get returns a block field value with blank lines preserved', () => {
88
+ const file = writeFixture();
89
+ const { status, stdout } = run(['get', file, 'issue-comment/111', 'comment-raw']);
90
+ assert.equal(status, 0);
91
+ assert.equal(stdout, 'Thanks for this PR!\n\nHappy to help test it.\n');
92
+ });
93
+
94
+ test('get returns a list field with dash prefixes stripped', () => {
95
+ const file = writeFixture();
96
+ const { status, stdout } = run(['get', file, 'review-comment/222', 'possible-follow-ups']);
97
+ assert.equal(status, 0);
98
+ assert.equal(stdout, '"fix: rename for consistency"\n"mark won\'t-fix: keep current naming"\n');
99
+ });
100
+
101
+ test('get returns the suggested-fix block field extracted for a comment', () => {
102
+ const file = writeFixture();
103
+ const { status, stdout } = run(['get', file, 'review-comment/222', 'suggested-fix']);
104
+ assert.equal(status, 0);
105
+ assert.equal(stdout, 'const value = compute();\n');
106
+ });
107
+
108
+ test('get returns the suggested-fix-assessment scalar field', () => {
109
+ const file = writeFixture();
110
+ const { status, stdout } = run(['get', file, 'review-comment/222', 'suggested-fix-assessment']);
111
+ assert.equal(status, 0);
112
+ assert.equal(stdout.trim(), 'accept-as-is');
113
+ });
114
+
115
+ test('set updates only the targeted scalar field, leaving the rest of the file untouched', () => {
116
+ const file = writeFixture();
117
+ const before = fs.readFileSync(file, 'utf8');
118
+ const { status } = run(['set', file, 'issue-comment/111', 'action', 'reply']);
119
+ assert.equal(status, 0);
120
+ const after = fs.readFileSync(file, 'utf8');
121
+ assert.equal(after, before.replace('action:\nresolve-on-apply: false\npending-reply: none', 'action: reply\nresolve-on-apply: false\npending-reply: none'));
122
+ });
123
+
124
+ test('set with an empty value clears a scalar field to "field:" with no trailing space', () => {
125
+ const file = writeFixture();
126
+ run(['set', file, 'review-comment/222', 'action', '']);
127
+ const { stdout } = run(['get', file, 'review-comment/222', 'action']);
128
+ assert.equal(stdout.trim(), '');
129
+ assert.match(fs.readFileSync(file, 'utf8'), /^action:$/m);
130
+ });
131
+
132
+ test('set-block replaces block content from stdin, preserving a blank line in the middle', () => {
133
+ const file = writeFixture();
134
+ const { status } = run(['set-block', file, 'issue-comment/111', 'reply-draft'], 'Line one.\n\nLine two.\n');
135
+ assert.equal(status, 0);
136
+ const { stdout } = run(['get', file, 'issue-comment/111', 'reply-draft']);
137
+ assert.equal(stdout, 'Line one.\n\nLine two.\n');
138
+ });
139
+
140
+ test('set-list replaces list content from stdin, skipping blank lines', () => {
141
+ const file = writeFixture();
142
+ const { status } = run(['set-list', file, 'issue-comment/111', 'possible-follow-ups'], 'item one\n\nitem two\n');
143
+ assert.equal(status, 0);
144
+ const { stdout } = run(['get', file, 'issue-comment/111', 'possible-follow-ups']);
145
+ assert.equal(stdout, 'item one\nitem two\n');
146
+ });
147
+
148
+ test('set-block replaces suggested-fix content, preserving multi-line code', () => {
149
+ const file = writeFixture();
150
+ const { status } = run(
151
+ ['set-block', file, 'review-comment/222', 'suggested-fix'],
152
+ 'const value = 42;\nconst other = 1;\n',
153
+ );
154
+ assert.equal(status, 0);
155
+ const { stdout } = run(['get', file, 'review-comment/222', 'suggested-fix']);
156
+ assert.equal(stdout, 'const value = 42;\nconst other = 1;\n');
157
+ });
158
+
159
+ test('set updates the suggested-fix-assessment scalar field', () => {
160
+ const file = writeFixture();
161
+ const { status } = run(['set', file, 'review-comment/222', 'suggested-fix-assessment', 'evolve-with-changes']);
162
+ assert.equal(status, 0);
163
+ const { stdout } = run(['get', file, 'review-comment/222', 'suggested-fix-assessment']);
164
+ assert.equal(stdout.trim(), 'evolve-with-changes');
165
+ });
166
+
167
+ test('preserves absence of a trailing newline on the original file', () => {
168
+ const file = writeFixture(FIXTURE.replace(/\n$/, ''));
169
+ run(['set', file, 'issue-comment/111', 'action', 'reply']);
170
+ assert.equal(fs.readFileSync(file, 'utf8').endsWith('\n'), false);
171
+ });
172
+
173
+ test('get on an unknown id fails with a clear error', () => {
174
+ const file = writeFixture();
175
+ const { status, stderr } = run(['get', file, 'issue-comment/999', 'status']);
176
+ assert.notEqual(status, 0);
177
+ assert.match(stderr, /no section found with id: issue-comment\/999/);
178
+ });
179
+
180
+ test('get on an ambiguous id (duplicated in a malformed file) fails rather than guessing', () => {
181
+ const file = writeFixture(FIXTURE + FIXTURE.replace('### Contributor offers to help', '### Duplicate section'));
182
+ const { status, stderr } = run(['get', file, 'issue-comment/111', 'status']);
183
+ assert.notEqual(status, 0);
184
+ assert.match(stderr, /ambiguous: 2 sections found with id: issue-comment\/111/);
185
+ });
186
+
187
+ test('set on a block-only field fails and points to set-block', () => {
188
+ const file = writeFixture();
189
+ const { status, stderr } = run(['set', file, 'issue-comment/111', 'reply-draft', 'oops']);
190
+ assert.notEqual(status, 0);
191
+ assert.match(stderr, /use set-block/);
192
+ });
193
+
194
+ test('set-block on a scalar-only field fails and points to set', () => {
195
+ const file = writeFixture();
196
+ const { status, stderr } = run(['set-block', file, 'issue-comment/111', 'action'], 'oops\n');
197
+ assert.notEqual(status, 0);
198
+ assert.match(stderr, /use set \(scalar\)/);
199
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentme",
3
- "version": "0.35.1",
3
+ "version": "0.36.0",
4
4
  "description": "",
5
5
  "dependencies": {
6
6
  "filedist": "^0.39.0"