@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 +85 -2
- package/bin/common.mjs +144 -0
- package/bin/issue-work +343 -0
- package/package.json +11 -4
- package/skill/SKILL.md +94 -0
package/README.md
CHANGED
|
@@ -1,3 +1,86 @@
|
|
|
1
|
-
#
|
|
1
|
+
# issue-work
|
|
2
2
|
|
|
3
|
-
|
|
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.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
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 |
|