@masculinecache/issue-work 0.0.0-stage → 0.1.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.
package/README.md CHANGED
@@ -1,3 +1,86 @@
1
- # Temporary Holding Version
1
+ # issue-work
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ GitHub issue triage + automation for agents. AXI-compliant TOON output, confidence-gated implementation.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install -g issue-work
9
+ ```
10
+
11
+ Or via mise:
12
+
13
+ ```toml
14
+ "npm:issue-work" = "0.1.0"
15
+ ```
16
+
17
+ ## Usage
18
+
19
+ ```
20
+ issue-work [command] [args] [flags]
21
+ commands[4]:
22
+ (none)=triage+implement, triage <number>, list, comment <number>
23
+ flags[4]:
24
+ -R/--repo <OWNER/NAME>, --dry-run, --confidence-threshold <N> (default 95), --help, -v/--version
25
+ ```
26
+
27
+ ### Examples
28
+
29
+ ```bash
30
+ # Triage + implement if confidence >= 95
31
+ issue-work 42
32
+
33
+ # Triage only (never implements)
34
+ issue-work triage 42
35
+
36
+ # Dry-run — show plan without mutating anything
37
+ issue-work triage 42 --dry-run
38
+
39
+ # List open issues with classification + confidence
40
+ issue-work list --repo owner/name
41
+
42
+ # List untriaged issues (no labels)
43
+ issue-work list --repo owner/name --untriaged
44
+
45
+ # Custom confidence threshold
46
+ issue-work 42 --confidence-threshold 80
47
+
48
+ # Post a comment
49
+ issue-work comment 42 --body "investigating"
50
+ ```
51
+
52
+ ## How it works
53
+
54
+ 1. **Triage**: classifies the issue (bug/feat/chore/docs/question), estimates confidence (0–100), scope (S/M/L/XL), and suggests labels.
55
+ 2. **Decision**: if confidence >= threshold (default 95), outputs implementation plan (branch name, steps). If below, outputs triage-only with blockers.
56
+ 3. **Comment**: posts a structured triage comment on the issue (unless `--dry-run`).
57
+ 4. **Implementation**: the CLI itself does not write code — it signals the calling agent (via stdout) that implementation is warranted. The agent follows the `issue-automation` skill for the actual implementation flow.
58
+
59
+ ## Confidence estimation
60
+
61
+ The CLI estimates confidence from issue content using heuristic signals:
62
+
63
+ | Signal | Contribution |
64
+ |---|---|
65
+ | Clear reproduction steps | +20 |
66
+ | Expected/actual behavior | +10 |
67
+ | Acceptance criteria | +15 |
68
+ | Single-file fix indicated | +15 |
69
+ | Existing test pattern mentioned | +10 |
70
+ | Breaking change implied | -20 |
71
+ | Security-sensitive area | -25 |
72
+ | Ambiguous language | -15 |
73
+ | Multi-file change needed | -20 |
74
+ | External API dependency | -15 |
75
+ | Short/unclear description | -10 |
76
+
77
+ Base score: 50. Adjusted by signals. Clamped to 0–100.
78
+
79
+ ## Dependencies
80
+
81
+ - `gh-axi` — all GitHub API access goes through gh-axi (never raw `gh`)
82
+ - Node.js >= 18
83
+
84
+ ## License
85
+
86
+ MIT
package/bin/common.mjs ADDED
@@ -0,0 +1,144 @@
1
+ // AXI shared helpers — TOON output, flag parsing, errors, truncation.
2
+ import { execFileSync } from 'node:child_process';
3
+ import { homedir } from 'node:os';
4
+
5
+ export const HOME = homedir();
6
+
7
+ export function collapseHome(p) {
8
+ return p.replace(HOME, '~');
9
+ }
10
+
11
+ export function list(name, fields, rows, opts = {}) {
12
+ const lines = [];
13
+ const n = rows.length;
14
+ if (opts.total !== undefined && opts.total > n) {
15
+ lines.push(`count: ${n} of ${opts.total} total`);
16
+ }
17
+ lines.push(`${name}[${n}]{${fields.join(',')}}:`);
18
+ for (const r of rows) {
19
+ lines.push(' ' + fields.map((f) => fmt(r[f])).join(','));
20
+ }
21
+ return lines.join('\n');
22
+ }
23
+
24
+ export function kv(obj) {
25
+ return Object.entries(obj)
26
+ .map(([k, v]) => `${k}: ${fmt(v)}`)
27
+ .join('\n');
28
+ }
29
+
30
+ export function fmt(v) {
31
+ if (v === null || v === undefined) return '';
32
+ if (typeof v === 'string') {
33
+ if (v.includes(',') || v.includes('\n') || v.includes(':')) {
34
+ return `"${v.replace(/"/g, "'")}"`;
35
+ }
36
+ return v;
37
+ }
38
+ return String(v);
39
+ }
40
+
41
+ export function help(lines) {
42
+ return lines.map((l, i) => `help[${i + 1}]: ${l}`).join('\n');
43
+ }
44
+
45
+ export function fail(message, code = 'ERROR', helpLines = []) {
46
+ const out = [`error: ${message}`, `code: ${code}`];
47
+ if (helpLines.length) out.push(help(helpLines));
48
+ process.stdout.write(out.join('\n') + '\n');
49
+ process.exit(1);
50
+ }
51
+
52
+ export function usageError(message, helpLines = []) {
53
+ const out = [`error: ${message}`, `code: USAGE`];
54
+ if (helpLines.length) out.push(help(helpLines));
55
+ process.stdout.write(out.join('\n') + '\n');
56
+ process.exit(2);
57
+ }
58
+
59
+ export function truncate(s, max = 500, fullHintCmd = null) {
60
+ if (s === undefined || s === null) return '';
61
+ const str = String(s);
62
+ if (str.length <= max) return str;
63
+ const head = str.slice(0, max);
64
+ const note = `... (truncated, ${str.length} chars total)`;
65
+ let out = head + '\n' + note;
66
+ if (fullHintCmd) out += '\n' + help([`Run \`${fullHintCmd}\` for full content`]);
67
+ return out;
68
+ }
69
+
70
+ export function parseFlags(args, spec = {}) {
71
+ const bools = new Set(['help', ...(spec.bools || [])]);
72
+ const values = new Set(spec.values || []);
73
+ const flags = { help: false, _: [] };
74
+ const valid = [...bools, ...values];
75
+ for (let i = 0; i < args.length; i++) {
76
+ const a = args[i];
77
+ if (a === '--') { flags._.push(...args.slice(i + 1)); break; }
78
+ if (a.startsWith('--')) {
79
+ const eq = a.indexOf('=');
80
+ const name = eq > 0 ? a.slice(2, eq) : a.slice(2);
81
+ if (name === 'help') { flags.help = true; continue; }
82
+ if (bools.has(name)) {
83
+ flags[name] = true;
84
+ continue;
85
+ }
86
+ if (values.has(name)) {
87
+ if (eq > 0) {
88
+ flags[name] = a.slice(eq + 1);
89
+ } else {
90
+ const v = args[i + 1];
91
+ if (v === undefined || v.startsWith('--')) {
92
+ usageError(`--${name} requires a value`, [`valid flags: --${valid.join(', --')} (--help always allowed)`]);
93
+ }
94
+ flags[name] = v;
95
+ i++;
96
+ }
97
+ continue;
98
+ }
99
+ usageError(`unknown flag --${name}`, [`valid flags for this command: --${valid.join(', --')} (--help always allowed)`]);
100
+ }
101
+ if (/^-\d+$/.test(a)) { flags._.push(a); continue; }
102
+ flags._.push(a);
103
+ }
104
+ return flags;
105
+ }
106
+
107
+ export function run(cmd, args, opts = {}) {
108
+ try {
109
+ const stdout = execFileSync(cmd, args, {
110
+ encoding: 'utf8',
111
+ stdio: ['ignore', 'pipe', 'pipe'],
112
+ timeout: opts.timeout || 30000,
113
+ env: { ...process.env, ...(opts.env || {}) },
114
+ maxBuffer: 16 * 1024 * 1024,
115
+ });
116
+ return { code: 0, stdout, stderr: '' };
117
+ } catch (e) {
118
+ return { code: e.status ?? 1, stdout: e.stdout?.toString() ?? '', stderr: e.stderr?.toString() ?? '' };
119
+ }
120
+ }
121
+
122
+ export function ghJson(args, opts = {}) {
123
+ const bin = process.env.GH_BIN || 'gh';
124
+ const r = run(bin, args, opts);
125
+ if (r.code !== 0) {
126
+ const msg = r.stderr.trim() || r.stdout.trim() || `gh exited ${r.code}`;
127
+ fail(`gh: ${msg}`, 'GH_ERROR', ['Ensure gh is installed and authenticated']);
128
+ }
129
+ try {
130
+ return JSON.parse(r.stdout);
131
+ } catch {
132
+ fail('gh returned invalid JSON', 'PARSE_ERROR');
133
+ }
134
+ }
135
+
136
+ export function ghMutate(args, opts = {}) {
137
+ const bin = process.env.GH_AXI_BIN || 'gh-axi';
138
+ const r = run(bin, args, opts);
139
+ if (r.code !== 0) {
140
+ const msg = r.stderr.trim() || r.stdout.trim() || `gh-axi exited ${r.code}`;
141
+ fail(`gh-axi: ${msg}`, 'GH_AXI_ERROR', ['Ensure gh-axi is installed and authenticated']);
142
+ }
143
+ return r.stdout;
144
+ }
package/bin/issue-work ADDED
@@ -0,0 +1,343 @@
1
+ #!/usr/bin/env node
2
+ // issue-work — GitHub issue triage + automation for agents. TOON output, confidence-gated.
3
+ import { list, kv, help, fail, usageError, parseFlags, ghJson, ghMutate, truncate, fmt } from './common.mjs';
4
+
5
+ const VERSION = '0.1.0';
6
+
7
+ import { run } from './common.mjs';
8
+
9
+ function resolveRepo(flags) {
10
+ if (flags.repo) return flags.repo;
11
+ const r = run('git', ['remote', 'get-url', 'origin']);
12
+ if (r.code === 0) {
13
+ const url = r.stdout.trim();
14
+ const m = url.match(/[:/]([^/:]+\/[^/.]+)(?:\.git)?$/);
15
+ if (m) return m[1];
16
+ }
17
+ return null;
18
+ }
19
+
20
+ const TOP_HELP = `usage: issue-work [command] [args] [flags]
21
+ commands[4]:
22
+ (none)=triage+implement, triage <number>, list, comment <number>
23
+ flags[4]:
24
+ -R/--repo <OWNER/NAME>, --dry-run, --confidence-threshold <N> (default 95), --help, -v/--version
25
+ examples:
26
+ issue-work 42
27
+ issue-work triage 42 --repo owner/name --dry-run
28
+ issue-work list --repo owner/name --label bug
29
+ issue-work comment 42 --body "triaged: confidence 92, scope M"`;
30
+
31
+ const COMMAND_HELP = {
32
+ triage: `usage: issue-work triage <number> [flags]
33
+ Triage a single issue: classify, estimate confidence, suggest labels.
34
+ Does not implement — always outputs triage only.
35
+ flags: -R/--repo, --dry-run, --help`,
36
+ list: `usage: issue-work list [flags]
37
+ List open issues for a repo.
38
+ flags: -R/--repo, --label <name>, --assignee <login>, --state <open|closed|all>, --limit <n> (default 30), --untriaged, --help`,
39
+ comment: `usage: issue-work comment <number> [flags]
40
+ Post a structured comment on an issue.
41
+ flags: -R/--repo, --body <text> (required), --help`,
42
+ };
43
+
44
+ function classifyIssue(issue) {
45
+ const title = (issue.title || '').toLowerCase();
46
+ const body = (issue.body || '').toLowerCase();
47
+ const labels = (issue.labels || []).map((l) => (typeof l === 'string' ? l : l.name || '')).join(' ');
48
+
49
+ const titleMatch = (() => {
50
+ if (/\b(bug|fix|broken|error|crash|fail|regression|wrong)\b/.test(title)) return 'bug';
51
+ if (/\b(feat|feature|add|new|support|implement|create)\b/.test(title)) return 'feat';
52
+ if (/\b(chore|refactor|clean|update|maintain|upgrade|bump|deps?)\b/.test(title)) return 'chore';
53
+ if (/\b(doc|docs|readme|comment|explain|guide)\b/.test(title)) return 'docs';
54
+ if (/\b(questi|how\s+to|help|confused|unclear)\b/.test(title)) return 'question';
55
+ return null;
56
+ })();
57
+ if (titleMatch) return titleMatch;
58
+
59
+ const text = `${title} ${body} ${labels}`;
60
+ if (/\b(bug|broken|error|crash|regression|wrong)\b/.test(text)) return 'bug';
61
+ if (/\b(feat|feature|add|new|support|implement|create)\b/.test(text)) return 'feat';
62
+ if (/\b(chore|refactor|clean|maintain|upgrade|bump|deps?)\b/.test(text)) return 'chore';
63
+ if (/\b(doc|docs|readme|explain|guide)\b/.test(text)) return 'docs';
64
+ if (/\b(questi|how\s+to|help|confused|unclear)\b/.test(text)) return 'question';
65
+ return 'task';
66
+ }
67
+
68
+ function estimateConfidence(issue) {
69
+ const body = issue.body || '';
70
+ const title = issue.title || '';
71
+ let score = 50;
72
+
73
+ if (/steps?\s+to\s+repro|reproduc|how\s+to\s+repro/i.test(body)) score += 20;
74
+ if (/expected\s+behavior|actual\s+behavior/i.test(body)) score += 10;
75
+ if (/acceptance\s+criteria|definition\s+of\s+done/i.test(body)) score += 15;
76
+ if (/single\s+file|one\s+file|only\s+in\s+\S+\.\S+/i.test(body)) score += 15;
77
+ if (/\b(test|spec|should|expect)\b/i.test(body)) score += 10;
78
+ if (/breaking|migration|refactor\s+multiple|across\s+\d+\s+file/i.test(body)) score -= 20;
79
+ if (/security|auth|secret|token|credential|password|encrypt/i.test(body)) score -= 25;
80
+ if (/\b(unknown|unclear|maybe|might|perhaps|tbd|todo)\b/i.test(body)) score -= 15;
81
+ if (/\b(multiple|several|many|various)\s+(file|module|component|service)/i.test(body)) score -= 20;
82
+ if (/external\s+api|third.?party|upstream|downstream/i.test(body)) score -= 15;
83
+ if (body.length < 100) score -= 10;
84
+
85
+ return Math.max(0, Math.min(100, score));
86
+ }
87
+
88
+ function estimateScope(issue) {
89
+ const body = (issue.body || '').toLowerCase();
90
+ const title = (issue.title || '').toLowerCase();
91
+ const text = `${title} ${body}`;
92
+ if (/single\s+file|typo|one.?liner|minor|small|quick/i.test(text)) return 'S';
93
+ if (/multiple\s+file|refactor|module|component|service/i.test(text)) return 'L';
94
+ if (/across\s+the\s+fleet|multi.?repo|migration|architecture/i.test(text)) return 'XL';
95
+ return 'M';
96
+ }
97
+
98
+ function suggestLabels(issue, classification) {
99
+ const suggested = [classification];
100
+ const body = (issue.body || '').toLowerCase();
101
+ if (/security|auth|secret|token/i.test(body)) suggested.push('security');
102
+ if (/breaking|migration/i.test(body)) suggested.push('breaking-change');
103
+ if (/fleet|swarm|multi.?host/i.test(body)) suggested.push('fleet');
104
+ if (/agent|automation|ci\/?cd/i.test(body)) suggested.push('automation');
105
+ return [...new Set(suggested)];
106
+ }
107
+
108
+ function branchName(issueNumber, title) {
109
+ const slug = (title || 'fix')
110
+ .toLowerCase()
111
+ .replace(/[^a-z0-9]+/g, '-')
112
+ .replace(/^-+|-+$/g, '')
113
+ .slice(0, 40);
114
+ return `issue-${issueNumber}-${slug}`;
115
+ }
116
+
117
+ function formatTriageOutput(issue, repo, dryRun) {
118
+ const classification = classifyIssue(issue);
119
+ const confidence = estimateConfidence(issue);
120
+ const scope = estimateScope(issue);
121
+ const labels = suggestLabels(issue, classification);
122
+ const branch = branchName(issue.number, issue.title);
123
+ const threshold = 95;
124
+ const action = confidence >= threshold ? 'implement' : 'triage-only';
125
+
126
+ const lines = [];
127
+ lines.push(`issue #${issue.number} | ${classification} | confidence ${confidence} | scope ${scope} | labels: ${labels.join(', ')}`);
128
+ lines.push(`action: ${action}${confidence < threshold ? ` (below threshold ${threshold})` : ''}`);
129
+ lines.push(`branch: ${branch}`);
130
+ if (dryRun) lines.push('dry-run: no mutations performed');
131
+ lines.push(`repo: ${repo}`);
132
+
133
+ if (confidence >= threshold) {
134
+ lines.push('');
135
+ lines.push(`implementation plan:`);
136
+ lines.push(` 1. branch: ${branch}`);
137
+ lines.push(` 2. implement fix for: ${truncate(issue.title, 80)}`);
138
+ lines.push(` 3. open PR targeting default branch`);
139
+ lines.push(` 4. stop — captain reviews and merges`);
140
+ } else {
141
+ lines.push('');
142
+ lines.push(`blockers for implementation:`);
143
+ if (confidence < 50) lines.push(' - very low confidence — requirements likely ambiguous');
144
+ else if (confidence < 70) lines.push(' - low confidence — some ambiguity or complexity');
145
+ else lines.push(' - near threshold — minor clarification may unlock implementation');
146
+ }
147
+
148
+ return lines.join('\n');
149
+ }
150
+
151
+ function formatTriageComment(issue) {
152
+ const classification = classifyIssue(issue);
153
+ const confidence = estimateConfidence(issue);
154
+ const scope = estimateScope(issue);
155
+ const labels = suggestLabels(issue, classification);
156
+ const threshold = 95;
157
+
158
+ const lines = [];
159
+ lines.push(`**Triage** (automated via \`issue-work\`)`);
160
+ lines.push('');
161
+ lines.push(`| Field | Value |`);
162
+ lines.push(`|---|---|`);
163
+ lines.push(`| Classification | ${classification} |`);
164
+ lines.push(`| Confidence | ${confidence}/100 |`);
165
+ lines.push(`| Scope | ${scope} |`);
166
+ lines.push(`| Suggested labels | ${labels.map((l) => '`' + l + '`').join(', ')} |`);
167
+ lines.push(`| Action | ${confidence >= threshold ? 'implement + PR' : 'triage only (below ' + threshold + ' threshold)'} |`);
168
+ lines.push('');
169
+ if (confidence < threshold) {
170
+ lines.push(`Confidence is below the ${threshold}% implementation threshold. The issue needs more detail or clarification before automated implementation can proceed.`);
171
+ }
172
+
173
+ return lines.join('\n');
174
+ }
175
+
176
+ function cmdTriage(args) {
177
+ const flags = parseFlags(args, {
178
+ bools: ['dry-run'],
179
+ values: ['repo', 'confidence-threshold'],
180
+ });
181
+ if (flags.help) { process.stdout.write(COMMAND_HELP.triage + '\n'); return; }
182
+ const number = flags._[0];
183
+ if (!number) usageError('triage requires an issue number', ['issue-work triage <number>']);
184
+ const repo = flags.repo || resolveRepo(flags);
185
+ if (!repo) fail('no repo specified and could not detect from git remote', 'NO_REPO', ['Pass -R/--repo owner/name']);
186
+
187
+ const ghArgs = ['issue', 'view', String(number), '--repo', repo, '--json', 'number,title,body,labels,state,author,createdAt'];
188
+ const issue = ghJson(ghArgs);
189
+
190
+ const dryRun = flags.dryRun || false;
191
+ const output = formatTriageOutput(issue, repo, dryRun);
192
+ process.stdout.write(output + '\n');
193
+
194
+ if (!dryRun && issue.state === 'OPEN') {
195
+ const commentBody = formatTriageComment(issue);
196
+ const commentArgs = ['issue', 'comment', String(number), '--repo', repo, '--body', commentBody];
197
+ ghMutate(commentArgs);
198
+ }
199
+ }
200
+
201
+ function cmdList(args) {
202
+ const flags = parseFlags(args, {
203
+ bools: ['dry-run', 'untriaged'],
204
+ values: ['repo', 'label', 'assignee', 'state', 'limit'],
205
+ });
206
+ if (flags.help) { process.stdout.write(COMMAND_HELP.list + '\n'); return; }
207
+
208
+ const repo = flags.repo || resolveRepo(flags);
209
+ if (!repo) fail('no repo specified and could not detect from git remote', 'NO_REPO', ['Pass -R/--repo owner/name']);
210
+
211
+ const ghArgs = ['issue', 'list', '--repo', repo, '--json', 'number,title,labels,author,createdAt,state'];
212
+ if (flags.label) ghArgs.push('--label', flags.label);
213
+ if (flags.assignee) ghArgs.push('--assignee', flags.assignee);
214
+ if (flags.state) ghArgs.push('--state', flags.state);
215
+ if (flags.limit) ghArgs.push('--limit', flags.limit);
216
+ else ghArgs.push('--limit', '30');
217
+
218
+ const issues = ghJson(ghArgs);
219
+ if (!Array.isArray(issues)) fail('unexpected gh-axi output', 'PARSE_ERROR');
220
+
221
+ let filtered = issues;
222
+ if (flags.untriaged) {
223
+ filtered = issues.filter((i) => {
224
+ const lbls = (i.labels || []).map((l) => (typeof l === 'string' ? l : l.name || ''));
225
+ return lbls.length === 0;
226
+ });
227
+ }
228
+
229
+ const rows = filtered.map((i) => ({
230
+ number: i.number,
231
+ title: truncate(i.title, 60),
232
+ classification: classifyIssue(i),
233
+ confidence: estimateConfidence(i),
234
+ scope: estimateScope(i),
235
+ state: i.state || 'OPEN',
236
+ }));
237
+
238
+ const output = list('issues', ['number', 'title', 'classification', 'confidence', 'scope', 'state'], rows, { total: issues.length });
239
+ process.stdout.write(output + '\n');
240
+ }
241
+
242
+ function cmdComment(args) {
243
+ const flags = parseFlags(args, {
244
+ bools: [],
245
+ values: ['repo', 'body'],
246
+ });
247
+ if (flags.help) { process.stdout.write(COMMAND_HELP.comment + '\n'); return; }
248
+ const number = flags._[0];
249
+ if (!number) usageError('comment requires an issue number', ['issue-work comment <number> --body "text"']);
250
+ if (!flags.body) usageError('--body is required', ['issue-work comment <number> --body "text"']);
251
+
252
+ const repo = flags.repo || resolveRepo(flags);
253
+ if (!repo) fail('no repo specified and could not detect from git remote', 'NO_REPO', ['Pass -R/--repo owner/name']);
254
+
255
+ ghMutate(['issue', 'comment', String(number), '--repo', repo, '--body', flags.body]);
256
+ process.stdout.write(`comment posted on #${number} (${repo})\n`);
257
+ }
258
+
259
+ function cmdMain(args) {
260
+ const flags = parseFlags(args, {
261
+ bools: ['dry-run'],
262
+ values: ['repo', 'confidence-threshold'],
263
+ });
264
+ if (flags.help) { process.stdout.write(TOP_HELP + '\n'); return; }
265
+
266
+ const number = flags._[0];
267
+ if (!number) {
268
+ process.stdout.write(TOP_HELP + '\n');
269
+ return;
270
+ }
271
+
272
+ const repo = flags.repo || resolveRepo(flags);
273
+ if (!repo) fail('no repo specified and could not detect from git remote', 'NO_REPO', ['Pass -R/--repo owner/name']);
274
+
275
+ const threshold = parseInt(flags['confidence-threshold'] || '95', 10);
276
+
277
+ const ghArgs = ['issue', 'view', String(number), '--repo', repo, '--json', 'number,title,body,labels,state,author,createdAt'];
278
+ const issue = ghJson(ghArgs);
279
+
280
+ if (issue.state !== 'OPEN') {
281
+ process.stdout.write(`issue #${number} is ${issue.state} — no action\n`);
282
+ return;
283
+ }
284
+
285
+ const dryRun = flags.dryRun || false;
286
+ const confidence = estimateConfidence(issue);
287
+ const classification = classifyIssue(issue);
288
+ const scope = estimateScope(issue);
289
+ const labels = suggestLabels(issue, classification);
290
+ const branch = branchName(issue.number, issue.title);
291
+
292
+ const triageOutput = formatTriageOutput(issue, repo, dryRun);
293
+ process.stdout.write(triageOutput + '\n');
294
+
295
+ if (!dryRun) {
296
+ const commentBody = formatTriageComment(issue);
297
+ const commentArgs = ['issue', 'comment', String(number), '--repo', repo, '--body', commentBody];
298
+ ghMutate(commentArgs);
299
+ }
300
+
301
+ if (confidence < threshold) {
302
+ process.stdout.write(`\nconfidence ${confidence} < threshold ${threshold} — stopping at triage\n`);
303
+ return;
304
+ }
305
+
306
+ if (dryRun) {
307
+ process.stdout.write(`\ndry-run: would implement on branch ${branch}\n`);
308
+ return;
309
+ }
310
+
311
+ process.stdout.write(`\nconfidence ${confidence} >= threshold ${threshold} — ready to implement\n`);
312
+ process.stdout.write(`branch: ${branch}\n`);
313
+ process.stdout.write(`next: implement fix and open PR\n`);
314
+ process.stdout.write(`\nissue-automation skill: the calling agent should now implement the fix\n`);
315
+ process.stdout.write(` 1. git checkout -b ${branch}\n`);
316
+ process.stdout.write(` 2. implement the fix described in #${number}\n`);
317
+ process.stdout.write(` 3. open a PR — captain reviews and merges\n`);
318
+ }
319
+
320
+ const args = process.argv.slice(2);
321
+
322
+ if (args.includes('-v') || args.includes('-V') || args.includes('--version')) {
323
+ process.stdout.write(`issue-work ${VERSION}\n`);
324
+ process.exit(0);
325
+ }
326
+
327
+ const sub = args[0];
328
+ const rest = args.slice(1);
329
+
330
+ switch (sub) {
331
+ case 'triage':
332
+ cmdTriage(rest);
333
+ break;
334
+ case 'list':
335
+ cmdList(rest);
336
+ break;
337
+ case 'comment':
338
+ cmdComment(rest);
339
+ break;
340
+ default:
341
+ cmdMain(args);
342
+ break;
343
+ }
package/package.json CHANGED
@@ -1,6 +1,13 @@
1
1
  {
2
2
  "name": "@masculinecache/issue-work",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "AXI-compliant GitHub issue triage + automation CLI — TOON output, confidence-gated implementation",
5
+ "type": "module",
6
+ "bin": { "issue-work": "bin/issue-work" },
7
+ "files": ["bin/", "skill/", "README.md"],
8
+ "engines": { "node": ">=18" },
9
+ "license": "MIT",
10
+ "repository": { "type": "git", "url": "git+https://github.com/masculinecache/issue-work.git" },
11
+ "keywords": ["github", "issues", "agents", "cli", "toon", "axi", "triage"],
12
+ "publishConfig": { "access": "public" }
13
+ }
package/skill/SKILL.md ADDED
@@ -0,0 +1,94 @@
1
+ # issue-automation
2
+
3
+ Automated GitHub issue triage and implementation workflow for agents. Load when triaging, implementing, or working with GitHub issues in an automated workflow.
4
+
5
+ ## Quick reference
6
+
7
+ ```
8
+ issue-work <number> # triage + implement if confidence >= 95
9
+ issue-work triage <number> --dry-run # triage only, no mutations
10
+ issue-work list --repo owner/name --untriaged # list untriaged issues
11
+ issue-work comment <number> --body "text" # post a comment
12
+ ```
13
+
14
+ ## Flow
15
+
16
+ ### 1. Triage (always runs first)
17
+
18
+ ```
19
+ issue-work triage <number> -R owner/name --dry-run
20
+ ```
21
+
22
+ This classifies the issue, estimates confidence, and outputs structured triage. Use `--dry-run` first for safety.
23
+
24
+ Read the output. If confidence < threshold (default 95):
25
+ - Review the blockers listed
26
+ - If you can clarify the issue (ask the author, check linked PRs/docs), do so
27
+ - Re-run triage after gathering more context
28
+ - Post findings as a comment if helpful
29
+
30
+ If confidence >= threshold, proceed to implementation.
31
+
32
+ ### 2. Implementation (confidence >= 95 only)
33
+
34
+ Follow this flow, adapted from the `lfg` skill:
35
+
36
+ 1. **Branch**: `git checkout -b issue-<number>-<slug>` (use the branch name from triage output)
37
+ 2. **Explore**: read the codebase to understand the change needed
38
+ 3. **Implement**: make minimal, correct changes
39
+ 4. **Verify**: run lint, typecheck, tests
40
+ 5. **PR**: open a pull request with a description referencing `Closes #<number>`
41
+ 6. **Stop**: captain reviews and merges — no auto-merge
42
+
43
+ Single-issue branches: one issue = one branch = one PR. Never mix.
44
+
45
+ ### 3. Fleet repos (swarm-runbook enhancements)
46
+
47
+ When the target repo is fleet-managed (dotfiles, fleetdeck, etc.), apply swarm-runbook conventions:
48
+
49
+ - **Sync-freeze**: check for active fleet operations before modifying host configs
50
+ - **FLEET_NODE**: use `$FLEET_NODE` in attestation comments (not `hostname -s`)
51
+ - **Staggered deployment**: changes that touch fleet host configs deploy via `chezmoi apply` on each host
52
+ - **Per-host verification**: verify on each host after deployment
53
+
54
+ ### 4. Post-implementation
55
+
56
+ - Post verification results as a comment on the issue
57
+ - Include: what was changed, how it was verified, any findings
58
+ - Do NOT close the issue — captain closes after review
59
+
60
+ ## Confidence rubric
61
+
62
+ The `issue-work` CLI estimates confidence from issue content. As an agent, you can override the CLI's estimate if you have additional context (e.g. you know the codebase well, or you've already explored the relevant code).
63
+
64
+ **Override up**: if you've already explored the code and are confident you can implement correctly, bump confidence (e.g. CLI says 88 but you've read the relevant module and the fix is obvious → proceed).
65
+
66
+ **Override down**: if the CLI says >= 95 but you see a pitfall it missed (security, fleet impact, cross-repo dependency), treat it as below threshold and triage-only.
67
+
68
+ ## Guardrails
69
+
70
+ - **PR required**: never push to the default branch
71
+ - **Single-issue branches**: one issue per branch/PR
72
+ - **Captain approval for**: closing issues, applying labels, breaking changes, security-sensitive code
73
+ - **Dry-run first**: always `--dry-run` on first pass
74
+ - **No auto-close**: captain closes issues manually after merge
75
+ - **No auto-label in MVP**: suggest labels in triage output but don't apply them
76
+
77
+ ## When to escalate
78
+
79
+ Escalate to captain when:
80
+ - The issue touches security-sensitive code (auth, secrets, networking)
81
+ - A breaking change is required
82
+ - Multi-repo coordination is needed
83
+ - The fix requires external API changes
84
+ - Confidence is borderline (90–94) and the area is unfamiliar
85
+
86
+ ## Integration with other skills
87
+
88
+ | Skill | Use |
89
+ |---|---|
90
+ | `gh-axi` | All GitHub API access (issue CRUD, comments, PRs) |
91
+ | `lfg` | Implementation engine — same branch→implement→PR flow |
92
+ | `swarm-runbook` | Fleet-slice conventions when repo is fleet-managed |
93
+ | `no-mistakes` | PR review gate (recommended but not required in MVP) |
94
+ | `dotfiles` | When target is phillias/dotfiles — use dotfiles commit/PR flow |