@gr8ful/spf 0.6.0 → 0.7.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
@@ -369,7 +369,7 @@ the Bitbucket identifier — finds no matching issues and looks exactly like
369
369
  nothing being configured at all.
370
370
 
371
371
  ```bash
372
- spf watch init # idempotently seed tracker state (no-op for Jira — see below); run this first
372
+ spf watch init # idempotently seed tracker state (labels are a no-op report on Jira — see below); run this first
373
373
  spf watch # foreground daemon; Ctrl-C drains in-flight claims first
374
374
  spf watch --once # one poll tick, then exit — good for cron
375
375
  spf watch --dry-run # log intended claims/transitions, mutate nothing
@@ -377,6 +377,18 @@ spf watch --dry-run # log intended claims/transitions, mutate nothing
377
377
 
378
378
  No GitHub App, no webhook, no Jira/Bitbucket app install — it's a plain REST poll against whichever combination is configured, same philosophy as the trace db's own polling contract. See [`assets/templates/`](assets/templates/) for full worked configs (also usable directly via `spf init --template <name>`), and `spf install-skill`'s installed skill (`roster.md`, `references/config.md`) for the field-by-field reference.
379
379
 
380
+ ### Priority, dependencies, and picking what's next
381
+
382
+ Among every `<prefix>:ready` issue, `spf watch` claims in this order:
383
+
384
+ 1. **Priority** — the `<prefix>:priority:p0|p1|p2|p3` label (`p0` drop-everything, `p2` the default, `p3` someday). `spf watch init` seeds all four. A human relabeling an issue is the whole override mechanism — there's no separate priority command.
385
+ 2. **Sibling affinity** — among issues at the same priority, one whose parent feature already has a sibling in flight goes first. This is what tends to finish one feature before starting the next without giving up the one-PR-per-story design (see "Refining specs" below): each story is still its own claim, its own worktree, its own PR — affinity only orders which claim happens next.
386
+ 3. **Creation order** (oldest first) — the final tiebreaker.
387
+
388
+ Before claiming anything, `spf watch` also checks the **frontier**: an issue's `blocked_by` dependencies (set by the refine lane, or by hand) must all carry `<prefix>:done` first. A leaf whose blockers aren't done yet is skipped, not blocked — it's simply reconsidered next tick, once the log line naming what it's waiting on stops applying.
389
+
390
+ This is a **label**, not this repo's own GitHub Projects v2 "Priority" field (if your board has one — Urgent/High/Medium/Low, say). `spf watch` never reads or writes Projects v2: no GraphQL, no `project` token scope, no Jira equivalent. If you use both, they're independent — nothing reconciles them, and `spf watch` obeys only the label. Keep them aligned yourself, or don't use the board field for this repo's issues.
391
+
380
392
  ### Refining specs (`watch.refine`)
381
393
 
382
394
  A product spec isn't individually workable — it needs to become a feature,
@@ -385,12 +397,12 @@ anything to claim. `watch.refine` is a second lane over the same poll loop
385
397
  that does exactly that: it polls `<prefix>:spec-ready`, runs a decomposition
386
398
  chain (`refine` by default — `request → scout → refiner → publish`) against
387
399
  the spec in its own worktree, and publishes what it produces as real tracker
388
- issues — a feature/epic container plus story/bug/task leaves, linked via
389
- GitHub's native sub-issue hierarchy.
400
+ issues — a feature/epic container plus story/bug/task leaves, linked into a
401
+ real hierarchy: GitHub's native sub-issues API, or Jira's `parent` field.
390
402
 
391
403
  ```yaml
392
404
  watch:
393
- issue_provider: github # required — issue authoring isn't implemented for Jira yet
405
+ issue_provider: github # or jira both implement issue authoring
394
406
  repo: owner/name
395
407
  refine:
396
408
  enabled: true
@@ -398,14 +410,58 @@ watch:
398
410
  concurrency: 1 # this lane's own budget, independent of watch.concurrency
399
411
  ```
400
412
 
413
+ On Jira, every `RefinedIssue.kind` (`epic`/`feature`/`story`/`bug`/`task`) maps
414
+ to a real Jira issue type through `watch.jira.issue_types` — defaults
415
+ `epic`/`feature` → `Epic`, `story` → `Story`, `bug` → `Bug`, `task` → `Task`,
416
+ overridable per kind since real projects rename or customize these:
417
+
418
+ ```yaml
419
+ watch:
420
+ issue_provider: jira
421
+ jira:
422
+ base_url: https://your-domain.atlassian.net
423
+ project_key: PROJ
424
+ issue_types: # optional — shown are the defaults
425
+ epic: Epic
426
+ feature: Epic
427
+ story: Story
428
+ bug: Bug
429
+ task: Task
430
+ refine:
431
+ enabled: true
432
+ ```
433
+
434
+ Hierarchy uses Jira's modern `parent` field only (no legacy "Epic Link"
435
+ custom-field support) — this works on team-managed projects and on
436
+ company-managed projects with Jira's current issue-hierarchy setting; a
437
+ project not configured for it surfaces Jira's own API error, unmodified.
438
+ One accepted platform limitation: Jira doesn't support Epic-under-Epic
439
+ nesting the way GitHub's sub-issues API supports up to 8 levels, so a
440
+ `feature` node parented under another `epic`/`feature` (both `Epic` by
441
+ default) will fail at publish time on Jira specifically — a real platform
442
+ difference, not a bug. Both `spf watch init` and `spf watch`'s own startup
443
+ check validate `watch.jira.issue_types` against the real project before
444
+ anything unattended runs — see "Jira" below.
445
+
401
446
  Every generated issue carries a `<prefix>:type:epic|feature|story|bug|task`
402
- label. A container (a feature/epic something else names it as `parent`)
403
- gets only that label; a leaf additionally gets `<prefix>:refined` **never**
404
- `<prefix>:ready`. Promoting a leaf to `<prefix>:ready` is a deliberate human
405
- decision: the refine lane never auto-promotes anything, so a spec fanning out
406
- into twenty stories doesn't turn into twenty unattended chain runs and twenty
407
- PRs with nobody having looked at the breakdown first. Once you do promote a
408
- leaf, the existing build lane picks it up completely unchanged.
447
+ label AND a `<prefix>:priority:p0|p1|p2|p3` label (see "Priority, dependencies,
448
+ and picking what's next" above) the refiner proposes the priority, a human
449
+ can relabel it before promoting. A container (a feature/epic — something
450
+ else names it as `parent`) gets only those two labels; a leaf additionally
451
+ gets `<prefix>:refined` **never** `<prefix>:ready`. Promoting a leaf to
452
+ `<prefix>:ready` is a deliberate human decision: the refine lane never
453
+ auto-promotes anything, so a spec fanning out into twenty stories doesn't
454
+ turn into twenty unattended chain runs and twenty PRs with nobody having
455
+ looked at the breakdown first. Once you do promote a leaf, the existing build
456
+ lane picks it up completely unchanged — including the frontier check: a
457
+ promoted leaf isn't claimed until every issue named in its `blocked_by` is
458
+ `<prefix>:done`.
459
+
460
+ A spec issue's own `<prefix>:priority:pN` label, if it has one, reaches the
461
+ refiner as a **ceiling**: no generated node may be more urgent than the spec
462
+ itself (enforced regardless of what the refiner emits — see
463
+ `core/refine.ts`'s `publish()`). `spf refine ... --priority p1` sets the same
464
+ ceiling for a bare manual run with no spec issue to read a label from.
409
465
 
410
466
  Grounding the decomposition in real code is structural, not just prose: a
411
467
  `scout` phase maps the subsystems the spec touches before the refiner ever
@@ -421,11 +477,11 @@ model, an external dependency choice, a UX contract, a breaking change, or
421
477
  anything that would contradict an existing ADR — see
422
478
  `assets/prompts/refiner/system.md`'s "Ask, don't decide"). When it hits real
423
479
  ambiguity it raises questions instead of publishing a partial tree, and the
424
- spec moves through an extra loop before it reaches `done`:
480
+ spec moves through an extra loop before it's even fully published:
425
481
 
426
482
  ```text
427
- spec-ready → refining ──┬─→ done / blocked (published a tree)
428
- └─→ needs-feedback (raised questions)
483
+ spec-ready → refining ──┬─→ spec-in-progress → done / blocked (published a tree, THEN tracked to completion)
484
+ └─→ needs-feedback (raised questions)
429
485
  │ a human answers in the issue's comments,
430
486
  │ then adds continue-refinement
431
487
 
@@ -440,19 +496,55 @@ already had, and it can loop through as many rounds as it takes; there's no
440
496
  cap. If channels are configured (see "Notifications" below), a
441
497
  `spec_needs_feedback` event fires every round.
442
498
 
443
- The spec issue itself moves through a `spec-ready refining
444
- needs-feedback refining done`/`blocked` lifecycle (the `needs-feedback
445
- refining` loop only when the refiner actually escalates), and a summary
446
- comment lists every issue it created noting how many rounds of feedback it
447
- took, if any. Once a spec reaches `done`, `spf watch` also closes the issue
448
- on the tracker (best-effort: a tracker or a failed close never turns a
449
- successfully refined spec back to `blocked`). Try it by hand first, against
450
- a real spec, before turning on the daemon:
499
+ **Publishing a tree does not mean the spec is done.** A product manager
500
+ watching a spec's status needs "done" to mean the work is actually finished,
501
+ not merely that a decomposition happened so once the refiner publishes,
502
+ the spec moves to `<prefix>:spec-in-progress`, not `<prefix>:done`. It stays
503
+ there with a summary comment listing every issue it created, noting how
504
+ many rounds of feedback it took, if any until **every one of those
505
+ issues** reaches `<prefix>:done` (a leaf that lands, or a feature/epic
506
+ container once `rollUp` has already finished it — see "Container roll-up"
507
+ below). Only then does `spf watch` transition the spec to `<prefix>:done`,
508
+ post a closing comment, and close it on the tracker where the tracker
509
+ supports closing at all — GitHub does (best-effort: a failed close never
510
+ turns completed work back into `blocked`); Jira doesn't have this wired up,
511
+ so a Jira spec still relabels and comments correctly, just stays open. A
512
+ `spec_done` event fires either way, distinct from the earlier
513
+ `spec_refined`. A spec whose
514
+ generated leaf never gets promoted, or whose work stalls `blocked`, simply
515
+ stays `spec-in-progress` — which is the truthful state, not a bug.
516
+
517
+ The spec issue's full lifecycle: `spec-ready → refining → needs-feedback ⇄
518
+ refining → spec-in-progress → done`/`blocked` (the `needs-feedback ⇄
519
+ refining` loop only when the refiner actually escalates). Try it by hand
520
+ first, against a real spec, before turning on the daemon:
451
521
 
452
522
  ```bash
453
- spf refine "<spec text or path/to/spec.md>" --issue 42 # --issue renders a "## Parent: #42" back-reference
523
+ spf refine "<spec text or path/to/spec.md>" --issue 42 # --issue renders a "## Parent" section, "Decomposed from #42."
524
+ spf refine "<spec text or path/to/spec.md>" --issue 42 --priority p1 # --priority clamps every generated node to p1 or less urgent
454
525
  ```
455
526
 
527
+ #### Container roll-up
528
+
529
+ A feature or epic isn't itself a unit of work, so it's never claimed and
530
+ never opens a PR — but it isn't abandoned once its children exist, either.
531
+ Every time a leaf's PR merges, `spf watch` checks that leaf's parent: once
532
+ **every** child under a container carries `<prefix>:done`, the container
533
+ gets a summary comment (every child it rolled up, by id), transitions to
534
+ `<prefix>:done`, and closes on the tracker if the tracker supports closing
535
+ at all (GitHub does; Jira doesn't have this wired up, so a Jira container
536
+ still relabels and comments, just stays open) — then the same check runs on
537
+ *its* parent, so an epic of features rolls up once its last feature does.
538
+ A container with even one unfinished child (including one still sitting at
539
+ `<prefix>:refined`, never promoted) is left exactly as it is; nothing times
540
+ out or force-closes it.
541
+
542
+ Roll-up needs to read back the hierarchy `publish()` created — GitHub's
543
+ native sub-issues API, or a Jira `parent = "<id>"` JQL search — so it works
544
+ on both providers today, same as authoring itself. A tracker that isn't
545
+ authoring-capable at all gets a logged no-op, not a startup failure, since
546
+ the build lane still works fine without roll-up.
547
+
456
548
  `spf watch init` seeds the type labels alongside the state ones — **re-run
457
549
  it** after upgrading to this version, so it can create the new
458
550
  `<prefix>:needs-feedback` and `<prefix>:continue-refinement` labels. This
@@ -492,7 +584,7 @@ export JIRA_API_TOKEN=... # id.atlassian.com -> Security -> API tokens
492
584
 
493
585
  State is modeled as Jira **labels** (`<prefix>:ready`, etc.), mirroring GitHub exactly, rather than native workflow status transitions — the latter would need per-project transition-id mapping, since workflows vary by project/scheme; labels work identically everywhere with zero per-project setup. One caveat: colons are a legal Jira label character and JQL matches on them fine, but they won't show up in Jira's own label autocomplete UI — cosmetic only.
494
586
 
495
- `spf watch init` is a no-op here (Jira labels are freeform strings with no color/description registry to seed, unlike GitHub's) — it just reports the labels this run will use.
587
+ `spf watch init` still doesn't create any labels here (Jira labels are freeform strings with no color/description registry to seed, unlike GitHub's) — it reports the labels this run will use. But with `watch.refine.enabled`, it now also validates `watch.jira.issue_types` against the real project's issue types, read-only, and exits non-zero on a mismatch — see "Refining specs" above.
496
588
 
497
589
  ### Bitbucket (`code_host: bitbucket`)
498
590
 
@@ -527,9 +619,12 @@ issues, watch errors, and a spec needing feedback (`spec_needs_feedback` —
527
619
  see "Human-in-the-loop escalation" above; it's `error`-level on purpose, the
528
620
  same class of event as a blocked issue, so an `errors`-scope channel sees it
529
621
  too); `all` adds every milestone — run started/finished, issue claimed, PR
530
- opened, issue done. A channel's own `events` overrides the top-level scope
531
- for just that channel. `spf doctor` reports whether each configured
532
- channel's env var is set.
622
+ opened, issue done, a container's roll-up (`feature_done` see "Container
623
+ roll-up" above), and a spec reaching actual completion (`spec_done`
624
+ distinct from `spec_refined`, which fires the moment a tree is published;
625
+ see "Human-in-the-loop escalation" above). A channel's own `events`
626
+ overrides the top-level scope for just that channel. `spf doctor` reports
627
+ whether each configured channel's env var is set.
533
628
 
534
629
  The webhook URL is a secret and lives only in `.env` — `webhook_url_env`
535
630
  names the key, never the URL itself, matching `GITHUB_TOKEN`/
@@ -31,7 +31,7 @@ You will hit real ambiguity: places where the spec (or the codebase, or both) ge
31
31
  - **Non-functional targets** — performance, scale, availability numbers the spec doesn't state.
32
32
  - **Anything that would contradict an existing ADR.**
33
33
 
34
- You MAY decide these yourself, following whatever pattern the codebase already uses — that's ordinary judgment, not ambiguity: naming, file placement, test framework and layout, internal module structure, and the ordering of independent slices.
34
+ You MAY decide these yourself, following whatever pattern the codebase already uses — that's ordinary judgment, not ambiguity: naming, file placement, test framework and layout, internal module structure, and the ordering of independent slices (defer to the `priority` you assign each one — see "Priority" below — rather than an arbitrary reading order).
35
35
 
36
36
  When you escalate, ask **everything you need in one batch** — don't trickle questions across rounds when you could have asked them all up front. Emit **no `issues`** in a round where you're asking questions; the two are mutually exclusive and a gate enforces it. For each question, give a human enough to answer in a word or two:
37
37
 
@@ -61,6 +61,16 @@ Emit a **flat list**, not nested JSON: each node names its `parent` by another n
61
61
 
62
62
  Give every node its `blocked_by`: the other nodes' `key`s that must land first. A node with no blockers can start immediately — leave `blocked_by` empty rather than inventing an order where none is required. The factory works the **frontier**: any leaf whose blockers are all done. For a purely linear chain that means top to bottom; for anything wider, only real dependencies belong in `blocked_by` — an artificial one just stalls the frontier.
63
63
 
64
+ ## Priority
65
+
66
+ Give every node a `priority`: `p0` (drop everything — a broken promise to users, or blocking everything else), `p1` (the spec's core value — the slices without which it isn't shipped), `p2` (the default — real scope, can wait a cycle), or `p3` (worth writing down, not worth scheduling yet). This is independent of `blocked_by` — a `p0` slice still waits for its blockers, same as anything else; priority decides scheduling ORDER among leaves that are otherwise both ready to start, not which one is allowed to start.
67
+
68
+ Three rules, all enforced, not just suggested:
69
+
70
+ - **A container takes the urgency of its most urgent child.** Don't set a feature's priority independently of the stories under it — pick the highest urgency among its children.
71
+ - **No node may be more urgent than its parent.** A `p0` story under a `p3` feature is a decomposition mistake, not a valid tree — a gate rejects it.
72
+ - **When the prompt states the spec's own priority, it is a CEILING for every node you produce — never a floor.** Deviate downward freely (a `p1` spec can still have `p3` polish tucked inside it); nothing you produce may be more urgent than the spec itself. This is enforced by the harness at publish time regardless of what you emit, so state the priority you actually mean — a violation is silently clamped, not sent back as a correction.
73
+
64
74
  ## Wide refactors — the one exception to vertical slicing
65
75
 
66
76
  A **wide refactor** is one mechanical change (rename a column, retype a shared symbol) whose blast radius fans across the codebase, so a single edit breaks thousands of call sites and no vertical slice can land green on its own. Sequence it as **expand -> migrate -> contract**, each stage its own leaf(s):
@@ -8,6 +8,8 @@
8
8
 
9
9
  `prompt` may include a `## Discussion on the spec issue` section — the spec issue's own comment thread, if it has one. On a first pass this is whatever discussion already existed before the spec was labeled ready; on a resumed pass (after you raised questions and a human answered) it's split into "Answers to your open questions (round N)" and "Earlier discussion." Treat the answers section as authoritative — see your system instructions on resuming.
10
10
 
11
+ `prompt` may also include a `## Priority` section, when the spec issue itself carries a priority label. Treat that as a **ceiling** for every node you produce — see your system instructions on priority. Its absence means no ceiling, not "assume p2."
12
+
11
13
  ### previous_envelope
12
14
 
13
15
  {{previous_envelope}}
@@ -46,7 +48,8 @@ Respond with ONLY valid JSON matching `RefineOutput` — no prose before or afte
46
48
  "title": "<feature title>",
47
49
  "body": "## What to build\n\n<end-to-end behavior this feature covers>\n\n## Acceptance criteria\n\n- [ ] <criterion>",
48
50
  "parent": "",
49
- "blocked_by": []
51
+ "blocked_by": [],
52
+ "priority": "p1"
50
53
  },
51
54
  {
52
55
  "key": "S1",
@@ -54,7 +57,8 @@ Respond with ONLY valid JSON matching `RefineOutput` — no prose before or afte
54
57
  "title": "<leaf title>",
55
58
  "body": "## What to build\n\n<end-to-end behavior from the user's perspective>\n\n## Acceptance criteria\n\n- [ ] <criterion>\n- [ ] <criterion>",
56
59
  "parent": "F1",
57
- "blocked_by": []
60
+ "blocked_by": [],
61
+ "priority": "p1"
58
62
  },
59
63
  {
60
64
  "key": "S2",
@@ -62,7 +66,8 @@ Respond with ONLY valid JSON matching `RefineOutput` — no prose before or afte
62
66
  "title": "<leaf title>",
63
67
  "body": "## What to build\n\n<the fix, described end-to-end>\n\n## Acceptance criteria\n\n- [ ] <criterion>",
64
68
  "parent": "F1",
65
- "blocked_by": ["S1"]
69
+ "blocked_by": ["S1"],
70
+ "priority": "p2"
66
71
  }
67
72
  ],
68
73
  "questions": []
@@ -99,4 +104,5 @@ Rules on the shape (enforced by a gate — a violation sends this back to you as
99
104
  - `parent` is another node's `key`, or `""` for a top-level feature/epic.
100
105
  - `blocked_by` is a list of other nodes' `key`s — real dependencies only, and no cycles (through `parent` or `blocked_by`, or both together).
101
106
  - `body` is `## What to build` then `## Acceptance criteria` only — no "Blocked by" or "Parent" section; those are rendered for you once every node has a real issue number.
107
+ - `priority` is one of `p0`|`p1`|`p2`|`p3`, defaulting to `p2` if you omit it. No node may be more urgent than its parent, and no node may exceed the spec's priority where the `## Priority` section states one — a violation is clamped, not sent back as a correction, so state the priority you actually mean.
102
108
  - `questions[].id` is your own local id for this round, unique within `questions` — stable enough that, on a resumed run, an answer in the thread can be matched back to the question it answers.
@@ -155,7 +155,8 @@ Full mechanism: the main README's "`spf watch`" section. Field reference:
155
155
  | `concurrency` | int ≥1 | Max issues claimed and run at once, the build lane's own budget (independent of `refine.concurrency`). Default `2`. |
156
156
  | `chain_options` | map of string -> string | Options passed straight through to `chain` (and `refine.chain`) for every unattended dispatch — the same shape an interactive `spf <chain> --suite <name>` builds, e.g. `{suite: strict}` or `{agent: some-agent}`. Default `{}`. Only useful for a chain whose behavior actually reads the option (a step-derived chain's `--suite`; an imperative chain ignores an option it doesn't know about). |
157
157
  | `jira.base_url` / `jira.project_key` | string | Only consulted when `issue_provider: jira`. |
158
- | `refine.enabled` | bool | Turns on the second lane: decompose a `<prefix>:spec-ready` product spec into a feature/story-or-bug tree of real issues, instead of running `chain` against it directly (a spec isn't individually workable). Default `false` off by default, so an existing `watch:` config is unaffected by upgrading. Needs `issue_provider: github` `spf watch` fails loudly at startup otherwise, since issue authoring (create + link a hierarchy) isn't implemented for Jira yet. |
158
+ | `jira.issue_types` | map: `epic`/`feature`/`story`/`bug`/`task` -> string | Only consulted when `issue_provider: jira` AND `refine.enabled`. What each `RefinedIssue.kind` creates as on Jira defaults `epic`/`feature` `Epic`, `story` `Story`, `bug` `Bug`, `task` `Task`, overridable per kind. Validated against the real project by both `spf watch init` and `spf watch`'s own startup check. |
159
+ | `refine.enabled` | bool | Turns on the second lane: decompose a `<prefix>:spec-ready` product spec into a feature/story-or-bug tree of real issues, instead of running `chain` against it directly (a spec isn't individually workable). Default `false` — off by default, so an existing `watch:` config is unaffected by upgrading. Needs `issue_provider: github` or `"jira"` — both implement issue authoring (create + link a hierarchy); any other value fails loudly at startup. |
159
160
  | `refine.chain` | string | Which registered chain runs per claimed spec. Default `refine`. |
160
161
  | `refine.concurrency` | int ≥1 | The refine lane's own budget, separate from `concurrency`. Default `1`. |
161
162
 
@@ -180,23 +181,60 @@ additionally gets `<prefix>:refined`, so a human can review and promote it to
180
181
  `<prefix>:ready` when it's worth building — the refine lane never
181
182
  auto-promotes anything.
182
183
 
184
+ A third label vocabulary — `<prefix>:priority:p0|p1|p2|p3` — is what
185
+ `claimNewWork` actually schedules `<prefix>:ready` issues by: priority first,
186
+ then sibling affinity (a leaf whose parent already has a sibling in flight
187
+ goes first), then creation order. The refiner proposes a priority per node
188
+ (clamped to the spec's own priority, when the spec issue has one, as a
189
+ ceiling — never a floor); a human can relabel before promoting, which is the
190
+ entire override mechanism. Independently, a `<prefix>:ready` leaf whose
191
+ `blocked_by` isn't fully `<prefix>:done` is skipped until it is — the
192
+ frontier check. This label is separate from any GitHub Projects v2
193
+ "Priority" field a repo's board might have; `spf watch` never touches
194
+ Projects v2, so the two are unreconciled if you use both.
195
+
183
196
  The `refine` chain grounds its decomposition with a `scout` phase before the
184
197
  refiner runs, so `scout` is a required agent for it — a roster that pruned
185
198
  it fails `spf watch` startup by name.
186
199
 
187
200
  The refine lane's own state machine has an extra loop beyond
188
- `spec-ready → refining → done`/`blocked`: when the refiner raises material
189
- ambiguity instead of a tree (see `assets/prompts/refiner/system.md`'s "Ask,
190
- don't decide"), the spec moves to `<prefix>:needs-feedback` with a comment
191
- naming its questions, instead of publishing anything. A human answers in the
192
- issue's comments and adds `<prefix>:continue-refinement`; `spf watch` claims
193
- that label back into `refining` and resumes the **same** `adw_id` — the
194
- comment thread (split into "answers to your open questions" and "earlier
195
- discussion") is folded into the resumed prompt, and the refiner's own
196
- coding-agent session continues rather than starting cold. This can loop any
197
- number of rounds. `spf watch init` must be re-run after upgrading to this
198
- version to seed the two new labels. Once a spec is fully published it's also
199
- closed on the tracker (GitHub only, best-effort), not just labeled `done`.
201
+ `spec-ready → refining → spec-in-progress → done`/`blocked`: when the
202
+ refiner raises material ambiguity instead of a tree (see
203
+ `assets/prompts/refiner/system.md`'s "Ask, don't decide"), the spec moves to
204
+ `<prefix>:needs-feedback` with a comment naming its questions, instead of
205
+ publishing anything. A human answers in the issue's comments and adds
206
+ `<prefix>:continue-refinement`; `spf watch` claims that label back into
207
+ `refining` and resumes the **same** `adw_id` the comment thread (split
208
+ into "answers to your open questions" and "earlier discussion") is folded
209
+ into the resumed prompt, and the refiner's own coding-agent session
210
+ continues rather than starting cold. This can loop any number of rounds.
211
+
212
+ Publishing a tree does NOT mean the spec is done: `<prefix>:spec-in-progress`
213
+ is where a spec lands right after publish, and it stays there — with a
214
+ summary comment listing every issue created — until **every one of those
215
+ issues** is itself `<prefix>:done` (`finishTrackedSpecs`, polled every tick,
216
+ reads `WatchMarker.refined` back to check). Only then does it move to
217
+ `<prefix>:done`, get a closing comment, and close on the tracker (GitHub
218
+ only, best-effort) — firing `spec_done`, distinct from the earlier
219
+ `spec_refined`. This exists so a spec's tracker status means what a product
220
+ manager expects it to mean: "done" is the work being finished, not merely
221
+ having been decomposed. `spf watch init` must be re-run after upgrading to
222
+ this version to seed the new labels.
223
+
224
+ Container roll-up mirrors that same "close, don't just label" behavior for a
225
+ generated feature/epic: once every child under it carries `<prefix>:done`
226
+ (checked reactively, each time a leaf's PR merges), the container gets a
227
+ summary comment and transitions to `<prefix>:done` — then the check repeats
228
+ on ITS OWN parent, so an epic rolls up once its last feature does. A
229
+ container with one unfinished child (including a leaf still sitting at
230
+ `<prefix>:refined`, never promoted) is left alone; nothing forces it. This
231
+ needs a native hierarchy read-back — GitHub's sub-issues API, or a Jira
232
+ `parent = "<id>"` JQL search — so it works on both today; a tracker that
233
+ implements neither gets a logged no-op, not a startup failure, since the
234
+ build lane still works fine without roll-up. Whether the container actually
235
+ CLOSES on the tracker (not just relabels) depends on `closeIssue` being
236
+ implemented at all — GitHub yes, Jira no (its own resolution workflow isn't
237
+ wired up), same asymmetry as leaf and spec completion already have on Jira.
200
238
 
201
239
  ### `notifications`
202
240
 
@@ -77,8 +77,12 @@ agents:
77
77
  # # suite: strict
78
78
  # # Optional second lane: decompose a spf:spec-ready product spec into a
79
79
  # # feature/story-or-bug tree of real issues instead of building it
80
- # # directly. Off by default; needs issue_provider: github (the default
81
- # # above) issue authoring isn't implemented for Jira yet.
80
+ # # directly. Off by default; needs issue_provider: github OR jira — both
81
+ # # support issue authoring (create + link a hierarchy). On Jira, each
82
+ # # RefinedIssue.kind maps to a real Jira issue type via
83
+ # # watch.jira.issue_types (defaults: epic/feature -> Epic, story -> Story,
84
+ # # bug -> Bug, task -> Task) — spf watch init validates this against the
85
+ # # real project when refine is enabled.
82
86
  # refine:
83
87
  # enabled: true
84
88
  # chain: refine
@@ -316,33 +316,6 @@ export declare function refine(opts?: {
316
316
  retries?: number;
317
317
  extraGates?: string[];
318
318
  }): Step;
319
- /**
320
- * Create the tree `refine()` produced on the tracker, in dependency order,
321
- * and link each node to its parent. A `code` phase, not an agent one — the
322
- * decision-making (topological order, label assignment, `## Blocked by`
323
- * rendering) is `core/refine.ts`'s job; this step is sequencing only, per
324
- * SKILL.md's "chains stay thin" rule. Requires a preceding refine() step.
325
- *
326
- * When the refiner raised material ambiguity instead of a tree
327
- * (`envelope.questions` non-empty — `gates.refinementWellFormed` already
328
- * guarantees `issues` is empty whenever that's true), this publishes
329
- * nothing: it writes those questions to
330
- * `<context_handoff_dir>/refine_questions.json` instead of
331
- * `refine_publish.json` and returns. That is a successful phase outcome, not
332
- * a failure — escalating to a human is a legitimate way for this chain to
333
- * end, same as publishing a tree is.
334
- *
335
- * Writes what it created (or asked) to `<context_handoff_dir>/`, one of
336
- * `refine_publish.json` or `refine_questions.json` — the side channel
337
- * `cli/commands/watch.ts`'s `runRefine` reads after the chain returns, since
338
- * a chain's own return value is just an exit code. `spf watch`'s own
339
- * marker/comment/transition bookkeeping for the spec issue (including
340
- * posting the questions and moving it to `needs-feedback`) lives entirely in
341
- * `core/watch.ts`'s `runSpec`/`escalateSpec`, never here — a bare `spf
342
- * refine` run (no daemon, no spec issue in play) still needs this step to
343
- * work standalone, and just leaves the questions on disk for a human to
344
- * read.
345
- */
346
319
  export declare function publishIssues(opts?: {
347
320
  description?: string;
348
321
  }): Step;
@@ -706,6 +706,23 @@ export function refine(opts = {}) {
706
706
  * work standalone, and just leaves the questions on disk for a human to
707
707
  * read.
708
708
  */
709
+ /**
710
+ * `state.options["priority"]` -> a `RefinedPriority` ceiling, or `null` when
711
+ * unset — the generic chain-options path (`cli/commands/run.ts`'s
712
+ * `--priority`, or `core/watch.ts`'s `runSpec` threading a spec's own
713
+ * `<prefix>:priority:pN` label) is the ONLY place this string enters the
714
+ * system, so it's validated here rather than trusted: a typo'd or stale
715
+ * `--priority urgent` fails the phase with a clear message instead of
716
+ * silently publishing every node at its own, un-clamped priority.
717
+ */
718
+ function parsePriorityOption(raw) {
719
+ if (raw === undefined)
720
+ return null;
721
+ if (raw !== "p0" && raw !== "p1" && raw !== "p2" && raw !== "p3") {
722
+ throw new Error(`publishIssues(): --priority ${JSON.stringify(raw)} is not one of p0|p1|p2|p3`);
723
+ }
724
+ return raw;
725
+ }
709
726
  export function publishIssues(opts = {}) {
710
727
  preflightDescription("publish", opts.description);
711
728
  const fn = async (run, state) => {
@@ -714,6 +731,7 @@ export function publishIssues(opts = {}) {
714
731
  throw new Error("publishIssues() requires a preceding refine() step in the chain's step list");
715
732
  }
716
733
  const questions = envelope.questions ?? [];
734
+ const priorityCeiling = parsePriorityOption(state.options["priority"]);
717
735
  await run.phase(makePhaseParams({
718
736
  name: "publish",
719
737
  kind: "code",
@@ -729,9 +747,10 @@ export function publishIssues(opts = {}) {
729
747
  const created = await refineLib.publish(tracker, envelope.issues, {
730
748
  labelPrefix: run.cfg.watch.label_prefix,
731
749
  specIssueId: state.issue_id,
750
+ priorityCeiling,
732
751
  });
733
752
  writeFileSync(path.join(run.context_handoff_dir, "refine_publish.json"), JSON.stringify(created.map((c) => ({ id: c.issue.id, title: c.issue.title, kind: c.kind, isLeaf: c.isLeaf })), null, 2));
734
- ph.log({ created: created.length, leaves: created.filter((c) => c.isLeaf).length });
753
+ ph.log({ created: created.length, leaves: created.filter((c) => c.isLeaf).length, priority_ceiling: priorityCeiling });
735
754
  });
736
755
  };
737
756
  return makeStep(fn, { label: "code(publish)" });
@@ -534,9 +534,11 @@ export async function doctorCommand(argv) {
534
534
  if (cfg.watch.refine.enabled) {
535
535
  const refineChain = findChain(cfg.watch.refine.chain);
536
536
  check(report, "watch.refine.chain", Boolean(refineChain), refineChain ? `${cfg.watch.refine.chain}${refineChain.source ? ` (repo: ${repoChainLabel(refineChain.source)})` : ""}` : `"${cfg.watch.refine.chain}" is not a registered chain`);
537
- check(report, "watch.refine issue authoring", cfg.watch.issue_provider === "github", cfg.watch.issue_provider === "github"
537
+ check(report, "watch.refine issue authoring", cfg.watch.issue_provider === "github" || cfg.watch.issue_provider === "jira", cfg.watch.issue_provider === "github"
538
538
  ? "github supports issue authoring (createIssue/sub-issues)"
539
- : `watch.issue_provider is ${JSON.stringify(cfg.watch.issue_provider)} the refine lane needs "github" (Jira issue authoring isn't implemented yet)`);
539
+ : cfg.watch.issue_provider === "jira"
540
+ ? "jira supports issue authoring (createIssue/parent field) — run `spf watch init` to validate watch.jira.issue_types against the real project"
541
+ : `watch.issue_provider is ${JSON.stringify(cfg.watch.issue_provider)} — the refine lane needs "github" or "jira"`);
540
542
  }
541
543
  }
542
544
  // OTel span export: informational in every direction. It is off unless
@@ -99,7 +99,10 @@ const STARTER_CONFIG = `# .spf/spf.config.yaml — merged ON TOP of spf's packag
99
99
  # base_branch: main
100
100
  # # Optional second lane: decompose a spf:spec-ready product spec into a
101
101
  # # feature/story-or-bug tree of real issues. Off by default; needs
102
- # # issue_provider: github — issue authoring isn't implemented for Jira yet.
102
+ # # issue_provider: github OR jira both support issue authoring. On Jira,
103
+ # # each kind maps to a real issue type via watch.jira.issue_types
104
+ # # (defaults: epic/feature -> Epic, story -> Story, bug -> Bug,
105
+ # # task -> Task) — \`spf watch init\` validates this against the project.
103
106
  # refine:
104
107
  # enabled: true
105
108
  # chain: refine
@@ -3,9 +3,9 @@ import * as paths from "../../core/paths.js";
3
3
  import { parseCli, resolvePrompt } from "../../core/utils.js";
4
4
  import { runChain } from "../../chains/index.js";
5
5
  import { isInteractive } from "../ask.js";
6
- const KNOWN_OPTIONS = ["config", "adw-id", "cwd", "agent", "base", "issue", "suite"];
6
+ const KNOWN_OPTIONS = ["config", "adw-id", "cwd", "agent", "base", "issue", "suite", "priority"];
7
7
  export function usageFor(chain) {
8
- return `usage: spf ${chain.name} "<prompt or path/to/prompt.md>" [--config <path>] [--adw-id <id>] [--cwd <dir>] [--suite <name>]`;
8
+ return `usage: spf ${chain.name} "<prompt or path/to/prompt.md>" [--config <path>] [--adw-id <id>] [--cwd <dir>] [--suite <name>] [--priority p0|p1|p2|p3]`;
9
9
  }
10
10
  export async function dispatchChain(chain, argv) {
11
11
  const { positionals, options } = parseCli(argv, KNOWN_OPTIONS);
@@ -52,5 +52,12 @@ export async function dispatchChain(chain, argv) {
52
52
  chainOptions["base"] = options["base"];
53
53
  if (options["suite"] !== undefined)
54
54
  chainOptions["suite"] = options["suite"];
55
+ // Only `refine`'s `publishIssues()` step reads this (a ceiling clamped
56
+ // onto every node it creates — see `core/refine.ts`'s `publish()`); every
57
+ // other chain ignores it, so it's harmless to always pass through, same
58
+ // as `issue` above. `publishIssues()` itself validates the value —
59
+ // rejecting anything but p0|p1|p2|p3 — so this stays a plain passthrough.
60
+ if (options["priority"] !== undefined)
61
+ chainOptions["priority"] = options["priority"];
55
62
  return runChain(chain, ctx, chainOptions);
56
63
  }
@@ -21,6 +21,14 @@ export declare function formatReviewDigest(review: ReviewOutputT): string;
21
21
  * machine needs, with sensible colors/descriptions. Doesn't touch git or
22
22
  * run anything, so it skips watchCommand's repo/chain checks entirely —
23
23
  * you can seed labels before ever wiring up a worktree-capable checkout.
24
+ *
25
+ * On Jira, with `watch.refine.enabled`, this also runs a READ-ONLY check of
26
+ * `watch.jira.issue_types` against the real project — the same check
27
+ * `watchCommand`'s own startup gate runs, exposed here too so a bad mapping
28
+ * can be caught (and fixed) before ever starting the daemon, not just at
29
+ * startup time. Labels themselves stay a pure report on Jira either way
30
+ * (Jira labels are freeform strings with no color/description registry to
31
+ * seed — see `jira_provider.ts`'s module comment).
24
32
  */
25
33
  export declare function watchInitCommand(argv: string[]): Promise<number>;
26
34
  export declare function watchCommand(argv: string[]): Promise<number>;