create-pmos 0.57.0 → 0.58.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/package.json +1 -1
- package/payload/KIT-VERSION +1 -1
- package/payload/planning/evals/eval-calibration.md +1 -1
- package/payload/planning/okrs/health-budgets.md +1 -1
- package/payload/state/discoveries.md +1 -1
- package/workspace/first-paint-audit.js +5 -2
- package/workspace/guide/docs.md +1 -1
- package/workspace/guide/index.md +2 -1
- package/workspace/guide/jira.md +141 -0
- package/workspace/guide/layout.md +2 -2
- package/workspace/lib/jira-cli.js +194 -0
- package/workspace/lib/jira-status.js +239 -0
- package/workspace/lib/jira-tickets.js +668 -0
- package/workspace/lib/pm-guard.js +2 -0
- package/workspace/lib/pmos-verbs.js +3 -0
- package/workspace/lib/report-draft.js +19 -0
- package/workspace/package-lock.json +1 -1
- package/workspace/package.json +1 -1
- package/workspace/public/pm/components.css +206 -0
- package/workspace/public/pm/dest-docs.js +1 -1
- package/workspace/public/pm/frame-copy.js +1 -0
- package/workspace/public/pm/initiative-sheet.js +7 -3
- package/workspace/public/pm/initiatives-copy.js +1 -0
- package/workspace/public/pm/sheet-jira.js +353 -0
- package/workspace/public/pm/topbar.js +5 -4
- package/workspace/server.js +69 -0
- package/workspace/skills/day-check.skill +1 -1
- package/workspace/skills/gather-requirements.skill +1 -1
- package/workspace/skills/jira-push.skill +38 -0
- package/workspace/skills/jira-sync.skill +42 -0
- package/workspace/skills/morning-brief.skill +1 -1
- package/workspace/skills/report-biweekly.skill +1 -1
- package/workspace/skills/tracker-refresh.skill +1 -0
- package/workspace/test-lists/jira-wbs.json +6 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-pmos",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.58.0",
|
|
4
4
|
"description": "Scaffold the PMOS portable kit — run the PMOS product-management workflow (initiative → lane-fit spec → contract → build → three gates, with work-type lanes for features, research, maintenance, incidents, and content) entirely inside your own repo, with all state as git-versioned files. Includes `workflow-sweep`, which audits the state every board derives from and separates real defects from work you simply have not done. `create-pmos workspace` also installs + launches the local Terminal Workspace GUI. No database, no network, no telemetry.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"bin": {
|
package/payload/KIT-VERSION
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
type: Reference
|
|
3
3
|
title: "Eval calibration log"
|
|
4
4
|
description: "Append-only record of every PM override of an evaluator verdict."
|
|
5
|
-
generated: {by: process:create-pmos, at: "2026-10-06T11:
|
|
5
|
+
generated: {by: process:create-pmos, at: "2026-10-06T11:54:59Z"}
|
|
6
6
|
status: stable
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
type: Reference
|
|
3
3
|
title: "Health budgets"
|
|
4
4
|
description: "Closed registry of health-budget anchors (D59), edited only by the PM."
|
|
5
|
-
generated: {by: process:create-pmos, at: "2026-10-06T11:
|
|
5
|
+
generated: {by: process:create-pmos, at: "2026-10-06T11:54:59Z"}
|
|
6
6
|
status: stable
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
type: Reference
|
|
3
3
|
title: "Discoveries and friction log"
|
|
4
4
|
description: "Append-only record of what runs discovered and where the process rubbed."
|
|
5
|
-
generated: {by: process:create-pmos, at: "2026-10-06T11:
|
|
5
|
+
generated: {by: process:create-pmos, at: "2026-10-06T11:54:59Z"}
|
|
6
6
|
status: stable
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -18,9 +18,12 @@
|
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
20
|
// The budgets start at specimen A's counts. reports: 14 → 15 in Run 6 for Present, the one control the PM asked to keep
|
|
21
|
-
// with the slides (2026-10-06).
|
|
21
|
+
// with the slides (2026-10-06). Every place +1 for Docs in the top bar (the PM, 2026-10-06, docs-three-way-sync): the
|
|
22
|
+
// budget is for the whole screen and the bar is on every screen. The calibration stays specimen A's own count: the
|
|
23
|
+
// specimen did not change.
|
|
22
24
|
const SPECIMEN_A = Object.freeze({ today: 17, initiatives: 24, board: 33, reports: 14 });
|
|
23
|
-
const
|
|
25
|
+
const BAR_DOCS = 1;
|
|
26
|
+
const EXPECT = Object.freeze({ today: 17 + BAR_DOCS, initiatives: 24 + BAR_DOCS, board: 33 + BAR_DOCS, reports: 15 + BAR_DOCS });
|
|
24
27
|
|
|
25
28
|
/* ── shoot.js audit(), copied verbatim (scratchpad/specimens2/shoot.js, 2026-10-05) ─────────────────────────────── */
|
|
26
29
|
// runs in the page
|
package/workspace/guide/docs.md
CHANGED
|
@@ -6,7 +6,7 @@ timestamp: 2026-10-06
|
|
|
6
6
|
|
|
7
7
|
# Docs
|
|
8
8
|
|
|
9
|
-
Open Docs from ⌘K (**Docs**) or press `g d`.
|
|
9
|
+
Open Docs from the top bar, from ⌘K (**Docs**), or press `g d`.
|
|
10
10
|
|
|
11
11
|
Docs lists every document in the repository that has a copy in Google Docs or Confluence. For each copy, it shows whether the repository version (PMOS) or the copy is newer. You can then use one version over the other, one copy at a time.
|
|
12
12
|
|
package/workspace/guide/index.md
CHANGED
|
@@ -12,7 +12,7 @@ This guide describes the PMOS workspace as it works today. It ships with the app
|
|
|
12
12
|
|
|
13
13
|
On a desktop the workspace is one frame. [Layout](layout.md) explains each part.
|
|
14
14
|
|
|
15
|
-
1. **The top bar.** Repository ▾, the
|
|
15
|
+
1. **The top bar.** Repository ▾, the five places (Docs included), search (⌘K), Sync when the repository is not in sync, and Settings.
|
|
16
16
|
2. **The place.** One of Today, Initiatives, Board or Reports, or Docs (⌘K or `g d`). It fills the screen under the bar.
|
|
17
17
|
3. **The detail.** The item you opened: an initiative with its status, next steps and artifacts, or the profiles. It opens beside the place and closes with ×.
|
|
18
18
|
4. **The agent dock.** Terminals, documents, conversations and web pages. It is open on Today and a thin rail elsewhere.
|
|
@@ -36,6 +36,7 @@ On a desktop the workspace is one frame. [Layout](layout.md) explains each part.
|
|
|
36
36
|
## Where to read next
|
|
37
37
|
|
|
38
38
|
- [Start the workspace without a terminal](launcher.md): the PMOS app, the login item and `pmos launcher`.
|
|
39
|
+
- [Jira tickets](jira.md): Jira tickets drafted from the PRD, pushed when you select them, and their status.
|
|
39
40
|
- [Docs](docs.md): whether each linked document is the same as, ahead of or behind its Google Docs and Confluence copy.
|
|
40
41
|
- [Layout](layout.md): the frame, the keys and the rule for where a list or a detail opens.
|
|
41
42
|
- [Statuses](statuses.md): what each status means, where it comes from, and what you can change.
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: Guide
|
|
3
|
+
title: "Jira: tickets drafted from the PRD, and their status"
|
|
4
|
+
description: "How to draft Jira tickets from a PRD in your ticket template, push the ones that are ready, and see their status, sprint and progress on the initiative's Jira tickets tab."
|
|
5
|
+
timestamp: 2026-10-06
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Jira: tickets drafted from the PRD, and their status
|
|
9
|
+
|
|
10
|
+
Your team works in Jira. You plan in PMOS. This page shows how the two stay in step without a second Jira:
|
|
11
|
+
|
|
12
|
+
1. PMOS drafts one Jira ticket for each ticket item of the PRD, in your ticket template.
|
|
13
|
+
2. You read the tickets, edit them if needed, and select the ones that are ready.
|
|
14
|
+
3. An agent creates the selected tickets in Jira, with your own Atlassian connection.
|
|
15
|
+
4. Each day, an agent reads their status back. The initiative's **Jira tickets** tab shows it.
|
|
16
|
+
|
|
17
|
+
PMOS owns the text of a ticket. Jira owns its status. PMOS holds no Jira password or token: your agent session reads and writes Jira with the connection you set up in it.
|
|
18
|
+
|
|
19
|
+
## Where it is
|
|
20
|
+
|
|
21
|
+
Open an initiative. Its sheet has four tabs: Breakdown, Files, History and **Jira tickets**.
|
|
22
|
+
|
|
23
|
+
In a document repository whose lane has a Handoff stage, the Jira tickets tab opens when the handoff is done. Mark the Handoff item done in the Breakdown, or record `handed_off:` in the initiative. In other repositories the tab is open as soon as the PRD has ticket items.
|
|
24
|
+
|
|
25
|
+
## Write the ticket items in the PRD
|
|
26
|
+
|
|
27
|
+
PMOS reads a section headed **User stories**, **Stories**, **Tickets**, **Jira tickets** or **Work items**. Each ticket is a subheading with an ID:
|
|
28
|
+
|
|
29
|
+
```markdown
|
|
30
|
+
## Tickets
|
|
31
|
+
|
|
32
|
+
### TD-01 · Carry the real runtime version on every GA4 event
|
|
33
|
+
Type: Task · Priority: High · Estimate: S · Depends on: —
|
|
34
|
+
|
|
35
|
+
As a PM, I want every GA4 event to carry the runtime version, so that I can compare releases.
|
|
36
|
+
|
|
37
|
+
#### Additional Context
|
|
38
|
+
#### User Flow
|
|
39
|
+
#### Functional Requirements
|
|
40
|
+
1. …
|
|
41
|
+
#### Terminal States
|
|
42
|
+
#### Acceptance Criteria
|
|
43
|
+
- Given …, when …, then …
|
|
44
|
+
#### Refs
|
|
45
|
+
- [Tagging plan](https://example.com/plan)
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
- Keep the ID (`TD-01`) stable. PMOS matches a ticket to its item by ID.
|
|
49
|
+
- The `Type · Priority · Estimate · Depends on` line is optional. The defaults are Task, Medium, no estimate and no dependency.
|
|
50
|
+
- Text before the first subsection is the User Story when it starts "As a …".
|
|
51
|
+
- A bullet such as `- **S1 · Pay with a saved card.** As a …` also works.
|
|
52
|
+
|
|
53
|
+
## Draft, read and edit
|
|
54
|
+
|
|
55
|
+
Press **Draft tickets**, or run:
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
pmos jira draft <initiative>
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
The draft is a file, `<state>/tracker/jira/<initiative>-tickets.md`. Every row of the tab opens a ticket: its fields, its full text and its status in Jira. **Edit text** changes the summary and the sections. Type, Priority, Estimate and Depends on are lists.
|
|
62
|
+
|
|
63
|
+
A ticket needs a User Story, Functional Requirements and Acceptance Criteria before it can be pushed. A ticket without them shows **Missing text**.
|
|
64
|
+
|
|
65
|
+
To see every ticket in the terminal:
|
|
66
|
+
|
|
67
|
+
```
|
|
68
|
+
pmos jira show <initiative>
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Choose the epic
|
|
72
|
+
|
|
73
|
+
Above the list, choose **Create new** (an epic named after the initiative, made at the first push) or **Use an existing epic** and paste its link or key. After the first push the epic is fixed. In the terminal:
|
|
74
|
+
|
|
75
|
+
```
|
|
76
|
+
pmos jira epic <initiative> new
|
|
77
|
+
pmos jira epic <initiative> ATL-482
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Push the tickets that are ready
|
|
81
|
+
|
|
82
|
+
Check the tickets that are ready and press **Push N to Jira**. Only checked tickets are sent. An agent session starts and creates them under the epic. A ticket you edit after you checked it must be checked again.
|
|
83
|
+
|
|
84
|
+
In your own terminal, the same selection is:
|
|
85
|
+
|
|
86
|
+
```
|
|
87
|
+
pmos jira approve <initiative> TD-01 TD-02
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
It asks you to type the initiative's id. An agent session cannot select tickets.
|
|
91
|
+
|
|
92
|
+
The agent runs the **Push to Jira** skill. It reads what to send with `pmos jira pending <initiative>`, and records each key with `pmos jira record <initiative> TD-01=ATL-491`. It never moves, assigns, comments on, closes or deletes a Jira issue.
|
|
93
|
+
|
|
94
|
+
## Status, sprint and progress
|
|
95
|
+
|
|
96
|
+
Each row shows the ticket's status in Jira, and whether it is in the **Active sprint**, the **Next sprint** or the **Backlog**, with its subtasks done. A blocked ticket shows **Blocked** and the reason.
|
|
97
|
+
|
|
98
|
+
The **Jira status check** reads this for every initiative. **Check now** on the tab reads it for one. To run it every day before the morning brief (06:45), schedule it at 06:30:
|
|
99
|
+
|
|
100
|
+
```
|
|
101
|
+
pmos schedule add --skill jira-sync --cadence daily --time 06:30
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
The check reads Jira only. It writes `<state>/tracker/jira/<initiative>.md` with:
|
|
105
|
+
|
|
106
|
+
```
|
|
107
|
+
pmos jira query [<initiative>]
|
|
108
|
+
pmos jira sync <initiative> --from <file|-> [--query "<jql>"] [--dry-run]
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## When the PRD changes
|
|
112
|
+
|
|
113
|
+
Each time the tab opens, PMOS compares every ticket with its item in the PRD:
|
|
114
|
+
|
|
115
|
+
- **PRD changed**: the item changed since the ticket was drafted. Open the ticket to see both versions, then press **Use the PRD version** or **Keep the ticket as it is**. A ticket that is in Jira then shows **Edited, not in Jira yet** until you push it again.
|
|
116
|
+
- **New in PRD**: an item has no ticket yet. Press **Add to tickets**.
|
|
117
|
+
- **Removed from PRD**: the item is gone. The Jira ticket is not changed.
|
|
118
|
+
|
|
119
|
+
A ticket you did not edit and did not push is redrafted at the next **Draft tickets**.
|
|
120
|
+
|
|
121
|
+
## Which initiative is this Jira key?
|
|
122
|
+
|
|
123
|
+
```
|
|
124
|
+
pmos jira which ATL-491
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
The morning brief, the day check and requirement gathering use it to tie a Jira issue to its initiative.
|
|
128
|
+
|
|
129
|
+
## Every command
|
|
130
|
+
|
|
131
|
+
- `pmos jira draft <initiative> [--dry-run]`
|
|
132
|
+
- `pmos jira show <initiative>`
|
|
133
|
+
- `pmos jira epic <initiative> new|<KEY or link>`
|
|
134
|
+
- `pmos jira approve <initiative> <ID> …`
|
|
135
|
+
- `pmos jira pending <initiative>`
|
|
136
|
+
- `pmos jira record <initiative> [ID=KEY …] [--epic KEY] [--updated ID,ID]`
|
|
137
|
+
- `pmos jira query [<initiative>]`
|
|
138
|
+
- `pmos jira sync <initiative> --from <file|-> [--query "<jql>"] [--dry-run]`
|
|
139
|
+
- `pmos jira which <KEY>`
|
|
140
|
+
|
|
141
|
+
Every command takes `--json` and `--root <dir>`.
|
|
@@ -15,7 +15,7 @@ The top bar has these parts, from left to right:
|
|
|
15
15
|
|
|
16
16
|
- **Repository ▾** names the current repository. Press it to choose another repository, or **All repositories** to see the work of every repository at once. All repositories replaces the old Portfolio mode.
|
|
17
17
|
- **Branches and sessions** is in ⌘K. It opens a sheet with every branch of the repository (main first) and how many sessions run in each. Press a branch to see its sessions, open one, end one, or start a session there. **+ branch** makes a branch, and **+ session** starts one with a branch, an agent and a profile. With All repositories, the sheet lists every repository.
|
|
18
|
-
- **Today, Initiatives, Board and
|
|
18
|
+
- **Today, Initiatives, Board, Reports and Docs** are the five places. Press one to show it. The Today count shows how many items need you. Docs shows whether each document is the same as its Google Docs and Confluence copy.
|
|
19
19
|
- **Search or capture ⌘K** opens the command palette. Type to find an initiative, a repository, an open tab or a command.
|
|
20
20
|
- **Sync** shows only when the current repository is not in sync with its remote. Press it to fetch, bring in the work of the other machine, record your changes and push. If the repository has not fetched in the last 10 minutes, Sync also shows, because a push from another machine is not visible until a fetch. When the repository fetched recently and is in sync, Sync is not shown. If Sync stops, the message names the step that stopped.
|
|
21
21
|
- **Sync on its own.** If you forget to press Sync, the workspace syncs for you. It syncs each repository once when the workspace starts. It also syncs a repository that has records not yet pushed when no record changed for 5 minutes. In a document repository it syncs every changed file on any branch. In a code repository it syncs only on the default branch (for example `main`), and only when every commit not yet pushed changes records and no code. If you close the tab while records are not yet pushed, the browser asks you first. To turn automatic sync off, start the workspace with `PMOS_AUTO_SYNC=0`.
|
|
@@ -70,7 +70,7 @@ The workspace is neutral. A primary button is the text colour on the page colour
|
|
|
70
70
|
|
|
71
71
|
| Key | What it does |
|
|
72
72
|
|---|---|
|
|
73
|
-
| g t, g i, g b, g r | Show Today, Initiatives, Board or
|
|
73
|
+
| g t, g i, g b, g r, g d | Show Today, Initiatives, Board, Reports or Docs. |
|
|
74
74
|
| ⌘K (Ctrl-K) | Open or close the command palette. |
|
|
75
75
|
| ⌘J (Ctrl-J) | Open or fold the dock. |
|
|
76
76
|
| ⌘⇧J (Ctrl-Shift-J) | Focus: give the dock the whole window, or show the place again. |
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/* jira-cli.js — `pmos jira …`: Jira tickets drafted from the PRD, pushed by the PM's selection, and their status
|
|
3
|
+
* (initiative jira-wbs).
|
|
4
|
+
*
|
|
5
|
+
* pmos jira draft <initiative> [--dry-run] the PRD's items → the ticket draft (lib/jira-tickets.js)
|
|
6
|
+
* pmos jira show <initiative> every ticket, its flags and its status in Jira
|
|
7
|
+
* pmos jira epic <initiative> new|<KEY or link> create a new epic, or use an existing one
|
|
8
|
+
* pmos jira approve <initiative> <ID> … the PM selects tickets to push, in their own terminal
|
|
9
|
+
* pmos jira pending <initiative> what the agent creates or updates in Jira (approved only)
|
|
10
|
+
* pmos jira record <initiative> [ID=KEY …] [--epic KEY] [--updated ID,ID] the keys the agent created
|
|
11
|
+
* pmos jira query [<initiative>] the Jira search for the tickets an initiative pushed
|
|
12
|
+
* pmos jira sync <initiative> --from <file|-> [--query "<jql>"] [--dry-run] a saved search → the status snapshot
|
|
13
|
+
* pmos jira which <KEY> the initiative a Jira key belongs to
|
|
14
|
+
* every command: [--json] [--root <dir>]
|
|
15
|
+
*
|
|
16
|
+
* This command never talks to Jira. An agent session does, with the PM's own Atlassian connection (D53, D88), and
|
|
17
|
+
* hands the result to `sync` or `record`. `approve` is PM-only (pm-guard act `push`): an agent session is refused.
|
|
18
|
+
*/
|
|
19
|
+
const fs = require('fs');
|
|
20
|
+
const path = require('path');
|
|
21
|
+
const { execFileSync } = require('child_process');
|
|
22
|
+
const { parseArgs } = require('./schedule-cli');
|
|
23
|
+
const jiraStatus = require('./jira-status.js');
|
|
24
|
+
const jiraTickets = require('./jira-tickets.js');
|
|
25
|
+
const planItems = require('./plan-items.js');
|
|
26
|
+
const guard = require('./pm-guard.js');
|
|
27
|
+
|
|
28
|
+
const COMMANDS = { jira: ['draft', 'show', 'epic', 'approve', 'pending', 'record', 'query', 'sync', 'which'] };
|
|
29
|
+
const USAGE = [
|
|
30
|
+
'usage: pmos jira draft|show|epic|approve|pending|record|query|sync|which — Jira tickets from the PRD, and their status',
|
|
31
|
+
' pmos jira draft <initiative> [--dry-run] draft one ticket per PRD item, in the ticket template',
|
|
32
|
+
' pmos jira show <initiative> every ticket, what needs you, and its status in Jira',
|
|
33
|
+
' pmos jira epic <initiative> new|<KEY or link> create a new epic at the first push, or use an existing one',
|
|
34
|
+
' pmos jira approve <initiative> <ID> … select tickets to push (you, in your own terminal)',
|
|
35
|
+
' pmos jira pending <initiative> what the push creates or updates in Jira',
|
|
36
|
+
' pmos jira record <initiative> [ID=KEY …] [--epic KEY] [--updated ID,ID] the keys created in Jira',
|
|
37
|
+
' pmos jira query [<initiative>] the Jira search for the pushed tickets',
|
|
38
|
+
' pmos jira sync <initiative> --from <file|-> [--query "<jql>"] [--dry-run] save the status read from Jira',
|
|
39
|
+
' pmos jira which <KEY> the initiative a Jira key belongs to',
|
|
40
|
+
' every command: [--json] [--root <dir>]',
|
|
41
|
+
'This command never calls Jira. Your agent reads and writes Jira with your own connection.',
|
|
42
|
+
];
|
|
43
|
+
const BOOL = new Set(['json', 'dry-run', 'help']);
|
|
44
|
+
const VALUE = new Set(['from', 'query', 'root', 'epic', 'updated']);
|
|
45
|
+
const FLAG_TEXT = { 'prd-changed': 'PRD changed', removed: 'Removed from PRD', blocked: 'Blocked', incomplete: 'Missing text', pushing: 'Selected, waiting for the push', edited: 'Edited, not in Jira yet' };
|
|
46
|
+
|
|
47
|
+
function parse(argv) {
|
|
48
|
+
const rest = []; const bools = {};
|
|
49
|
+
for (const a of argv) {
|
|
50
|
+
if (a.startsWith('--') && BOOL.has(a.slice(2))) bools[a.slice(2)] = true;
|
|
51
|
+
else if (a === '-h') bools.help = true;
|
|
52
|
+
else rest.push(a);
|
|
53
|
+
}
|
|
54
|
+
const { pos, flags } = parseArgs(rest);
|
|
55
|
+
return { pos, flags: { ...flags, ...bools } };
|
|
56
|
+
}
|
|
57
|
+
function repoRoot(cwd) {
|
|
58
|
+
try { return execFileSync('git', ['rev-parse', '--show-toplevel'], { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim() || cwd; }
|
|
59
|
+
catch { return cwd; }
|
|
60
|
+
}
|
|
61
|
+
function readInput(from, cwd, stdin) {
|
|
62
|
+
if (from === '-') return stdin != null ? stdin : fs.readFileSync(0, 'utf8');
|
|
63
|
+
return fs.readFileSync(path.resolve(cwd, from), 'utf8');
|
|
64
|
+
}
|
|
65
|
+
function statusLine(t) {
|
|
66
|
+
if (!t.inJira) return 'not in Jira';
|
|
67
|
+
if (!t.jira) return `${t.key} · not checked yet`;
|
|
68
|
+
const j = t.jira;
|
|
69
|
+
const where = j.category === 'done' ? 'Done' : jiraStatus.sprintLabel(j.sprint);
|
|
70
|
+
return `${t.key} · ${j.status} · ${where}${j.subtasks && j.subtasks.total ? ` · subtasks ${j.subtasks.done} of ${j.subtasks.total}` : ''}${j.stale ? ' · not in the last read' : ''}`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function run(argv, { say = (s) => console.log(s), cwd = process.cwd(), isTTY, env, now, stdin, ask } = {}) {
|
|
74
|
+
const args = argv[0] === 'jira' ? argv.slice(1) : argv.slice();
|
|
75
|
+
const verb = args[0];
|
|
76
|
+
const { pos, flags } = parse(args.slice(1));
|
|
77
|
+
const json = !!flags.json;
|
|
78
|
+
if (!verb || verb === 'help' || verb === '--help' || verb === '-h' || flags.help) {
|
|
79
|
+
for (const l of USAGE) say(l);
|
|
80
|
+
return verb ? 0 : 1;
|
|
81
|
+
}
|
|
82
|
+
if (!COMMANDS.jira.includes(verb)) { say(`unknown jira command "${verb}" — one of ${COMMANDS.jira.join(', ')}`); return 2; }
|
|
83
|
+
for (const k of Object.keys(flags)) {
|
|
84
|
+
if (!BOOL.has(k) && !VALUE.has(k)) { say(`unknown option --${k}`); return 2; }
|
|
85
|
+
if (VALUE.has(k) && flags[k] === '') { say(`--${k} needs a value`); return 2; }
|
|
86
|
+
}
|
|
87
|
+
const root = flags.root ? path.resolve(cwd, flags.root) : repoRoot(cwd);
|
|
88
|
+
const fail = (error, code = 1) => { if (json) say(JSON.stringify({ ok: false, error })); else say(`✗ ${error}`); return code; };
|
|
89
|
+
const out = (r, lines) => { if (json) say(JSON.stringify(r)); else for (const l of lines) say(l); return 0; };
|
|
90
|
+
const need = (what) => { say(`usage: pmos jira ${verb} ${what}`); return 2; };
|
|
91
|
+
const initiative = pos[0];
|
|
92
|
+
const known = () => planItems.INIT_RE.test(initiative || '') && !!jiraTickets.recordFm(root, initiative);
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
if (verb === 'query') {
|
|
96
|
+
if (initiative) {
|
|
97
|
+
const q = jiraStatus.queryFor(root, initiative);
|
|
98
|
+
if (!q.ok) return fail(q.error);
|
|
99
|
+
if (!q.linked) return fail(`${initiative} has no ticket in Jira yet — push its tickets first`);
|
|
100
|
+
return out(q, [q.query]);
|
|
101
|
+
}
|
|
102
|
+
const all = jiraStatus.linkedInitiatives(root).filter((x) => x.linked);
|
|
103
|
+
return out({ ok: true, initiatives: all }, all.length ? all.map((x) => `${x.initiative}\t${x.query}`) : ['no initiative has a ticket in Jira yet']);
|
|
104
|
+
}
|
|
105
|
+
if (verb === 'which') {
|
|
106
|
+
if (!initiative) return need('<KEY>');
|
|
107
|
+
const r = jiraStatus.which(root, initiative);
|
|
108
|
+
if (!r.ok) return fail(r.error);
|
|
109
|
+
return out(r, r.initiatives.length ? r.initiatives.map((h) => `${r.key} → ${h.initiative} · ${h.role}${h.ticket ? ` ${h.ticket} · ${h.summary}` : ''}`) : [`${r.key} belongs to no initiative in this repository`]);
|
|
110
|
+
}
|
|
111
|
+
if (!initiative) return need('<initiative> …');
|
|
112
|
+
if (!known()) return fail(`no initiative ${initiative} in this repository`);
|
|
113
|
+
|
|
114
|
+
if (verb === 'draft') {
|
|
115
|
+
const r = jiraTickets.draft(root, initiative, { dryRun: !!flags['dry-run'], now });
|
|
116
|
+
if (!r.ok) return fail(r.error);
|
|
117
|
+
const lines = [`${initiative} · ${r.tickets} ${r.tickets === 1 ? 'ticket' : 'tickets'} in ${r.draft}${r.changed ? '' : ' · no change'}${flags['dry-run'] ? ' · dry run, nothing written' : ''}`];
|
|
118
|
+
for (const c of r.changes) lines.push(` ${c.what.padEnd(9)} ${c.ticket} · ${c.summary}${c.why ? ` (${c.why})` : ''}`);
|
|
119
|
+
lines.push('Read them on the Jira tickets tab, or with pmos jira show. Select the ready ones there, or with pmos jira approve.');
|
|
120
|
+
return out(r, lines);
|
|
121
|
+
}
|
|
122
|
+
if (verb === 'show') {
|
|
123
|
+
const r = jiraTickets.view(root, initiative, { snapshot: jiraStatus.readSnapshot(root, initiative) });
|
|
124
|
+
if (!r.ok) return fail(r.error);
|
|
125
|
+
const lines = [`${initiative} · ${r.inJira} of ${r.tickets.length} in Jira · ${r.checkedAt ? `checked ${r.checkedAt}` : 'not checked yet'} · epic ${r.epic.key || (r.epic.mode === 'new' ? 'new, created at the first push' : 'not chosen')}`];
|
|
126
|
+
if (!r.gate.open) lines.push(` The Jira tickets tab opens when the handoff is done (${r.gate.why}).`);
|
|
127
|
+
for (const t of r.tickets) {
|
|
128
|
+
lines.push(` ${t.id} · ${t.summary} · ${[t.type, t.priority, t.estimate].filter(Boolean).join(' · ')}`);
|
|
129
|
+
lines.push(` ${statusLine(t)}${t.flag ? ` · ${FLAG_TEXT[t.flag]}${t.flag === 'incomplete' ? `: ${t.missing.join(', ')}` : t.flag === 'blocked' && t.jira.reason ? `: ${t.jira.reason}` : ''}` : ''}`);
|
|
130
|
+
}
|
|
131
|
+
for (const n of r.newInPrd) lines.push(` ${n.id} · ${n.summary} · New in PRD: run pmos jira draft ${initiative}`);
|
|
132
|
+
if (!r.drafted) lines.push(` No draft yet: pmos jira draft ${initiative}`);
|
|
133
|
+
return out(r, lines);
|
|
134
|
+
}
|
|
135
|
+
if (verb === 'epic') {
|
|
136
|
+
const v = pos[1];
|
|
137
|
+
if (!v) return need('<initiative> new|<KEY or link>');
|
|
138
|
+
const r = jiraTickets.setEpic(root, initiative, v === 'new' ? { mode: 'new' } : { mode: 'existing', key: v }, { now });
|
|
139
|
+
if (!r.ok) return fail(r.error);
|
|
140
|
+
return out(r, [r.epic.mode === 'new' ? `${initiative} · a new epic is created at the first push` : `${initiative} · tickets go under ${r.epic.key}`]);
|
|
141
|
+
}
|
|
142
|
+
if (verb === 'approve') {
|
|
143
|
+
const ids = pos.slice(1);
|
|
144
|
+
if (!ids.length) return need('<initiative> <ID> …');
|
|
145
|
+
const g = await guard.requirePmCli({ act: 'push', expect: initiative,
|
|
146
|
+
...(isTTY === undefined ? {} : { isTTY }), ...(env === undefined ? {} : { env }), ask: ask || guard.askAtTerminal });
|
|
147
|
+
if (!g.ok) return fail(g.error);
|
|
148
|
+
const r = jiraTickets.approve(root, initiative, ids, { now });
|
|
149
|
+
if (!r.ok) return fail(r.error);
|
|
150
|
+
return out(r, [`selected ${r.approved.join(', ')} for ${initiative}. Your agent can push them now: run the Push to Jira skill, or press Push on the tab.`]);
|
|
151
|
+
}
|
|
152
|
+
if (verb === 'pending') {
|
|
153
|
+
const r = jiraTickets.pushPlan(root, initiative);
|
|
154
|
+
if (!r.ok) return fail(r.error);
|
|
155
|
+
const lines = [`${initiative} · ${r.create.length} to create · ${r.update.length} to update · ${r.unchanged} unchanged · ${r.notApproved} not selected`];
|
|
156
|
+
lines.push(r.epic.key ? ` epic ${r.epic.key}` : ` epic create "${r.epic.create.summary}"`);
|
|
157
|
+
for (const t of r.create) lines.push(` create ${t.ticket} · ${t.summary} · ${t.issuetype} · ${t.priority}`);
|
|
158
|
+
for (const t of r.update) lines.push(` update ${t.ticket} ${t.key} · ${t.summary}`);
|
|
159
|
+
return out(r, lines);
|
|
160
|
+
}
|
|
161
|
+
if (verb === 'record') {
|
|
162
|
+
const tickets = {};
|
|
163
|
+
for (const p of pos.slice(1)) {
|
|
164
|
+
const m = /^([A-Za-z0-9][\w.-]{0,40})=(.+)$/.exec(p);
|
|
165
|
+
if (!m) return fail(`"${p}" is not <ID>=<KEY>`, 2);
|
|
166
|
+
tickets[m[1]] = m[2];
|
|
167
|
+
}
|
|
168
|
+
const updated = flags.updated ? String(flags.updated).split(',').map((s) => s.trim()).filter(Boolean) : [];
|
|
169
|
+
if (!Object.keys(tickets).length && !updated.length && !flags.epic) return need('<initiative> [ID=KEY …] [--epic KEY] [--updated ID,ID]');
|
|
170
|
+
const r = jiraTickets.record(root, initiative, { epic: flags.epic || null, tickets, pushed: updated }, { now });
|
|
171
|
+
if (!r.ok) return fail(r.error);
|
|
172
|
+
return out(r, [`${initiative}${r.epic ? ` · epic ${r.epic}` : ''} · recorded ${r.recorded.length}`, ...r.recorded.map((x) => ` ${x.what} ${x.ticket} ${x.key}`)]);
|
|
173
|
+
}
|
|
174
|
+
if (verb === 'sync') {
|
|
175
|
+
if (!flags.from) return need('<initiative> --from <file|->');
|
|
176
|
+
let raw;
|
|
177
|
+
try { raw = JSON.parse(readInput(flags.from, cwd, stdin)); }
|
|
178
|
+
catch (e) { return fail(`could not read ${flags.from} as JSON — ${String((e && e.message) || e).slice(0, 160)}`); }
|
|
179
|
+
const r = jiraStatus.sync(root, initiative, raw, { query: flags.query || undefined, dryRun: !!flags['dry-run'], now });
|
|
180
|
+
if (!r.ok) return fail(r.error);
|
|
181
|
+
const lines = [`${initiative} · read ${r.read} ${r.read === 1 ? 'ticket' : 'tickets'}${r.ignored ? ` · ${r.ignored} other issues ignored` : ''}${r.skipped ? ` · ${r.skipped} skipped (no key or summary)` : ''}${r.dryRun ? ' · dry run, nothing written' : ''}`];
|
|
182
|
+
for (const c of r.changes) lines.push(` ${c.key} · ${c.what}${c.from !== undefined ? `: ${c.from} → ${c.to}` : c.status ? `: ${c.status}` : ''}${c.reason ? ` (${c.reason})` : ''}`);
|
|
183
|
+
if (!r.changes.length) lines.push(' no change');
|
|
184
|
+
for (const k of r.missing) lines.push(` ${k} · not in this read; its last status is kept`);
|
|
185
|
+
if (!r.dryRun) lines.push(`snapshot ${r.snapshot}`);
|
|
186
|
+
return out(r, lines);
|
|
187
|
+
}
|
|
188
|
+
} catch (e) {
|
|
189
|
+
return fail(String((e && e.message) || e).slice(0, 200));
|
|
190
|
+
}
|
|
191
|
+
return 2;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
module.exports = { run, COMMANDS, USAGE };
|