tissues 0.5.0 → 0.5.1

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
@@ -32,11 +32,11 @@ Requires Node.js >= 18.
32
32
  # Authenticate (auto-detects gh CLI token)
33
33
  tissues auth
34
34
 
35
- # Set your active repo
36
- tissues open
35
+ # Set your active repo (optional — saves typing --repo every time)
36
+ tissues use calebogden/tissues
37
37
 
38
- # Create an issue interactively
39
- tissues create
38
+ # Create an issue interactively (title as positional argument — no quotes needed)
39
+ tissues create fix the login bug
40
40
 
41
41
  # Check safety status before running in CI
42
42
  tissues status
@@ -54,13 +54,14 @@ Authenticate with GitHub. Auto-detects your `gh` CLI token if you're already log
54
54
  tissues auth
55
55
  ```
56
56
 
57
- ### `tissues open`
57
+ ### `tissues use`
58
58
 
59
- Set the active repository context. All subsequent commands use this repo unless overridden with `--repo`.
59
+ Set the active repository context. Optional — all commands also accept `--repo` directly. Once set, subsequent commands use this repo automatically unless overridden.
60
60
 
61
61
  ```bash
62
- tissues open
63
- # prompts: pick a repo from your GitHub account
62
+ tissues use calebogden/tissues # set by name
63
+ tissues use # prompts: pick a repo from your GitHub account
64
+ tissues use --repo calebogden/tissues # same, via flag
64
65
  ```
65
66
 
66
67
  ### `tissues create`
@@ -68,7 +69,7 @@ tissues open
68
69
  Create a new GitHub issue. Runs dedup checks and safety gates before creating anything.
69
70
 
70
71
  ```bash
71
- tissues create [options]
72
+ tissues create [title...] [options]
72
73
  ```
73
74
 
74
75
  **Options:**
@@ -78,7 +79,10 @@ tissues create [options]
78
79
  | `--repo <owner/name>` | Override active repo |
79
80
  | `--template <name>` | Template to use: `bug`, `feature`, `security`, `performance`, `refactor` |
80
81
  | `--title <title>` | Issue title (skips interactive prompt) |
81
- | `--body <text>` | Issue body / description (skips interactive prompt) |
82
+ | `--body <text>` | Issue body / description — the actual content describing the issue (skips interactive prompt) |
83
+ | `--instructions <text>` | AI enhancement instructions: guides how the AI writes the issue body (e.g. "keep it under 200 words", "use formal tone"). Does not appear in the created issue. Optional — skips interactive prompt when provided. |
84
+ | `--no-enhance` | Skip AI enhancement, use rendered template as-is |
85
+ | `--batch <file>` | Create multiple issues from a JSON file. Each item supports: `title`, `body`, `template`, `labels`, `agent`, `session` |
82
86
  | `--labels <labels>` | Comma-separated labels to apply |
83
87
  | `--agent <name>` | Agent identifier for attribution (default: `human`) |
84
88
  | `--session <id>` | Session ID for attribution and idempotency |
@@ -88,8 +92,8 @@ tissues create [options]
88
92
  **Examples:**
89
93
 
90
94
  ```bash
91
- # Interactive
92
- tissues create
95
+ # Interactive (title pre-filled from positional argument)
96
+ tissues create fix the login bug
93
97
 
94
98
  # Fully scripted (no prompts)
95
99
  tissues create \
@@ -97,6 +101,7 @@ tissues create \
97
101
  --template bug \
98
102
  --title "Login fails on Safari 17" \
99
103
  --body "Reproducible on fresh profile. Console shows CORS error." \
104
+ --instructions "Keep it under 150 words and include a clear reproduction checklist." \
100
105
  --labels "bug,P1"
101
106
 
102
107
  # From an AI agent
@@ -279,7 +284,7 @@ Template files support `{{variable}}` substitution:
279
284
  | Variable | Value |
280
285
  |---|---|
281
286
  | `{{title}}` | Issue title |
282
- | `{{description}}` | User's description |
287
+ | `{{description}}` | Issue description — the actual content describing the issue |
283
288
  | `{{agent}}` | Agent identifier |
284
289
  | `{{session}}` | Session ID |
285
290
  | `{{date}}` | ISO date (YYYY-MM-DD) |
@@ -320,11 +325,10 @@ Every issue created by tissues includes a machine-readable HTML comment at the b
320
325
  <!-- tissues-meta
321
326
  agent: claude-opus-4-6
322
327
  session: abc123
323
- pid: 84921
324
328
  trigger: cli-create
325
329
  fingerprint: sha256:3f2a1b...
326
330
  created_at: 2026-02-19T15:30:00.000Z
327
- created_via: tissues-cli/0.3.0
331
+ created_via: tissues-cli/0.5
328
332
  -->
329
333
  ```
330
334
 
@@ -336,6 +340,8 @@ The block is used by tissues to:
336
340
 
337
341
  You can pass additional fields via `--agent`, `--session`, and the programmatic API.
338
342
 
343
+ `pid` is available but opt-in — pass it explicitly via the programmatic API (`pid: process.pid`). It is omitted by default to avoid leaking process info into public issues.
344
+
339
345
  ---
340
346
 
341
347
  ## State Database
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tissues",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
4
4
  "description": "AI-enhanced GitHub issue creation with built-in safety guardrails. Wraps gh CLI with circuit breakers, rate limiting, dedup, and templates.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli.js CHANGED
@@ -3,7 +3,7 @@ import { createRequire } from 'module'
3
3
  const { version } = createRequire(import.meta.url)('../package.json')
4
4
  import { authCommand } from './commands/auth.js'
5
5
  import { configCommand } from './commands/config.js'
6
- import { openCommand } from './commands/open.js'
6
+ import { useCommand } from './commands/use.js'
7
7
  import { createCommand } from './commands/create.js'
8
8
  import { listCommand } from './commands/list.js'
9
9
  import { statusCommand } from './commands/status.js'
@@ -40,7 +40,7 @@ program
40
40
  requireGh()
41
41
 
42
42
  // Show active repo context on every command (except auth/open)
43
- if (name !== 'auth' && name !== 'open' && name !== 'login' && name !== 'status' && name !== 'switch' && name !== 'logout') {
43
+ if (name !== 'auth' && name !== 'use' && name !== 'login' && name !== 'status' && name !== 'switch' && name !== 'logout') {
44
44
  const activeRepo = store.get('activeRepo')
45
45
  if (activeRepo) {
46
46
  console.log(chalk.dim(`Working in: ${activeRepo}\n`))
@@ -63,7 +63,7 @@ program.hook('preAction', () => {
63
63
  })
64
64
  program.addCommand(authCommand)
65
65
  program.addCommand(configCommand)
66
- program.addCommand(openCommand)
66
+ program.addCommand(useCommand)
67
67
  program.addCommand(createCommand)
68
68
  program.addCommand(listCommand)
69
69
  program.addCommand(statusCommand)
@@ -88,14 +88,19 @@ function runPostCreateHook(hookCmd, ctx) {
88
88
  * Enhance issue body with AI. Currently a structured placeholder.
89
89
  * Wire up real AI (OpenAI / Anthropic) here.
90
90
  *
91
- * @param {string} title
92
- * @param {string} description
91
+ * @param {string} title - issue title
92
+ * @param {string} description - the actual issue content describing what the issue is about;
93
+ * this is rendered into the template body via {{description}} and forms the core of the issue
93
94
  * @param {string} templateBody - already-rendered template body to use as context
95
+ * @param {string} [instructions] - optional AI prompt instructions that guide *how* the AI
96
+ * writes or post-processes the issue body (e.g. "keep it under 200 words",
97
+ * "after creating, send to this webhook: https://..."). Does NOT appear in the issue itself.
94
98
  * @returns {Promise<string>}
95
99
  */
96
- async function enhanceWithAI(title, description, templateBody) {
100
+ async function enhanceWithAI(title, description, templateBody, instructions) {
97
101
  // TODO: wire up real AI (OpenAI / Anthropic)
98
- // templateBody is provided as context for the AI to refine
102
+ // - description goes INTO the issue body (rendered via {{description}} in the template)
103
+ // - instructions guide the AI behaviour but are NOT included in the output
99
104
  return templateBody
100
105
  }
101
106
 
@@ -164,7 +169,8 @@ async function runCreate(opts) {
164
169
  // -----------------------------------------------------------------------
165
170
  // 4. Get inputs
166
171
  // -----------------------------------------------------------------------
167
- const title = opts.title ?? (await input({ message: 'Issue title' }))
172
+ const title =
173
+ opts.title ?? (await input({ message: 'Issue title', default: opts._titleDefault }))
168
174
  if (!title || !title.trim()) {
169
175
  console.error(chalk.red('Title is required.'))
170
176
  throw new Error('Title is required')
@@ -173,7 +179,13 @@ async function runCreate(opts) {
173
179
  const description =
174
180
  opts.body ??
175
181
  (await input({
176
- message: 'Brief description (optional, used for AI enhancement)',
182
+ message: 'Issue description (optional)',
183
+ }))
184
+
185
+ const instructions =
186
+ opts.instructions ??
187
+ (await input({
188
+ message: 'Issue instruction (optional, guides AI enhancement)',
177
189
  }))
178
190
 
179
191
  // -----------------------------------------------------------------------
@@ -286,7 +298,7 @@ async function runCreate(opts) {
286
298
  if (opts.enhance !== false && !opts.dryRun) {
287
299
  const aiSpinner = ora('Enhancing with AI...').start()
288
300
  try {
289
- body = await enhanceWithAI(title, description, renderedTemplate)
301
+ body = await enhanceWithAI(title, description, renderedTemplate, instructions)
290
302
  aiSpinner.succeed('Enhanced')
291
303
  } catch {
292
304
  aiSpinner.warn('AI enhancement unavailable — using template as-is')
@@ -427,12 +439,14 @@ async function runCreate(opts) {
427
439
 
428
440
  export const createCommand = new Command('create')
429
441
  .description('Create a new GitHub issue')
442
+ .argument('[title...]', 'Issue title (positional shorthand for --title)')
430
443
  .option('--repo <repo>', 'Repository override (owner/name)')
431
444
  .option('--template <name>', 'Template to use (bug, feature, security, performance, refactor)')
432
445
  .option('--agent <name>', 'Agent identifier for attribution (default: human)')
433
446
  .option('--session <id>', 'Session ID for attribution')
434
447
  .option('--title <title>', 'Issue title (skips interactive prompt)')
435
448
  .option('--body <body>', 'Issue body / description (skips interactive prompt)')
449
+ .option('--instructions <text>', 'AI enhancement instruction — a prompt that guides how the AI writes the issue body (skips interactive prompt)')
436
450
  .option('--labels <labels>', 'Comma-separated labels to apply')
437
451
  .option('--force', 'Skip dedup warnings (still blocks on exact matches)')
438
452
  .option('--dry-run', 'Check dedup and safety without creating the issue')
@@ -441,7 +455,10 @@ export const createCommand = new Command('create')
441
455
  '--batch <file>',
442
456
  'Create multiple issues from a JSON file (title, body, template, labels, agent, session per item)',
443
457
  )
444
- .action(async (opts) => {
458
+ .action(async (titleArg, opts) => {
459
+ // Positional words → prefill the prompt (user can still edit)
460
+ // --title flag → skip the prompt entirely (scripted/batch use)
461
+ if (titleArg?.length && !opts.title) opts._titleDefault = titleArg.join(' ')
445
462
  // -----------------------------------------------------------------------
446
463
  // Batch mode
447
464
  // -----------------------------------------------------------------------
@@ -83,7 +83,7 @@ export const statusCommand = new Command('status')
83
83
  }
84
84
 
85
85
  if (!repo) {
86
- console.error(chalk.red('No active repo. Set one with: tissues open'))
86
+ console.error(chalk.red('No active repo. Set one with: tissues use <owner/repo>'))
87
87
  process.exit(1)
88
88
  }
89
89
 
@@ -3,7 +3,7 @@ import { pickRepo } from '../lib/repo-picker.js'
3
3
  import { setConfig } from '../lib/config.js'
4
4
  import chalk from 'chalk'
5
5
 
6
- export const openCommand = new Command('open')
6
+ export const useCommand = new Command('use')
7
7
  .description('Set the active repository context')
8
8
  .argument('[repo]', 'Repository in owner/name format (e.g. owner/repo)')
9
9
  .option('--repo <repo>', 'Repository in owner/name format (alias for positional argument)')
@@ -56,7 +56,7 @@ function nowISO() {
56
56
  * @typedef {object} AttributionOpts
57
57
  * @property {string} [agent] - agent identifier (e.g. 'claude-opus-4-6')
58
58
  * @property {string} [session] - session or conversation ID
59
- * @property {number} [pid] - process ID of the creating process
59
+ * @property {number} [pid] - process ID (opt-in; omitted by default)
60
60
  * @property {string} [model] - AI model used (if any)
61
61
  * @property {string} [trigger] - how the issue was created (e.g. 'cli-create')
62
62
  * @property {string} [fingerprint] - content fingerprint (sha256:...)
@@ -72,7 +72,8 @@ function nowISO() {
72
72
  * Build a normalized attribution metadata object from raw options.
73
73
  *
74
74
  * The returned object includes all provided fields plus automatic defaults
75
- * (`created_at`, `created_via`, `pid`). Undefined/null fields are omitted.
75
+ * (`created_at`, `created_via`). Undefined/null fields are omitted.
76
+ * `pid` is opt-in — it is only included if explicitly passed.
76
77
  *
77
78
  * @param {AttributionOpts} opts
78
79
  * @returns {object} metadata record
@@ -100,7 +101,7 @@ export function buildAttribution(opts = {}) {
100
101
  if (session != null) meta.session = String(session)
101
102
  if (model != null) meta.model = String(model)
102
103
 
103
- // Process info
104
+ // Process info (pid is opt-in — pass it explicitly to include)
104
105
  if (pid != null) meta.pid = Number(pid)
105
106
  meta.trigger = trigger != null ? String(trigger) : 'cli-create'
106
107
 
@@ -120,7 +121,7 @@ export function buildAttribution(opts = {}) {
120
121
 
121
122
  // Timestamps / versioning
122
123
  meta.created_at = createdAt ?? nowISO()
123
- meta.created_via = `tissues-cli/${PKG_VERSION}`
124
+ meta.created_via = `tissues-cli/${PKG_VERSION.split('.').slice(0, 2).join('.')}`
124
125
 
125
126
  return meta
126
127
  }
@@ -137,11 +138,10 @@ export function buildAttribution(opts = {}) {
137
138
  * <!-- tissues-meta
138
139
  * agent: claude-opus-4-6
139
140
  * session: abc123
140
- * pid: 12345
141
141
  * trigger: cli-create
142
142
  * fingerprint: sha256:deadbeef
143
143
  * created_at: 2026-02-19T15:30:00Z
144
- * created_via: tissues-cli/0.1.0
144
+ * created_via: tissues-cli/0.1
145
145
  * -->
146
146
  *
147
147
  * @param {AttributionOpts} opts
@@ -170,7 +170,7 @@ export function renderAttribution(opts = {}) {
170
170
  * Returns the parsed key/value pairs as a plain object, or `null` if no
171
171
  * attribution block is present in `issueBody`.
172
172
  *
173
- * Numeric fields (`pid`, `risk`, `complexity`, `confidence`) are coerced to
173
+ * Numeric fields (`risk`, `complexity`, `confidence`) are coerced to
174
174
  * numbers. The `context_tags` field is split back into an array.
175
175
  *
176
176
  * @param {string} issueBody - raw GitHub issue body markdown
package/src/lib/db.js CHANGED
@@ -277,6 +277,25 @@ export function getCircuitState(repo, agent = 'human') {
277
277
  }
278
278
  }
279
279
 
280
+ /**
281
+ * Increment the failure count for a circuit without opening it.
282
+ * Used to persist intermediate failures before the trip threshold is reached.
283
+ *
284
+ * @param {string} repo
285
+ * @param {string} [agent='human']
286
+ */
287
+ export function incrementFailureCount(repo, agent = 'human') {
288
+ const db = getDb()
289
+ const id = `${repo}:${agent}`
290
+ db.prepare(
291
+ `INSERT INTO circuit_breaker (id, repo, agent, status, failure_count, updated_at)
292
+ VALUES (?, ?, ?, 'closed', 1, datetime('now'))
293
+ ON CONFLICT(id) DO UPDATE SET
294
+ failure_count = failure_count + 1,
295
+ updated_at = datetime('now')`,
296
+ ).run(id, repo, agent)
297
+ }
298
+
280
299
  /**
281
300
  * Trip (open) the circuit breaker and set a cooldown window.
282
301
  *
package/src/lib/safety.js CHANGED
@@ -16,6 +16,7 @@ import {
16
16
  probeCircuit,
17
17
  recordRateEvent,
18
18
  countRecentEvents,
19
+ incrementFailureCount,
19
20
  getDb,
20
21
  } from './db.js'
21
22
 
@@ -206,10 +207,8 @@ export function recordFailure(repo, agent, config = {}) {
206
207
  `Cooldown: ${cfg.cooldownMinutes} minutes.`
207
208
  )
208
209
  } else {
209
- // Update failure count without tripping db.tripCircuit handles the trip;
210
- // for intermediate increments we record a 'failure' rate event so the count
211
- // is persisted across calls.
212
- recordRateEvent(repo, agent, 'failure')
210
+ // Persist intermediate failure count without opening the circuit yet
211
+ incrementFailureCount(repo, agent)
213
212
  }
214
213
  }
215
214