create-pmos 0.519.0 → 0.521.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 +27 -31
- package/payload/AGENTS.md +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/changelog.json +26 -0
- package/workspace/guide/agent-capabilities.md +2 -1
- package/workspace/guide/descriptions.md +43 -0
- package/workspace/guide/index.md +1 -0
- package/workspace/guide/initiatives-roadmap.md +1 -0
- package/workspace/guide/next-prompt.md +4 -4
- package/workspace/lib/board-trail.js +8 -3
- package/workspace/lib/description-props.js +214 -0
- package/workspace/lib/initiative-cli.js +28 -2
- package/workspace/lib/initiative-writer.js +41 -1
- package/workspace/lib/proposals.js +13 -4
- package/workspace/lib/routes-next-prompt.js +12 -4
- package/workspace/lib/routes-proposals.js +5 -0
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +1 -1
- package/workspace/public/pm/cozy-home.css +2 -5
- package/workspace/public/pm/ini-about.js +26 -11
- package/workspace/public/pm/ini-props.js +5 -3
- package/workspace/public/pm/ini-view-model.js +1 -1
- package/workspace/public/pm/ini-view.css +1 -0
- package/workspace/public/pm/ini-view.js +1 -1
- package/workspace/public/pm/next-prompt-ui.js +6 -3
- package/workspace/server.js +14 -0
- package/workspace/test-lists/initiative-description-field.json +5 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-pmos",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.521.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": {
|
|
@@ -32,6 +32,32 @@
|
|
|
32
32
|
"audit"
|
|
33
33
|
],
|
|
34
34
|
"pmosChanges": [
|
|
35
|
+
{
|
|
36
|
+
"v": "0.521.0",
|
|
37
|
+
"at": "2026-11-04T02:17:43.880Z",
|
|
38
|
+
"f": 4,
|
|
39
|
+
"prs": [
|
|
40
|
+
{
|
|
41
|
+
"n": 1217,
|
|
42
|
+
"t": "Sharper prompt keeps to the composer column and starts lean",
|
|
43
|
+
"s": "Sharper prompt keeps to the composer column and starts lean",
|
|
44
|
+
"k": "fix"
|
|
45
|
+
}
|
|
46
|
+
]
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
"v": "0.520.0",
|
|
50
|
+
"at": "2026-11-04T01:52:53.932Z",
|
|
51
|
+
"f": 4,
|
|
52
|
+
"prs": [
|
|
53
|
+
{
|
|
54
|
+
"n": 1216,
|
|
55
|
+
"t": "Every initiative has its own description",
|
|
56
|
+
"s": "Every initiative has its own description",
|
|
57
|
+
"k": "new"
|
|
58
|
+
}
|
|
59
|
+
]
|
|
60
|
+
},
|
|
35
61
|
{
|
|
36
62
|
"v": "0.519.0",
|
|
37
63
|
"at": "2026-11-04T01:26:27.710Z",
|
|
@@ -368,36 +394,6 @@
|
|
|
368
394
|
"d": "The Timeline still cut long task names short. Now they wrap in full there too."
|
|
369
395
|
}
|
|
370
396
|
]
|
|
371
|
-
},
|
|
372
|
-
{
|
|
373
|
-
"v": "0.501.0",
|
|
374
|
-
"at": "2026-11-03T04:49:22.234Z",
|
|
375
|
-
"f": 4,
|
|
376
|
-
"prs": [
|
|
377
|
-
{
|
|
378
|
-
"n": 1184,
|
|
379
|
-
"t": "Home's list toggle always opens and closes the list",
|
|
380
|
-
"s": "Home's list toggle always opens and closes the list",
|
|
381
|
-
"k": "fix",
|
|
382
|
-
"h": "Home's list opens and closes",
|
|
383
|
-
"d": "On a wide Viewer, Home's list could fold away and refuse to open again. Now the toggle opens and closes it every time."
|
|
384
|
-
}
|
|
385
|
-
]
|
|
386
|
-
},
|
|
387
|
-
{
|
|
388
|
-
"v": "0.500.0",
|
|
389
|
-
"at": "2026-11-03T04:25:59.021Z",
|
|
390
|
-
"f": 4,
|
|
391
|
-
"prs": [
|
|
392
|
-
{
|
|
393
|
-
"n": 1183,
|
|
394
|
-
"t": "Delete a captured meeting-note point from its edit card",
|
|
395
|
-
"s": "Delete a captured meeting-note point from its edit card",
|
|
396
|
-
"k": "new",
|
|
397
|
-
"h": "Delete a meeting point",
|
|
398
|
-
"d": "A point captured from a meeting could not be removed. Now you can delete it from its edit card."
|
|
399
|
-
}
|
|
400
|
-
]
|
|
401
397
|
}
|
|
402
398
|
]
|
|
403
399
|
}
|
package/payload/AGENTS.md
CHANGED
|
@@ -33,7 +33,7 @@ It refuses frontmatter, fenced and indented code, table rows, headings, delibera
|
|
|
33
33
|
|
|
34
34
|
| Hosted tool (as skills name them) | File-mode equivalent |
|
|
35
35
|
|---|---|
|
|
36
|
-
| `create_initiative` | write `pmos/state/initiatives/<id>.md` (frontmatter: id, title, parent_kr, stage, status, **type** — the D56 lane, default `feature` — plus `intent` for the chore lane, prd_path, rubric_path, created; plus `outcome_metric` + `outcome_horizon` — the D57 declaration for post-ship success criteria. No initiative goes without one: after you create it, and once its PRD exists, run `pmos outcomes propose <id>` and show the cited draft; write it with `pmos outcomes declare` only after the PM's yes) |
|
|
36
|
+
| `create_initiative` | write `pmos/state/initiatives/<id>.md` (frontmatter: id, title, parent_kr, stage, status, **type** — the D56 lane, default `feature` — plus `intent` for the chore lane, prd_path, rubric_path, created; plus `outcome_metric` + `outcome_horizon` — the D57 declaration for post-ship success criteria. No initiative goes without one: after you create it, and once its PRD exists, run `pmos outcomes propose <id>` and show the cited draft; write it with `pmos outcomes declare` only after the PM's yes; plus `description` — one paragraph that says what the initiative is, shown in About before any PRD exists: `pmos initiative describe <id>` lists the cited lines to draft it from, and `pmos initiative describe <id> "<text>"` writes it only after the PM's yes) |
|
|
37
37
|
| `save_spec` | the PRD file itself at `pmos/planning/prd/<id>-prd.md` (set `prd_path` on the initiative file) |
|
|
38
38
|
| `get_spec` / `get_planning_artifact` | read `pmos/planning/…` |
|
|
39
39
|
| `get_skill` | read `pmos/skills/<name>.skill` |
|
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-11-
|
|
5
|
+
generated: {by: process:create-pmos, at: "2026-11-04T02:17:40Z"}
|
|
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-11-
|
|
5
|
+
generated: {by: process:create-pmos, at: "2026-11-04T02:17:40Z"}
|
|
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-11-
|
|
5
|
+
generated: {by: process:create-pmos, at: "2026-11-04T02:17:40Z"}
|
|
6
6
|
status: stable
|
|
7
7
|
---
|
|
8
8
|
|
package/workspace/changelog.json
CHANGED
|
@@ -1,4 +1,30 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"v": "0.521.0",
|
|
4
|
+
"at": "2026-11-04T02:17:43.880Z",
|
|
5
|
+
"f": 4,
|
|
6
|
+
"prs": [
|
|
7
|
+
{
|
|
8
|
+
"n": 1217,
|
|
9
|
+
"t": "Sharper prompt keeps to the composer column and starts lean",
|
|
10
|
+
"s": "Sharper prompt keeps to the composer column and starts lean",
|
|
11
|
+
"k": "fix"
|
|
12
|
+
}
|
|
13
|
+
]
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"v": "0.520.0",
|
|
17
|
+
"at": "2026-11-04T01:52:53.932Z",
|
|
18
|
+
"f": 4,
|
|
19
|
+
"prs": [
|
|
20
|
+
{
|
|
21
|
+
"n": 1216,
|
|
22
|
+
"t": "Every initiative has its own description",
|
|
23
|
+
"s": "Every initiative has its own description",
|
|
24
|
+
"k": "new"
|
|
25
|
+
}
|
|
26
|
+
]
|
|
27
|
+
},
|
|
2
28
|
{
|
|
3
29
|
"v": "0.519.0",
|
|
4
30
|
"at": "2026-11-04T01:26:27.710Z",
|
|
@@ -31,6 +31,7 @@ This page lists every job, so you know what runs and why. Each row links to the
|
|
|
31
31
|
| [New dates, done and tidy names](claude-proposes.md) | Each morning, and when a date passes | The tasks of the initiative, and the evidence that work happened | The initiative's ✦ | `PMOS_PROPOSE=off` |
|
|
32
32
|
| [Grill](claude-proposes.md) | Before an initiative leaves a stage, when a forecast turns at risk, when scope or dates move a lot, and once a month on running work with late work. Also when you write "grill this" in the initiative's chat. | The initiative, your stored research, your meeting notes and the decisions that stand | "Claude grilled it" under the initiative's ✦ | Settings › Claude's checks: Off. `PMOS_GRILL=off` |
|
|
33
33
|
| [You decided this before](ask-the-past.md) | You add an initiative that repeats an earlier decision | `DECISIONS.md`, the decision records, the PM journal, `planning/out-of-scope/` and the initiative records | The initiative's ✦ | `pmos ai off past-repeat` |
|
|
34
|
+
| [Initiative descriptions](descriptions.md) | Once a day. At most 3 drafts a day for each product. | The PRD's first Problem or Why paragraph, or the record's own first paragraph | "Description" under the initiative's ✦ | `pmos ai off descriptions`. `PMOS_DESCRIPTIONS=off` |
|
|
34
35
|
| [Expected outcomes](outcomes.md) | Once a day. At most 3 drafts a day for each product. | The PRD's success criteria, or the anchored key result | "Expected outcome" under the initiative's ✦. Today when a check is due. | `pmos ai off outcomes`. `PMOS_OUTCOMES=off` |
|
|
35
36
|
| [Metric drafts](metric-drafts.md) | A product with no North Star, the first 14 days of a quarter, a program with no goal, and an initiative that gets a PRD | The last 8 weeks of real readings | The page of that product, program or initiative | `PMOS_METRIC_DRAFT=off` |
|
|
36
37
|
| [Horizon scan](horizon-scan.md) | Daily by default, at most once a day. Scan now: at most 3 a day. | The public web for your kept watch list, and your repository | Report documents and proposals you approve | Cadence: Off. `PMOS_SCAN=off` |
|
|
@@ -70,4 +71,4 @@ When you ask an agent session to do a piece of work, it can finish it, including
|
|
|
70
71
|
- `pmos ai log [<job>]`: the last audit lines. Each dropped proposal shows why.
|
|
71
72
|
- `pmos ai off [<job>]` and `pmos ai on [<job>]`: turn every AI job, or one job, off or on. `PMOS_AI=off` in the environment wins over the setting.
|
|
72
73
|
|
|
73
|
-
The AI jobs are `past-repeat`, `decision-memory`, `outcomes`, `drift`, `rehearse`, `next-prompt` and `sharpen-prompt`. A background run cannot change the switch.
|
|
74
|
+
The AI jobs are `past-repeat`, `decision-memory`, `outcomes`, `descriptions`, `drift`, `rehearse`, `next-prompt` and `sharpen-prompt`. A background run cannot change the switch.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: Guide
|
|
3
|
+
title: "Initiative descriptions"
|
|
4
|
+
description: "Every initiative says what it is in About, before any PRD exists. Where the description is kept, how Claude drafts it, how you keep one, how a PRD enriches it, and pmos initiative describe."
|
|
5
|
+
timestamp: 2026-11-04
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Initiative descriptions
|
|
9
|
+
|
|
10
|
+
Every initiative has a description: one paragraph that says what it is. About, in the initiative's mission control, shows it. An initiative needs one long before its PRD is written.
|
|
11
|
+
|
|
12
|
+
## Where it is kept
|
|
13
|
+
|
|
14
|
+
The description is the `description:` line in the initiative's record. It is not in the PRD. One paragraph, up to 700 characters.
|
|
15
|
+
|
|
16
|
+
## What About shows
|
|
17
|
+
|
|
18
|
+
- The record has a description: About shows it.
|
|
19
|
+
- The PRD says more than the description: About also shows the PRD's paragraph once, below it, marked **From the PRD**.
|
|
20
|
+
- The record has no description: About shows the PRD's first Problem, Why or Background paragraph, marked **From the PRD**.
|
|
21
|
+
- Neither: About says "No description yet" and shows **Ask Claude to draft it**.
|
|
22
|
+
|
|
23
|
+
## Claude drafts it
|
|
24
|
+
|
|
25
|
+
Once a day Claude drafts a description for an initiative that has none, and files it under the initiative's ✦ in **Check these**. The draft is the PRD's own paragraph, or the first paragraph of the record that says what the work is. Each draft cites the line it comes from. When there is nothing to read, Claude makes no draft. At most 3 drafts a day for each product.
|
|
26
|
+
|
|
27
|
+
When a PRD is written later and says more than the description, Claude drafts **A fuller description from the PRD**.
|
|
28
|
+
|
|
29
|
+
- ✓ saves the draft to the record. Undo puts back what was there.
|
|
30
|
+
- ✕ drops it. Claude does not draft the same one again.
|
|
31
|
+
|
|
32
|
+
## Ask Claude to draft it
|
|
33
|
+
|
|
34
|
+
**Ask Claude to draft it** opens Home's chat with a prompt. Claude reads what it can, shows you a draft, and saves it after you say yes.
|
|
35
|
+
|
|
36
|
+
## Commands
|
|
37
|
+
|
|
38
|
+
- `pmos initiative describe <id>`: the description, and the lines Claude can draft it from.
|
|
39
|
+
- `pmos initiative describe <id> "<text>"`: save the description. Claude runs this only after your yes.
|
|
40
|
+
|
|
41
|
+
## Turn it off
|
|
42
|
+
|
|
43
|
+
`pmos ai off descriptions` stops the daily drafts. `pmos ai on descriptions` starts them again. `PMOS_DESCRIPTIONS=off` also stops them.
|
package/workspace/guide/index.md
CHANGED
|
@@ -56,6 +56,7 @@ PMOS has three places: **Home**, **Board** and **Intelligence**. The round menu
|
|
|
56
56
|
- [Claude proposes: the one ✦ on each initiative](claude-proposes.md): what Claude finds when you add an initiative, new dates, done and tidy names for late tasks, ✓, ✕, Undo, and `pmos propose`.
|
|
57
57
|
- [The next prompt, suggested and sharpened](next-prompt.md): what the chat suggests after a reply, and the sharper version of a rough prompt.
|
|
58
58
|
- [Ask about the past](ask-the-past.md): why something was decided or dropped, answered in the initiative chat from the records; "You decided this before" on a new initiative.
|
|
59
|
+
- [Initiative descriptions](descriptions.md): what About says about an initiative, where it is kept, Claude's drafts, and `pmos initiative describe`.
|
|
59
60
|
- [Expected outcomes](outcomes.md): what each initiative should change once it ships, the date to check it, and `pmos outcomes`.
|
|
60
61
|
- [Rehearse a plan](rehearse.md): what a date change or a cut breaks or helps, before you approve it, and `pmos rehearse`.
|
|
61
62
|
- [Your impact](your-impact.md): what counts as your impact, the weekly check in Home's chat, the switch, and `pmos impact`. Private to you.
|
|
@@ -111,6 +111,7 @@ Every measurement needs a source: the file the value comes from and its commit,
|
|
|
111
111
|
| `pmos initiative drop <proposal>` | Closes a proposal. |
|
|
112
112
|
| `pmos initiative list` | Shows the Roadmap in rank order, with each lifecycle stage, check-by date and the open proposals. |
|
|
113
113
|
| `pmos initiative export <id>` | Writes the breakdown as markdown to the terminal. |
|
|
114
|
+
| `pmos initiative describe <id> ["<text>"]` | Shows the description and the lines Claude can draft it from. With text, saves the description. See [Initiative descriptions](descriptions.md). |
|
|
114
115
|
|
|
115
116
|
Every command takes `--json` and `--root <dir>`. The command does not accept `--by` or `--via`. `pmos initiative now`, `next` and `later` are retired: they write nothing, say what to use instead and exit 2.
|
|
116
117
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
type: Guide
|
|
3
3
|
title: "The next prompt, suggested and sharpened"
|
|
4
4
|
description: "After a reply, the chat suggests what to say next. While you type a rough prompt, Claude offers a sharper version. What each card means, the styles, the settings, and the guardrails."
|
|
5
|
-
timestamp: 2026-11-
|
|
5
|
+
timestamp: 2026-11-04
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# The next prompt, suggested and sharpened
|
|
@@ -30,7 +30,7 @@ Each card names only the initiatives, tasks and PRs that the reply or your messa
|
|
|
30
30
|
|
|
31
31
|
## While you type
|
|
32
32
|
|
|
33
|
-
When you stop typing for about
|
|
33
|
+
When you stop typing for about 2 seconds on a rough prompt of three or more words, one quiet line, **Sharpening…**, shows above the box while Claude sharpens it. This takes a few seconds, and you can keep typing. Then one lit **Sharper** card shows. If you typed on, or no answer comes in 30 seconds, nothing shows. Tap it to put the sharper text in the box. ↶ in the box puts back what you wrote. When the sharper version says the same as yours, no card shows.
|
|
34
34
|
|
|
35
35
|
The sliders icon on the Sharper card sets how Claude sharpens:
|
|
36
36
|
|
|
@@ -38,14 +38,14 @@ The sliders icon on the Sharper card sets how Claude sharpens:
|
|
|
38
38
|
- **Prompt engineer** (the default): the best prompt for the job. It names the real initiative or task and says what a good answer holds. A date move or a cut becomes a rehearsal that waits for your yes.
|
|
39
39
|
- **Your own**: your own wording rules. You write them once in Settings.
|
|
40
40
|
|
|
41
|
-
The same menu has the **Sharpen as I type** switch. Off means no
|
|
41
|
+
The same menu has the **Sharpen as I type** switch. Off means no Sharpening… line and no Sharper card.
|
|
42
42
|
|
|
43
43
|
## Settings › Claude › Sharpen prompts
|
|
44
44
|
|
|
45
45
|
| Row | What it does |
|
|
46
46
|
|---|---|
|
|
47
47
|
| Suggest the next prompt | On by default. Off: no cards after a reply. |
|
|
48
|
-
| Sharpen as I type | On by default. Off: no
|
|
48
|
+
| Sharpen as I type | On by default. Off: no Sharpening… line and no Sharper card. |
|
|
49
49
|
| Sharpen as | Polish, Prompt engineer or Your own. |
|
|
50
50
|
| Your own rules | The rules for Your own, up to 1000 characters. |
|
|
51
51
|
|
|
@@ -163,7 +163,8 @@ function trail({ root, ids, rows = [], moves = [], now = Date.now(), io } = {})
|
|
|
163
163
|
const evalNames = X.list ? X.list('planning/evals') : [];
|
|
164
164
|
const rowBy = new Map(rows.map((r) => [r.id, r]));
|
|
165
165
|
const fmCache = new Map();
|
|
166
|
-
|
|
166
|
+
// a kit product keeps its records in pmos/state/initiatives and its PRDs under pmos/ (initiative-description-field: About read none there)
|
|
167
|
+
const fmOf = (id) => { if (!fmCache.has(id)) { const t = X.text('planning/initiatives/' + id + '.md') ?? X.text('pmos/state/initiatives/' + id + '.md'); fmCache.set(id, t == null ? null : { fm: frontmatter(t), text: t }); } return fmCache.get(id); };
|
|
167
168
|
|
|
168
169
|
// "usually N days": every record's ladder, and every run of board moves
|
|
169
170
|
const samples = Object.fromEntries(STAGES.map((s) => [s, []]));
|
|
@@ -204,8 +205,10 @@ function trail({ root, ids, rows = [], moves = [], now = Date.now(), io } = {})
|
|
|
204
205
|
const stage = wk ? null : stageKey(row.stage);
|
|
205
206
|
const lane = String(row.lane || fm.lane || 'feature').toLowerCase();
|
|
206
207
|
const evals = evalFiles(id, evalNames);
|
|
207
|
-
|
|
208
|
-
|
|
208
|
+
let prdRel = pathOf(fm.prd_path); const rubRel = pathOf(fm.rubric_path);
|
|
209
|
+
let prdText = prdRel ? X.text(prdRel) : null;
|
|
210
|
+
if (prdRel && prdText == null && !prdRel.startsWith('pmos/')) { const k = X.text('pmos/' + prdRel); if (k != null) { prdText = k; prdRel = 'pmos/' + prdRel; } }
|
|
211
|
+
const prdFm = frontmatter(prdText);
|
|
209
212
|
const briefName = (X.list ? X.list('planning/briefs') : []).find((n) => n.startsWith(id) && n.endsWith('.md'));
|
|
210
213
|
const jv = X.jira ? X.jira(id) : null;
|
|
211
214
|
const tks = jv && jv.ok && Array.isArray(jv.tickets) ? jv.tickets : [];
|
|
@@ -276,6 +279,8 @@ function trail({ root, ids, rows = [], moves = [], now = Date.now(), io } = {})
|
|
|
276
279
|
skipped, parts, next: parts ? (parts.find((p) => !p.done) || {}).name || null : null, you,
|
|
277
280
|
links: { prd: prdRel, prdVersion: prdRel && (typeof prdFm.version === 'string' || typeof prdFm.version === 'number') && String(prdFm.version).trim() ? String(prdFm.version).trim().slice(0, 12) : null,
|
|
278
281
|
rubric: rubRel, evaluation: evals[0] || null, epic: jv && jv.ok && jv.epic && jv.epic.key ? jv.epic.key : null },
|
|
282
|
+
// the record's own description (initiative-description-field): About shows it before the PRD's paragraph
|
|
283
|
+
desc: typeof fm.description === 'string' && fm.description.trim() ? fm.description.replace(/\\("|\\)/g, '$1').replace(/\s+/g, ' ').trim().slice(0, 700) : null,
|
|
279
284
|
drafted, gate: jv && jv.ok ? jv.gate || null : null, drift: drift.map((t) => t.key), deps, theme: tkey, tickets,
|
|
280
285
|
};
|
|
281
286
|
}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/* description-props.js — every initiative has its own description, drafted by Claude and kept by the PM (initiative initiative-description-field).
|
|
3
|
+
*
|
|
4
|
+
* drafts(S, { only? }) PURE-ISH (reads files) → { drafts, ask: [{ id, title }] } S is lib/outcomes.js scan(root)
|
|
5
|
+
* new an initiative with no `description:` line: the PRD's paragraph (the first under Problem, Why, Background, Context, Summary or
|
|
6
|
+
* Overview, else its first prose paragraph), else the record's own first paragraph with the record boilerplate left out
|
|
7
|
+
* ("Feature lane.", "Repo-native initiative record", the gate notes). Nothing readable: no draft, the id is listed in `ask`.
|
|
8
|
+
* enrich a description exists and the PRD's paragraph is fuller and not in it yet: the PRD's paragraph replaces it once the PM keeps it.
|
|
9
|
+
* Each draft is the ai-guard shape { id, title, say, confidence, claims: [{ text, sources: [{ label, ref, quote }] }], data: { kind,
|
|
10
|
+
* initiative, text, from? } }, ref path:line from the repository root and quote that line's text. No model: the sources' own words.
|
|
11
|
+
* paragraphOf(lines, { prd }) → { text, n } | null the paragraph and the 1-based line it starts on
|
|
12
|
+
* create(deps) → { tick({ now }), runProduct(w, { now }) } once a day per product, at most PER_DAY drafts under the initiative's ✦
|
|
13
|
+
* (lib/proposals.js group `description`, section Check these), through lib/ai-guard.js file() (job `descriptions`, replay per product).
|
|
14
|
+
* A dropped draft is not filed again; an open `new` draft goes once the record has a description. It never writes a record: only ✓.
|
|
15
|
+
* deps: as lib/outcome-props.js { workspaces(), isLead(w), stateDirFor(w), boardOf(w, id), runtime(), open(now), env, every, mayRun(), warn() }
|
|
16
|
+
* Off: PMOS_DESCRIPTIONS=off, or `pmos ai off descriptions`. */
|
|
17
|
+
const fs = require('fs');
|
|
18
|
+
const path = require('path');
|
|
19
|
+
const crypto = require('crypto');
|
|
20
|
+
const O = require('./outcomes.js');
|
|
21
|
+
const PR = require('./proposals.js');
|
|
22
|
+
|
|
23
|
+
const JOB = 'descriptions';
|
|
24
|
+
const VERSION = 'descriptions-1';
|
|
25
|
+
const PER_DAY = 3;
|
|
26
|
+
const MAX = 700;
|
|
27
|
+
const MIN = 60;
|
|
28
|
+
const HEAD = /^#{1,3}\s*(problem|why|background|context|summary|overview)\b/i;
|
|
29
|
+
const SKIP = /^\s*([-*+]|\d+[.)])\s|^\s*>|^\s*\||^\s*```|^\s*<!--|^\*\*[^*]+:\*\*/;
|
|
30
|
+
// record boilerplate: a sentence that speaks about the record, the lane or the gates, not about the work
|
|
31
|
+
const META = /\blane\b|\(D\d+[^)]*\)|done-criteria|Initiative Gate|frontmatter|front matter|Repo-native|^Anchored to\b|^Batch \d+ item \d+\.?$|\bno PRD\b|\bno rubric\b/i;
|
|
32
|
+
|
|
33
|
+
const flagOff = (v) => /^(off|0|false|no)$/i.test(String(v == null ? '' : v).trim());
|
|
34
|
+
const off = (env) => flagOff((env || process.env).PMOS_DESCRIPTIONS);
|
|
35
|
+
const rel = (root, f) => path.relative(root, f).split(path.sep).join('/');
|
|
36
|
+
const linesOf = (f) => { try { return fs.readFileSync(f, 'utf8').replace(/\r\n?/g, '\n').split('\n'); } catch { return null; } };
|
|
37
|
+
const plain = (s) => String(s).replace(/!\[[^\]]*\]\([^)]*\)/g, '').replace(/\[([^\]]+)\]\([^)]*\)/g, '$1').replace(/[*`]+/g, '').replace(/(^|\W)_+(?=\w)|(?<=\w)_+(?=\W|$)/g, '$1').replace(/<[^>]+>/g, '').replace(/\s+/g, ' ').trim();
|
|
38
|
+
const cut = (t) => (t.length > MAX ? t.slice(0, MAX - 1).replace(/\s+\S*$/, '') + '…' : t);
|
|
39
|
+
const norm = (s) => String(s || '').toLowerCase().replace(/[^a-z0-9]+/g, ' ').trim();
|
|
40
|
+
const clip = (s, n) => { const t = String(s || ''); return t.length > n ? t.slice(0, n - 1).replace(/\s+\S*$/, '') + '…' : t; };
|
|
41
|
+
const localDay = (ms) => { const d = new Date(ms); return d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0'); };
|
|
42
|
+
|
|
43
|
+
/** The body's paragraphs after the front matter: [{ n (1-based), lines: [text] }] */
|
|
44
|
+
function paragraphs(L) {
|
|
45
|
+
let i = 0;
|
|
46
|
+
if (/^---\s*$/.test(L[0] || '')) { i = 1; while (i < L.length && !/^---\s*$/.test(L[i])) i++; i++; }
|
|
47
|
+
const out = [];
|
|
48
|
+
let cur = null;
|
|
49
|
+
for (; i < L.length; i++) {
|
|
50
|
+
const l = L[i];
|
|
51
|
+
if (!l.trim() || /^#{1,6}\s/.test(l)) { if (cur) out.push(cur); cur = null; if (/^#{1,6}\s/.test(l)) out.push({ n: i + 1, head: l }); continue; }
|
|
52
|
+
if (!cur) { if (SKIP.test(l)) continue; cur = { n: i + 1, lines: [] }; }
|
|
53
|
+
else if (SKIP.test(l)) { out.push(cur); cur = null; continue; }
|
|
54
|
+
cur.lines.push(l.trim());
|
|
55
|
+
}
|
|
56
|
+
if (cur) out.push(cur);
|
|
57
|
+
return out;
|
|
58
|
+
}
|
|
59
|
+
/** The paragraph a PRD (prd: true) or a record gives, boilerplate left out of a record's. → { text, n } | null */
|
|
60
|
+
function paragraphOf(L, { prd = false } = {}) {
|
|
61
|
+
if (!L) return null;
|
|
62
|
+
const P = paragraphs(L);
|
|
63
|
+
if (prd) {
|
|
64
|
+
const h = P.findIndex((p) => p.head && HEAD.test(p.head));
|
|
65
|
+
const after = h >= 0 ? P.slice(h + 1).find((p) => p.head || p.lines) : null;
|
|
66
|
+
if (after && after.lines) { const t = plain(after.lines.join(' ')); if (t.length >= MIN) return { text: cut(t), n: after.n }; }
|
|
67
|
+
const first = P.find((p) => p.lines && plain(p.lines.join(' ')).length >= MIN);
|
|
68
|
+
return first ? { text: cut(plain(first.lines.join(' '))), n: first.n } : null;
|
|
69
|
+
}
|
|
70
|
+
// a record: the first paragraph before any section heading that is about the work. A paragraph with any sentence about the record, its
|
|
71
|
+
// lane or the gates is boilerplate and is skipped whole (precision first: a weak draft costs the PM a ✕).
|
|
72
|
+
for (const p of P) {
|
|
73
|
+
if (p.head) { if (/^#{2,6}\s/.test(p.head)) break; continue; }
|
|
74
|
+
const t = plain(p.lines.join(' '));
|
|
75
|
+
if (t.split(/(?<=[.!?:])\s+/).some((x) => META.test(x))) continue;
|
|
76
|
+
if (!/^[A-Z“"]/.test(t) || /^[\w ]{1,24}:\s/.test(t)) continue; // a cut-off continuation or a labelled note ("Rollback: …") is not a description
|
|
77
|
+
if (t.length >= MIN && !/:$/.test(t)) return { text: cut(t), n: p.n };
|
|
78
|
+
}
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function prdFile(root, it) {
|
|
83
|
+
const cands = [];
|
|
84
|
+
if (it.prd) cands.push(path.join(root, it.prd), path.join(root, 'pmos', it.prd));
|
|
85
|
+
cands.push(path.join(root, 'planning', 'prd', it.id + '-prd.md'), path.join(root, 'pmos', 'planning', 'prd', it.id + '-prd.md'));
|
|
86
|
+
for (const f of cands) {
|
|
87
|
+
const abs = path.resolve(f);
|
|
88
|
+
if (abs !== root && !abs.startsWith(root + path.sep)) continue;
|
|
89
|
+
try { if (fs.statSync(abs).isFile()) return abs; } catch { /* next */ }
|
|
90
|
+
}
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
const srcOf = (root, file, L, n, label) => ({ label, ref: rel(root, file) + ':' + n, quote: String(L[n - 1] || '').trim().slice(0, 400) });
|
|
94
|
+
|
|
95
|
+
/** What a PRD says (the source and text), or null. */
|
|
96
|
+
function fromPrd(S, it) {
|
|
97
|
+
const f = prdFile(S.root, it); if (!f) return null;
|
|
98
|
+
const L = linesOf(f), p = paragraphOf(L, { prd: true });
|
|
99
|
+
return p ? { text: p.text, source: srcOf(S.root, f, L, p.n, 'PRD') } : null;
|
|
100
|
+
}
|
|
101
|
+
function fromRecord(S, it) {
|
|
102
|
+
const f = path.join(S.root, it.record), L = linesOf(f), p = paragraphOf(L);
|
|
103
|
+
return p ? { text: p.text, source: srcOf(S.root, f, L, p.n, 'Its record') } : null;
|
|
104
|
+
}
|
|
105
|
+
/** The PRD says more: not already in the description, and fuller. */
|
|
106
|
+
const saysMore = (desc, prd) => { const d = norm(desc), q = norm(prd); return !!q && !d.includes(q.slice(0, 60)) && !q.includes(d) ? q.length > d.length + 40 : false; };
|
|
107
|
+
|
|
108
|
+
/** The ✦ drafts of one repository. */
|
|
109
|
+
function drafts(S, { only = null } = {}) {
|
|
110
|
+
const out = [], ask = [];
|
|
111
|
+
for (const it of S.subjects) {
|
|
112
|
+
if (only && it.id !== only) continue;
|
|
113
|
+
const desc = it.fm.description ? String(it.fm.description.value || '').trim() : '';
|
|
114
|
+
const prd = fromPrd(S, it);
|
|
115
|
+
if (!desc) {
|
|
116
|
+
const d = prd || fromRecord(S, it);
|
|
117
|
+
if (!d) { ask.push({ id: it.id, title: it.title }); continue; }
|
|
118
|
+
out.push({ id: `description-new:${it.id}`, title: `Description for ${clip(it.title, 80)}`, confidence: 'medium',
|
|
119
|
+
claims: [{ text: clip(`${d.source.label === 'PRD' ? 'Its PRD' : 'Its record'} says: ${d.text}`, 240), sources: [d.source] }],
|
|
120
|
+
say: `It has no description yet. I suggest: "${d.text}"`, data: { kind: 'new', initiative: it.id, text: d.text } });
|
|
121
|
+
} else if (prd && saysMore(desc, prd.text)) {
|
|
122
|
+
out.push({ id: `description-enrich:${it.id}`, title: `A fuller description for ${clip(it.title, 80)}`, confidence: 'medium',
|
|
123
|
+
claims: [{ text: clip('Its PRD says: ' + prd.text, 240), sources: [prd.source] }],
|
|
124
|
+
say: `Its PRD says more than its description now. I suggest: "${prd.text}"`, data: { kind: 'enrich', initiative: it.id, text: prd.text, from: desc } });
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return { drafts: out, ask };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** The proposals line a draft becomes. */
|
|
131
|
+
function lineOf(d, board, run) {
|
|
132
|
+
const type = d.data.kind === 'enrich' ? 'enrich' : 'new', ini = d.data.initiative;
|
|
133
|
+
const c = PR.clean('description', type, { title: type === 'enrich' ? 'A fuller description from the PRD' : 'What this initiative is', why: d.say,
|
|
134
|
+
sources: d.claims.flatMap((x) => x.sources).map((s) => ({ label: s.label, ref: s.ref })), items: [{ id: 'description', title: d.data.text }],
|
|
135
|
+
data: { text: d.data.text, ...(d.data.from ? { from: d.data.from } : {}) } });
|
|
136
|
+
if (c.error) return null;
|
|
137
|
+
return { kind: 'propose', ini, board, run, id: PR.idOf(ini, 'description', type, ''), ...c.prop };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** One product's replay cases: its own drafts must all pass, and an initiative with nothing readable gets no draft. */
|
|
141
|
+
function cases(S, list) {
|
|
142
|
+
return [{ name: 'the drafts of ' + String(S.root).split(/[\\/]/).pop(), run: async () => list.slice(0, PER_DAY), expect: { filed: Math.min(PER_DAY, list.length) } },
|
|
143
|
+
{ name: 'nothing readable, no draft', run: async () => drafts({ ...S, subjects: [{ id: 'zz-no-source', title: 'No source', fm: {}, prd: '', record: 'zz-none/zz-no-source.md' }] }).drafts, expect: { none: true } }];
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function create(deps) {
|
|
147
|
+
const warn = deps.warn || (() => {});
|
|
148
|
+
const lastDay = new Map();
|
|
149
|
+
let busy = false;
|
|
150
|
+
const guardLib = () => { try { return require('./ai-guard.js'); } catch { return null; } };
|
|
151
|
+
|
|
152
|
+
async function runProduct(w, { now = Date.now() } = {}) {
|
|
153
|
+
const dir = deps.stateDirFor(w); if (!dir) return { ws: w.id, filed: 0, why: 'no state folder' };
|
|
154
|
+
const rt = deps.runtime && deps.runtime();
|
|
155
|
+
const S = O.scan(w.root, { today: localDay(now) });
|
|
156
|
+
const F = PR.read(dir), kept = PR.keptIds(F);
|
|
157
|
+
// a record that has its description now: its open draft goes (zero is silence)
|
|
158
|
+
const has = new Map(S.subjects.map((it) => [it.id, it]));
|
|
159
|
+
for (const p of F.props.values()) {
|
|
160
|
+
if (p.group !== 'description' || kept.has(p.id)) continue;
|
|
161
|
+
const it = has.get(p.ini), sig = PR.sigOf(p);
|
|
162
|
+
if (F.drops.has(sig)) continue;
|
|
163
|
+
const desc = it && it.fm.description ? String(it.fm.description.value || '') : '';
|
|
164
|
+
if (!it || (p.type === 'new' && desc) || (p.type === 'enrich' && p.data && norm(desc) === norm(p.data.text))) PR.append(dir, { by: 'pmos:descriptions' }, { kind: 'drop', ini: p.ini, sig });
|
|
165
|
+
}
|
|
166
|
+
const fresh = [];
|
|
167
|
+
for (const d of drafts(S).drafts) {
|
|
168
|
+
const board = deps.boardOf(w, d.data.initiative); if (!board) continue;
|
|
169
|
+
const l = lineOf(d, board, ''); if (!l) continue;
|
|
170
|
+
const prev = F.props.get(l.id);
|
|
171
|
+
if (kept.has(l.id)) continue;
|
|
172
|
+
if (prev && (F.drops.has(PR.sigOf(prev)) || (prev.data && prev.data.text === l.data.text))) continue; // dropped for good, or filed already
|
|
173
|
+
fresh.push({ d, board });
|
|
174
|
+
if (fresh.length >= PER_DAY) break;
|
|
175
|
+
}
|
|
176
|
+
if (!fresh.length) return { ws: w.id, filed: 0 };
|
|
177
|
+
const AG = guardLib(); if (!AG) return { ws: w.id, filed: 0, why: 'the shared guard is not installed' };
|
|
178
|
+
const version = VERSION + ':' + w.id, run = 'ds' + now.toString(36) + crypto.randomBytes(3).toString('hex');
|
|
179
|
+
if (!AG.off(JOB, { env: deps.env || process.env, settingsDir: rt }) && !AG.ready(rt, JOB, version)) {
|
|
180
|
+
const rep = await AG.replay(JOB, version, cases(S, fresh.map((x) => x.d)), { root: w.root, stateDir: rt, max: PER_DAY });
|
|
181
|
+
if (!rep.ok) { warn('descriptions replay ' + w.id, new Error(rep.failed.map((f) => f.name + ': ' + f.why).join('; '))); return { ws: w.id, filed: 0, replay: rep }; }
|
|
182
|
+
}
|
|
183
|
+
const byId = new Map(fresh.map((x) => [x.d.id, x]));
|
|
184
|
+
const r = await AG.file({ job: JOB, version, run, root: w.root, stateDir: rt, settingsDir: rt, env: deps.env || process.env, max: PER_DAY }, fresh.map((x) => x.d),
|
|
185
|
+
async (acc) => { for (const p of acc) { const x = byId.get(p.id); const l = x && lineOf(x.d, x.board, run); if (l) PR.append(dir, { by: 'agent:descriptions' }, l); } });
|
|
186
|
+
return { ws: w.id, filed: (r.filed || []).length, held: !!r.held, off: r.off || null };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async function tick({ now = Date.now() } = {}) {
|
|
190
|
+
if (off(deps.env) || busy) return { off: off(deps.env), running: busy };
|
|
191
|
+
let open = true;
|
|
192
|
+
try { open = deps.open ? !!deps.open(now) : true; } catch { open = true; }
|
|
193
|
+
if (!open) return { closed: true };
|
|
194
|
+
busy = true;
|
|
195
|
+
const out = [];
|
|
196
|
+
try {
|
|
197
|
+
const day = localDay(now);
|
|
198
|
+
for (const w of deps.workspaces()) {
|
|
199
|
+
if (lastDay.get(w.id) === day) continue;
|
|
200
|
+
try { if (deps.isLead && !deps.isLead(w)) continue; } catch { continue; }
|
|
201
|
+
lastDay.set(w.id, day);
|
|
202
|
+
try { out.push(await runProduct(w, { now })); } catch (e) { warn('descriptions ' + w.id, e); }
|
|
203
|
+
}
|
|
204
|
+
} finally { busy = false; }
|
|
205
|
+
return { ran: out };
|
|
206
|
+
}
|
|
207
|
+
if (deps.every > 0) {
|
|
208
|
+
const t = setInterval(() => { try { if (deps.mayRun && !deps.mayRun()) return; } catch { /* no lock: run */ } tick({}).catch((e) => warn('descriptions tick', e)); }, deps.every);
|
|
209
|
+
if (t.unref) t.unref();
|
|
210
|
+
}
|
|
211
|
+
return { tick, runProduct };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
module.exports = { JOB, VERSION, PER_DAY, drafts, paragraphOf, lineOf, cases, create, off, fromPrd, fromRecord };
|
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
* pmos initiative drop <proposal-eid>
|
|
12
12
|
* pmos initiative list
|
|
13
13
|
* pmos initiative export <id> the breakdown as markdown on stdout; nothing is written
|
|
14
|
+
* pmos initiative describe <id> ["<text>"] no text: its description and the cited lines Claude can draft it from; with text: write
|
|
15
|
+
* its `description:` line (initiative-description-field). An agent writes only after the PM's yes
|
|
14
16
|
* every command: [--json] [--root <dir>]
|
|
15
17
|
*
|
|
16
18
|
* The SAME writers the workspace page uses (lib/initiative-writer.js, lib/placements.js) — no server is needed. Who
|
|
@@ -59,11 +61,11 @@ function docNote(docs) {
|
|
|
59
61
|
return `Not on the Roadmap · ${docs.length} on a document lane: ${names}. A document lane does not show on Home or the Board. If it is work, set lane: in its record to ${WORK_LANES}.`;
|
|
60
62
|
}
|
|
61
63
|
|
|
62
|
-
const COMMANDS = { initiative: ['new', 'top', 'up', 'down', 'bottom', 'after', 'check-by', 'adopt', 'drop', 'list', 'export'] };
|
|
64
|
+
const COMMANDS = { initiative: ['new', 'top', 'up', 'down', 'bottom', 'after', 'check-by', 'adopt', 'drop', 'list', 'export', 'describe'] };
|
|
63
65
|
/** The horizon verbs D105 retired: they answer with what to use instead. */
|
|
64
66
|
const RETIRED = Object.freeze(['now', 'next', 'later']);
|
|
65
67
|
const USAGE = [
|
|
66
|
-
'usage: pmos initiative new|top|up|down|bottom|after|check-by|adopt|drop|list|export — initiatives on the Roadmap',
|
|
68
|
+
'usage: pmos initiative new|top|up|down|bottom|after|check-by|adopt|drop|list|export|describe — initiatives on the Roadmap',
|
|
67
69
|
' pmos initiative new "<title>" --lane <lane> --anchor <KR|health:id> [--intent "<text>"] [--id <id>]',
|
|
68
70
|
' pmos initiative new --from capture-YYYY-MM-DD-<n> [--anchor <anchor>] [--lane <lane>] [--title "<title>"]',
|
|
69
71
|
' pmos initiative top|up|down|bottom <id> reorder it on the Roadmap',
|
|
@@ -73,6 +75,7 @@ const USAGE = [
|
|
|
73
75
|
' pmos initiative drop <proposal> close an open proposal',
|
|
74
76
|
' pmos initiative list the Roadmap, ranked, with each lifecycle stage',
|
|
75
77
|
' pmos initiative export <id> the breakdown as markdown on stdout',
|
|
78
|
+
' pmos initiative describe <id> ["<text>"] read its description and what to draft it from; with text, save it (after the PM\'s yes)',
|
|
76
79
|
' every command: [--json] [--root <dir>]',
|
|
77
80
|
'An agent session\'s move applies at once and names the agent. A new initiative lands at the end of the Roadmap.',
|
|
78
81
|
'Now, Next and Later are retired (D105): the rank says what comes first, the lifecycle stage says where it is.',
|
|
@@ -205,6 +208,29 @@ async function run(argv, { say = (s) => console.log(s), cwd = process.cwd(), isT
|
|
|
205
208
|
return done({ written: r.written }, `✓ proposal ${eid} ${verb === 'adopt' ? 'kept' : 'dropped'}`);
|
|
206
209
|
}
|
|
207
210
|
|
|
211
|
+
if (verb === 'describe') {
|
|
212
|
+
// initiative-description-field: the record's own description. Read with no text; write with text, only after the PM said yes.
|
|
213
|
+
const did = pos[0];
|
|
214
|
+
if (!did || pos.length > 2) { say('usage: pmos initiative describe <id> ["<text>"]'); return 2; }
|
|
215
|
+
if (pos.length === 2) {
|
|
216
|
+
const r = writer.setDescription(root, did, pos[1], { actor: actorLib.fromCli(who) });
|
|
217
|
+
if (!r.ok) return fail(r.error, r.status === 400 ? 2 : 1);
|
|
218
|
+
return done({ id: did, description: r.text, from: r.from, same: !!r.same }, r.same ? `${did} says that already.` : `✓ ${did}: description saved. About shows it now. Commit the record to keep it.`);
|
|
219
|
+
}
|
|
220
|
+
const O = require('./outcomes.js'), DP = require('./description-props.js');
|
|
221
|
+
const S = O.scan(root);
|
|
222
|
+
const it = S.subjects.find((x) => x.id === did);
|
|
223
|
+
if (!it) return fail(`no initiative ${did} here (or it is stopped, or on a document lane)`);
|
|
224
|
+
const cur = it.fm.description ? String(it.fm.description.value || '') : '';
|
|
225
|
+
const srcs = [DP.fromPrd(S, it), DP.fromRecord(S, it)].filter(Boolean);
|
|
226
|
+
if (json) { say(JSON.stringify({ ok: true, id: did, description: cur || null, sources: srcs.map((x) => ({ label: x.source.label, ref: x.source.ref, text: x.text })) })); return 0; }
|
|
227
|
+
say(cur ? `${did}: ${cur}` : `${did} has no description yet.`);
|
|
228
|
+
if (srcs.length) say('Draft it from:\n' + srcs.map((x) => ` · ${x.source.label} [${x.source.ref}]: ${x.text}`).join('\n'));
|
|
229
|
+
else say('Nothing readable to draft it from (no PRD paragraph, no paragraph in its record): ask the PM what it is.');
|
|
230
|
+
say(`Show the draft to the PM; after a yes: pmos initiative describe ${did} "<text>"`);
|
|
231
|
+
return 0;
|
|
232
|
+
}
|
|
233
|
+
|
|
208
234
|
const id = pos[0];
|
|
209
235
|
if (!id) { say(`usage: pmos initiative ${verb} <id>${verb === 'check-by' ? ' <YYYY-MM-DD|none>' : ''}`); return 2; }
|
|
210
236
|
|
|
@@ -298,6 +298,46 @@ function restoreOutcome(root, id, from = {}, to = {}, { actor } = {}) {
|
|
|
298
298
|
return { ok: true, id };
|
|
299
299
|
}
|
|
300
300
|
|
|
301
|
+
/** The record's own description (initiative-description-field): one `description:` line in the front matter, one paragraph of at most
|
|
302
|
+
* DESC_MAX characters. About shows it before the PRD's paragraph (pm/ini-about.js). The PM's ✓ on a ✦ draft (lib/routes-proposals.js) and
|
|
303
|
+
* `pmos initiative describe <id> "<text>"` after the PM's yes reach here. → { ok: true, id, from, text, same? } | refusal */
|
|
304
|
+
const DESC_MAX = 700;
|
|
305
|
+
const frontOfText = (text) => /^---\r?\n([\s\S]*?)\r?\n---(\r?\n|$)/.exec(text);
|
|
306
|
+
const descLine = (lines) => { const at = lines.findIndex((l) => l.startsWith('description:')); return { at, val: at >= 0 ? unquote(lines[at].slice(12)) : '' }; };
|
|
307
|
+
function setDescription(root, id, text0, { actor } = {}) {
|
|
308
|
+
if (!actorLib.valid(actor)) return refuse(500, 'initiative-writer: the caller must pass the actor from pm-guard.js or actor.js');
|
|
309
|
+
const t = typeof text0 === 'string' ? text0.replace(/\s+/g, ' ').trim() : '';
|
|
310
|
+
if (t.length < 12 || t.length > DESC_MAX) return refuse(400, `say what the initiative is in one paragraph of 12 to ${DESC_MAX} characters`, 'description');
|
|
311
|
+
const f = recordFile(root, id);
|
|
312
|
+
if (!f.ok) return f;
|
|
313
|
+
const text = fs.readFileSync(f.file, 'utf8');
|
|
314
|
+
const fm = frontOfText(text);
|
|
315
|
+
if (!fm) return refuse(409, `the record of ${id} has no front matter — nothing was saved`, 'id');
|
|
316
|
+
const lines = fm[1].split(/\r?\n/);
|
|
317
|
+
const { at, val: from } = descLine(lines);
|
|
318
|
+
if (from === t) return { ok: true, id, from, text: t, same: true };
|
|
319
|
+
const line = 'description: ' + q(t);
|
|
320
|
+
if (at >= 0) lines[at] = line; else lines.push(line);
|
|
321
|
+
try { fs.writeFileSync(f.file, '---\n' + lines.join('\n') + '\n---' + fm[2] + text.slice(fm[0].length), { flag: 'w' }); } catch { return refuse(500, 'the record could not be written — nothing was saved'); }
|
|
322
|
+
return { ok: true, id, from, text: t };
|
|
323
|
+
}
|
|
324
|
+
/** The Undo of a ✓ on a description ✦: the line back to `from` (removed when there was none), only while it still says `to`.
|
|
325
|
+
* A record the PM edited since keeps its edit (→ { ok: true, kept: true }). */
|
|
326
|
+
function restoreDescription(root, id, from, to, { actor } = {}) {
|
|
327
|
+
if (!actorLib.valid(actor)) return refuse(500, 'initiative-writer: the caller must pass the actor from pm-guard.js or actor.js');
|
|
328
|
+
const f = recordFile(root, id);
|
|
329
|
+
if (!f.ok) return f;
|
|
330
|
+
const text = fs.readFileSync(f.file, 'utf8');
|
|
331
|
+
const fm = frontOfText(text);
|
|
332
|
+
if (!fm) return refuse(409, `the record of ${id} has no front matter — nothing was saved`, 'id');
|
|
333
|
+
const lines = fm[1].split(/\r?\n/);
|
|
334
|
+
const { at, val } = descLine(lines);
|
|
335
|
+
if (val !== String(to || '')) return { ok: true, id, kept: true };
|
|
336
|
+
if (from) { const line = 'description: ' + q(from); if (at >= 0) lines[at] = line; else lines.push(line); } else if (at >= 0) lines.splice(at, 1);
|
|
337
|
+
try { fs.writeFileSync(f.file, '---\n' + lines.join('\n') + '\n---' + fm[2] + text.slice(fm[0].length), { flag: 'w' }); } catch { return refuse(500, 'the record could not be written — nothing was saved'); }
|
|
338
|
+
return { ok: true, id };
|
|
339
|
+
}
|
|
340
|
+
|
|
301
341
|
function withdrawSplit(root, id, want, { actor } = {}) {
|
|
302
342
|
if (!actorLib.valid(actor)) return refuse(500, 'initiative-writer: the caller must pass the actor from pm-guard.js or actor.js');
|
|
303
343
|
const f = recordFile(root, id);
|
|
@@ -309,4 +349,4 @@ function withdrawSplit(root, id, want, { actor } = {}) {
|
|
|
309
349
|
return { ok: true, removed: true };
|
|
310
350
|
}
|
|
311
351
|
|
|
312
|
-
module.exports = { setOutcome, restoreOutcome, createInitiative, renameInitiative, setAnchor, restoreAnchor, withdrawSplit, recordSha, slugOf, ID_RE, INPUT_KEYS };
|
|
352
|
+
module.exports = { setDescription, restoreDescription, DESC_MAX, setOutcome, restoreOutcome, createInitiative, renameInitiative, setAnchor, restoreAnchor, withdrawSplit, recordSha, slugOf, ID_RE, INPUT_KEYS };
|
|
@@ -18,6 +18,9 @@
|
|
|
18
18
|
* outcome the expected outcome and its check date (D57; lib/outcomes.js, filed by lib/outcome-props.js): type new (no outcome yet) or date
|
|
19
19
|
* (an outcome with no date). data { metric, horizon }; one item { id: 'outcome', title: the metric, due: the date }. Always section
|
|
20
20
|
* 'check': its own ✓, never Keep all. ✓ writes the record's outcome_metric and outcome_horizon lines (lib/initiative-writer.js)
|
|
21
|
+
* description the record's own description (initiative-description-field; filed by lib/description-props.js): type new (none yet) or
|
|
22
|
+
* enrich (the PRD says more). data { text, from? }; one item { id: 'description', title: the text cut }. Always section 'check'.
|
|
23
|
+
* ✓ writes the record's description line (lib/initiative-writer.js setDescription)
|
|
21
24
|
* grill what Claude found when it grilled the initiative by itself (lib/grill.js): type = the angle, no items, a next step in data.next;
|
|
22
25
|
* always section 'check' (its own ✓, never Keep all). 3 show, best rank first; the group's `more` counts the rest
|
|
23
26
|
*
|
|
@@ -56,8 +59,8 @@ const crypto = require('crypto');
|
|
|
56
59
|
|
|
57
60
|
const LEDGER = 'proposals.jsonl';
|
|
58
61
|
const KINDS = new Set(['epoch', 'look', 'src', 'end', 'propose', 'drop', 'keep', 'undo']);
|
|
59
|
-
const GROUPS = ['found', 'dates', 'status', 'names', 'outcome'];
|
|
60
|
-
const TYPES = { found: ['stage', 'docs', 'tasks', 'link', 'maybe', 'unplaced'], dates: ['stage', 'task'], status: ['done'], names: ['tidy'], outcome: ['new', 'date'] };
|
|
62
|
+
const GROUPS = ['found', 'dates', 'status', 'names', 'outcome', 'description'];
|
|
63
|
+
const TYPES = { found: ['stage', 'docs', 'tasks', 'link', 'maybe', 'unplaced'], dates: ['stage', 'task'], status: ['done'], names: ['tidy'], outcome: ['new', 'date'], description: ['new', 'enrich'] };
|
|
61
64
|
const SOURCES = ['drive', 'docs', 'sheets', 'slides', 'jira', 'calendar', 'notes', 'mail', 'slack', 'confluence', 'chat', 'repo'];
|
|
62
65
|
const SOURCE_NAMES = { drive: 'Drive and Slides', docs: 'Docs', sheets: 'Sheets', slides: 'Slides', jira: 'Jira', calendar: 'Calendar and meeting notes', notes: 'Meeting notes',
|
|
63
66
|
mail: 'Gmail', slack: 'Slack', confluence: 'Confluence', chat: 'Chat', repo: 'This repository' };
|
|
@@ -155,7 +158,13 @@ function clean(group, type, input) {
|
|
|
155
158
|
if (!DAY_RE.test(horizon)) return { error: 'data.horizon is the check date, YYYY-MM-DD' };
|
|
156
159
|
data.metric = metric; data.horizon = horizon;
|
|
157
160
|
}
|
|
158
|
-
|
|
161
|
+
if (group === 'description') {
|
|
162
|
+
const d = isObj(input.data) ? input.data : {};
|
|
163
|
+
const text = one(d.text, 700);
|
|
164
|
+
if (text.length < 12) return { error: 'data.text is the description, one paragraph of 12 to 700 characters' };
|
|
165
|
+
data.text = text; if (str(d.from)) data.from = one(d.from, 700);
|
|
166
|
+
}
|
|
167
|
+
return { prop: { group, type, section: group === 'outcome' || group === 'description' ? 'check' : section, title: one(input.title, LIM.title) || null, why, sources: s.v, items, data } };
|
|
159
168
|
}
|
|
160
169
|
/** The proposal's own id: one per initiative, group, type and key, so a newer run files over the older one. */
|
|
161
170
|
const idOf = (ini, group, type, key) => 'pr-' + hash([ini, group, type, key || ''].join('|'));
|
|
@@ -323,7 +332,7 @@ function datesOf(ini, T, o = {}) {
|
|
|
323
332
|
}
|
|
324
333
|
|
|
325
334
|
/* ── what r.props carries ── */
|
|
326
|
-
const ORDER = { outcome: -1, found: 0, dates: 1, status: 2, names: 3, links: 4, grill: 5 };
|
|
335
|
+
const ORDER = { description: -2, outcome: -1, found: 0, dates: 1, status: 2, names: 3, links: 4, grill: 5 };
|
|
327
336
|
const GRILL_SHOWN = 3;
|
|
328
337
|
/**
|
|
329
338
|
* One initiative's proposals, as the ✦ and the Viewer draw them, or null when there is nothing to show.
|
|
@@ -31,6 +31,11 @@ const ROOT = path.join(__dirname, '..'); // the workspace package: the files t
|
|
|
31
31
|
const FIXTURE = path.join(ROOT, 'fixtures/next-prompt-replay.json');
|
|
32
32
|
const KEEP = 30;
|
|
33
33
|
const TIMEOUT_MS = 90000;
|
|
34
|
+
const SHARPEN_TIMEOUT_MS = 30000; // a sharper prompt that comes later than this is for words the PM has typed past: drop it
|
|
35
|
+
/* A background run for these two short jobs starts lean (hotfix 2026-11-04, Wawan: "Loading too long"): a fixed fast model at low effort,
|
|
36
|
+
* no MCP servers, no skills, only the PM's user settings (no project hooks such as a session-start script) and one read tool. Measured on
|
|
37
|
+
* one sharpen prompt: about 61,000 input tokens and 7 s before, about 10,000 and 5 s after; the rest is the CLI's own start. */
|
|
38
|
+
const LEAN_FLAGS = Object.freeze([{ flag: '--strict-mcp-config' }, { flag: '--disable-slash-commands' }, { flag: '--setting-sources', value: 'user' }, { flag: '--tools', value: 'Read' }]);
|
|
34
39
|
const DEFAULT_MODEL = 'claude-sonnet-5-5';
|
|
35
40
|
|
|
36
41
|
const text = (v, n) => (typeof v === 'string' ? v.slice(0, n) : '');
|
|
@@ -112,16 +117,18 @@ async function viaApi(key, prompt, env, create = null) {
|
|
|
112
117
|
return null;
|
|
113
118
|
}
|
|
114
119
|
/** A background read-only Claude run → its answer text, or null after `fail(why)` says why it gave none. */
|
|
115
|
-
function viaHeadless(ctx, prompt, origin, fail = () => {}) {
|
|
120
|
+
function viaHeadless(ctx, prompt, origin, fail = () => {}, { timeoutMs = TIMEOUT_MS } = {}) {
|
|
116
121
|
const headless = require('./headless.js');
|
|
117
122
|
if (typeof ctx.spawnSession !== 'function') { fail('no background run here'); return Promise.resolve(null); }
|
|
118
123
|
if (headless.kindFor({ agent: 'claude', origin }) !== 'ask') { fail('the run would not be read-only'); return Promise.resolve(null); }
|
|
119
124
|
return new Promise((resolve) => {
|
|
120
125
|
let settled = false; const done = (v, why) => { if (settled) return; settled = true; clearTimeout(t); if (v == null) fail(why); resolve(v); };
|
|
121
|
-
const t = setTimeout(() => done(null, 'the run took longer than ' + Math.round(
|
|
126
|
+
const t = setTimeout(() => done(null, 'the run took longer than ' + Math.round(timeoutMs / 1000) + ' s'), timeoutMs);
|
|
122
127
|
let s = null;
|
|
123
128
|
try {
|
|
129
|
+
const model = String(envOf(ctx).PMOS_NEXT_PROMPT_MODEL || '').trim() || DEFAULT_MODEL;
|
|
124
130
|
s = ctx.spawnSession({ name: 'chat: next prompt', agent: 'claude', origin, initialInput: prompt,
|
|
131
|
+
modelAsk: { explicit: { model, effort: 'low' } }, launchFlags: LEAN_FLAGS.map((f) => ({ ...f })),
|
|
125
132
|
onDone: (d) => { const r = d && d.result; done(r && r.ok && !r.isError ? String(r.text || '') : null, 'the run ended without an answer' + (d && d.note ? ': ' + String(d.note).slice(0, 80) : '')); } });
|
|
126
133
|
} catch (e) { s = null; }
|
|
127
134
|
if (!s || s.state === 'exited') done(null, 'the run did not start' + (s && s.note ? ': ' + String(s.note).slice(0, 80) : ''));
|
|
@@ -145,7 +152,8 @@ async function ask(ctx, prompt, { job, key, run }) {
|
|
|
145
152
|
catch (e) { failed(apiWhy(e), e && (e.status === 401 || e.status === 403)); }
|
|
146
153
|
}
|
|
147
154
|
return viaHeadless(ctx, prompt, `briefing-ask:next-prompt:${job}:${crypto.createHash('sha1').update(String(key)).digest('hex').slice(0, 10)}`,
|
|
148
|
-
(why) => AG.audit(ctx.runtimeDir, { job, version: VERSION, run, event: 'run-error', error: String(why).slice(0, 160) })
|
|
155
|
+
(why) => AG.audit(ctx.runtimeDir, { job, version: VERSION, run, event: 'run-error', error: String(why).slice(0, 160) }),
|
|
156
|
+
{ timeoutMs: job === NP.JOB_SHARPEN ? SHARPEN_TIMEOUT_MS : TIMEOUT_MS });
|
|
149
157
|
}
|
|
150
158
|
|
|
151
159
|
/* ── what a card cites ───────────────────────────────────────────────────────────────────── */
|
|
@@ -285,4 +293,4 @@ async function handle(req, res, url, ctx) {
|
|
|
285
293
|
catch (e) { return send(bad(500, 'The suggestion failed: ' + String((e && e.message) || e).slice(0, 200))); }
|
|
286
294
|
}
|
|
287
295
|
|
|
288
|
-
module.exports = { prefixes: ['/api/next-prompt'], handle, VERSION, FIXTURE, MODELS, prefs, setPref, lineRef, replayCases, next, sharpen };
|
|
296
|
+
module.exports = { prefixes: ['/api/next-prompt'], handle, VERSION, FIXTURE, MODELS, LEAN_FLAGS, prefs, setPref, lineRef, replayCases, next, sharpen };
|
|
@@ -138,6 +138,10 @@ function applyOne(ctx, p, g, f, ini, x, choices, outbox) {
|
|
|
138
138
|
const d = x.data || {};
|
|
139
139
|
const r = require('./initiative-writer.js').setOutcome(p.w.root, ini, { metric: d.metric, horizon: d.horizon }, { actor: g.actor });
|
|
140
140
|
if (r.ok && !r.same) add({ op: 'outcome', from: r.from, to: { metric: r.metric, horizon: r.horizon } });
|
|
141
|
+
} else if (x.group === 'description') {
|
|
142
|
+
// the record's own description (initiative-description-field): ✓ writes its description line, Undo puts back what was there
|
|
143
|
+
const r = require('./initiative-writer.js').setDescription(p.w.root, ini, (x.data || {}).text, { actor: g.actor });
|
|
144
|
+
if (r.ok && !r.same) add({ op: 'description', from: r.from, to: r.text });
|
|
141
145
|
} else if (x.group === 'grill') {
|
|
142
146
|
// a grill finding (lib/grill.js): ✓ adds its next step as a task, with the finding and its sources in the notes
|
|
143
147
|
// ✎ on the finding: the PM's own words for the next step (choices[id]) replace Claude's
|
|
@@ -301,6 +305,7 @@ const WRITES = {
|
|
|
301
305
|
else if (w.op === 'prd') { try { const file = path.resolve(p.w.root, w.file), text = fs.readFileSync(file, 'utf8'); if (text.includes(w.to)) { fs.writeFileSync(file, text.split(w.to).join(w.from)); back++; } else kept++; } catch { kept++; } continue; }
|
|
302
306
|
else if (w.op === 'doc' || w.op === 'linked' || w.op === 'ran') { back++; continue; } // the fold leaves an undone batch out
|
|
303
307
|
else if (w.op === 'outbox') { kept++; continue; } // a drafted comment waits in the outbox, where the PM drops it
|
|
308
|
+
else if (w.op === 'description') { const r2 = require('./initiative-writer.js').restoreDescription(p.w.root, k.ini, w.from || '', w.to || '', { actor: g.actor }); if (r2.ok && !r2.kept) back++; else kept++; continue; } // a record edited since keeps its edit
|
|
304
309
|
else if (w.op === 'outcome') { const r2 = require('./initiative-writer.js').restoreOutcome(p.w.root, k.ini, w.from || {}, w.to || {}, { actor: g.actor }); if (r2.ok && !r2.kept) back++; else kept++; continue; } // a record edited since keeps its edit
|
|
305
310
|
if (r && r.body && r.body.ok) back++; else kept++;
|
|
306
311
|
}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pmos-workspace",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.521.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "pmos-workspace",
|
|
9
|
-
"version": "0.
|
|
9
|
+
"version": "0.521.0",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@anthropic-ai/sdk": "^1.3.0",
|
package/workspace/package.json
CHANGED
|
@@ -176,7 +176,7 @@ body.cz-home { min-height: 100vh; overflow: hidden; }
|
|
|
176
176
|
@media (min-width: 761px) {
|
|
177
177
|
/* the log scrolls at the panel's edge, not at the column's (PM, 2026-10-27 "Bad layout": the scrollbar stood in the middle of the page);
|
|
178
178
|
* its content and the header line keep to the same centred column as the box */
|
|
179
|
-
body.cz-home:not(.hm-vw-open) .hm-foot { box-sizing: border-box; width: 100%; max-width: 880px; align-self: center; }
|
|
179
|
+
body.cz-home:not(.hm-vw-open) .hm-foot, body.cz-home:not(.hm-vw-open) .np { box-sizing: border-box; width: 100%; max-width: 880px; align-self: center; }
|
|
180
180
|
body.cz-home:not(.hm-vw-open) .hm-log { box-sizing: border-box; width: 100%; padding-inline: max(0px, calc((100% - 880px) / 2)); }
|
|
181
181
|
body.cz-home:not(.hm-vw-open) .hm-chd { padding-left: max(0px, calc((100% - 880px) / 2)); }
|
|
182
182
|
@container (max-width: 968px) { .hm-reopen:not([hidden]) ~ .hm-log { padding-top: 46px; } }
|
|
@@ -810,13 +810,10 @@ form.hm-box textarea { min-height: 48px; border: 0; background: none; resize: no
|
|
|
810
810
|
.np-use { display: flex; align-items: flex-start; gap: 10px; flex: 1; min-width: 0; padding: 11px 0 11px 14px; border: 0; background: none; color: inherit; font: inherit; text-align: left; cursor: pointer; }
|
|
811
811
|
.np-ib { flex: none; width: 36px; height: 36px; display: inline-grid; place-items: center; border: 0; border-radius: 999px; background: none; color: var(--cz-ink2); cursor: pointer; }
|
|
812
812
|
.np-ib:hover, .np-ib[aria-expanded="true"] { background: var(--cz-well); }
|
|
813
|
-
.np-spin { display: flex; align-items: center; gap:
|
|
813
|
+
.np-spin { display: flex; align-items: center; gap: 8px; padding: 2px 14px; font-size: var(--fs-meta); color: var(--cz-ink2); } /* quiet: one small line, not a card */
|
|
814
814
|
.np-sp { display: inline-flex; animation: np-rot 1.1s linear infinite; }
|
|
815
815
|
@keyframes np-rot { to { transform: rotate(360deg); } }
|
|
816
816
|
@media (prefers-reduced-motion: reduce) { .np-sp { animation: none; } }
|
|
817
|
-
.np-sk { flex: 1; display: flex; flex-direction: column; gap: 8px; }
|
|
818
|
-
.np-sk i { display: block; height: 12px; border-radius: 6px; background: var(--cz-bluebg); }
|
|
819
|
-
.np-sk i:first-child { width: 88%; } .np-sk i:last-child { width: 60%; }
|
|
820
817
|
.np-menu { position: absolute; right: 0; bottom: calc(100% + 8px); z-index: 20; width: 310px; max-width: calc(100vw - 32px); box-sizing: border-box; padding: 8px; border-radius: 20px;
|
|
821
818
|
background: var(--cz-card); box-shadow: 0 24px 60px -24px rgba(42,46,53,.45), 0 0 0 1px var(--cz-hair); display: flex; flex-direction: column; gap: 2px; }
|
|
822
819
|
.np-mh { margin: 0; padding: 8px 12px 4px; font-size: var(--fs-1); font-weight: 800; color: var(--cz-ink2); }
|
|
@@ -7,6 +7,12 @@
|
|
|
7
7
|
* aboutHtml(v, o, open) → the markup: the header press (data-iv-mc="ab") with its one-line summary, then the two columns when open
|
|
8
8
|
* resetAbout() forget the descriptions read (for the tests)
|
|
9
9
|
*
|
|
10
|
+
* The description (initiative-description-field): the record's own `description:` line (v.desc, from the board trail) wins. When the PRD's
|
|
11
|
+
* paragraph says more than it, the PRD's shows once below it, marked "From the PRD". With no line, the PRD's paragraph shows as before. With
|
|
12
|
+
* neither, "Ask Claude to draft it" opens Home chat with a prompt that names the id, so the kept text is saved to the record
|
|
13
|
+
* (`pmos initiative describe`). Claude also drafts it by itself under the ✦ (lib/description-props.js).
|
|
14
|
+
* saysMore(desc, prd) → true when the PRD's paragraph is fuller than the description and not in it yet. Pure.
|
|
15
|
+
*
|
|
10
16
|
* The success measure is the one Claude drafts (pm/metric-drafts.js, GET /api/metric-drafts): its proposal keeps its own card and presses; a
|
|
11
17
|
* kept measure is listed here as rows, each with its Now → Target. A row that also carries `base` (where it started) and `pace` (how far it
|
|
12
18
|
* should be by today, 0 to 1) draws as a track with a tick at the pace and a verdict: On pace, Ahead of pace, Behind pace, Short of target,
|
|
@@ -21,7 +27,7 @@ const STALE_DAYS = 30;
|
|
|
21
27
|
const MAX_CHARS = 700;
|
|
22
28
|
|
|
23
29
|
/* ── the description ── */
|
|
24
|
-
const plain = (s) => String(s).replace(/!\[[^\]]*\]\([^)]*\)/g, '').replace(/\[([^\]]+)\]\([^)]*\)/g, '$1').replace(/[
|
|
30
|
+
const plain = (s) => String(s).replace(/!\[[^\]]*\]\([^)]*\)/g, '').replace(/\[([^\]]+)\]\([^)]*\)/g, '$1').replace(/[*`]+/g, '').replace(/(^|\W)_+(?=\w)|(?<=\w)_+(?=\W|$)/g, '$1').replace(/<[^>]+>/g, '').replace(/\s+/g, ' ').trim();
|
|
25
31
|
/** The description a PRD gives. */
|
|
26
32
|
export function descOf(md) {
|
|
27
33
|
let t = String(md || '').replace(/\r\n?/g, '\n');
|
|
@@ -64,6 +70,9 @@ export function want(ws, path, onChange) {
|
|
|
64
70
|
return one;
|
|
65
71
|
}
|
|
66
72
|
const descFor = (ws, path) => READ.get((ws || '') + '|' + path) || null;
|
|
73
|
+
const norm = (s) => String(s || '').toLowerCase().replace(/[^a-z0-9]+/g, ' ').trim();
|
|
74
|
+
/** The PRD says more: not already in the description, and fuller (the same rule as lib/description-props.js). */
|
|
75
|
+
export function saysMore(desc, prd) { const d = norm(desc), q = norm(prd); return !!q && !d.includes(q.slice(0, 60)) && !q.includes(d) ? q.length > d.length + 40 : false; }
|
|
67
76
|
export const resetAbout = () => { READ.clear(); OPEN.clear(); LASTWS = ''; };
|
|
68
77
|
const OPEN = new Set(); // initiative ids whose description is open past three lines, for the session
|
|
69
78
|
|
|
@@ -96,7 +105,7 @@ const rowHtml = (r, o) => {
|
|
|
96
105
|
const homeHref = (ws, id, q) => { const u = new URLSearchParams(); if (ws) u.set('ws', ws); u.set('ask', id); u.set('q', q); return '/premium.html?' + u.toString(); };
|
|
97
106
|
/** The folded line: the first measure's dot and value, or the one next action. */
|
|
98
107
|
function summaryOf(v, M, d, o) {
|
|
99
|
-
const none = !d || d.state !== 'ok';
|
|
108
|
+
const none = !v.desc && (!d || d.state !== 'ok');
|
|
100
109
|
if (M && M.mode === 'kept') {
|
|
101
110
|
const r = M.rows[0], t = trackOf(r, o), st = t ? t.state : r.now == null ? 'unread' : '';
|
|
102
111
|
const val = r.now == null && !r.nowTxt ? 'not read yet' : nowText(r) + (r.targetTxt ? ' → ' + r.targetTxt : '');
|
|
@@ -108,18 +117,24 @@ function summaryOf(v, M, d, o) {
|
|
|
108
117
|
}
|
|
109
118
|
export function aboutHtml(v, o, open) {
|
|
110
119
|
const id = v.id || v.key || '', ws = o.ws || LASTWS;
|
|
111
|
-
const prd = v.links && v.links.prd ? v.links.prd : '';
|
|
120
|
+
const prd = v.links && v.links.prd ? v.links.prd : v.prd || ''; // the trail's PRD path (v.prd); v.links is the task links on the Board
|
|
112
121
|
const d = prd ? descFor(ws, prd) : null;
|
|
113
122
|
const M = MD.measuresOf(id);
|
|
114
123
|
v = { ...v, docState: !prd ? 'none' : d ? d.state : 'loading' };
|
|
115
124
|
const sum = summaryOf(v, M, d, o);
|
|
116
|
-
let desc;
|
|
117
|
-
|
|
118
|
-
const long =
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
125
|
+
let desc, fromPrd = false;
|
|
126
|
+
const para = (key, text, cls = '') => {
|
|
127
|
+
const long = text.length > 260, shut = long && !OPEN.has(key);
|
|
128
|
+
return `<p class="ab-p${cls}${shut ? ' cl' : ''}">${esc(text)}</p>${long ? `<button type="button" class="ab-lk" data-ab-more="${esc(key)}" aria-expanded="${!shut}">${shut ? 'Show more' : 'Show less'}</button>` : ''}`;
|
|
129
|
+
};
|
|
130
|
+
const openPrd = o.doc ? `<button type="button" class="ab-lk" data-iv-doc="${esc(prd)}">Open the PRD ${SVG(P.go, 14, 2.2)}</button>` : '';
|
|
131
|
+
const own = v.desc ? String(v.desc) : '';
|
|
132
|
+
if (own) {
|
|
133
|
+
// the record's own description wins; the PRD's paragraph shows once, below, only when it says more
|
|
134
|
+
desc = para(id, own) + (d && d.state === 'ok' && saysMore(own, d.text) ? `<h5 class="ab-from">From the PRD</h5>${para(id + '|prd', d.text)}${openPrd}` : '');
|
|
135
|
+
} else if (d && d.state === 'ok') { fromPrd = true; desc = para(id, d.text) + openPrd; }
|
|
136
|
+
else if (prd && (!d || d.state === 'loading')) desc = '<p class="ab-p q">Reading the PRD…</p>';
|
|
137
|
+
else desc = `<p class="ab-p q">No description yet.</p><a class="ab-lk" href="${esc(homeHref(ws, id, 'Draft the description of ' + (v.title || id) + ' (' + id + ') from what you can read, and save it to the initiative once I keep it: '))}">${SVG(P.chat, 14)} Ask Claude to draft it</a>`;
|
|
123
138
|
let met;
|
|
124
139
|
if (M && M.mode === 'open') met = MD.iniHtml(id, ws, v.title);
|
|
125
140
|
else if (M && M.mode === 'kept') met = `<div class="ab-ml">${M.rows.map((r) => rowHtml(r, o)).join('')}</div>`
|
|
@@ -127,7 +142,7 @@ export function aboutHtml(v, o, open) {
|
|
|
127
142
|
else met = `<p class="ab-p q">No success measure yet.</p><a class="ab-lk" href="${esc(homeHref(ws, id, 'Propose a success measure for ' + (v.title || id) + ': '))}">${SVG(P.chat, 14)} Ask Claude to propose one</a>`;
|
|
128
143
|
const anchor = v.anchor ? `<span>Anchored to ${esc(v.anchor)}</span>` : '';
|
|
129
144
|
return `<section class="iv-ab" aria-label="About"><button type="button" class="iv-ab-h" data-iv-mc="ab" aria-expanded="${!!open}"><b>About</b>${sum}<span class="chev" aria-hidden="true">${SVG(P.down, 16, 2)}</span></button>`
|
|
130
|
-
+ (open ? `<div class="iv-ab-b"><div class="ab-d"><h4>Description${
|
|
145
|
+
+ (open ? `<div class="iv-ab-b"><div class="ab-d"><h4>Description${fromPrd ? ' <span>From the PRD</span>' : ''}</h4>${desc}</div><div class="ab-m"><h4>Success measure ${anchor}</h4>${met}</div></div>` : '') + '</section>';
|
|
131
146
|
}
|
|
132
147
|
/** The banner's one measure chip (R2): the first kept measure's dot and value, or a proposal's mark; '' when there is none. */
|
|
133
148
|
export function chipHtml(v, o = {}) {
|
|
@@ -36,7 +36,7 @@ const md = (iso) => { const m = DAY.exec(String(iso || '')); return m ? MON[+m[2
|
|
|
36
36
|
const span = (a, b) => (a && b && a !== b ? md(a) + ' – ' + md(b) : md(a || b));
|
|
37
37
|
const plural = (n, a, b) => n + ' ' + (n === 1 ? a : b);
|
|
38
38
|
const clip = (s, n) => { s = String(s || ''); return s.length > n ? s.slice(0, n - 1) + '…' : s; };
|
|
39
|
-
const GROUP = { outcome: 'Expected outcome', found: 'Found elsewhere', dates: 'Dates', status: 'Looks done', names: 'Names', links: 'Links', grill: 'Claude grilled it' };
|
|
39
|
+
const GROUP = { description: 'Description', outcome: 'Expected outcome', found: 'Found elsewhere', dates: 'Dates', status: 'Looks done', names: 'Names', links: 'Links', grill: 'Claude grilled it' };
|
|
40
40
|
|
|
41
41
|
/* ── the chip and the live look ── */
|
|
42
42
|
export function chipOf(p) {
|
|
@@ -94,10 +94,11 @@ const titleOf = (p) => {
|
|
|
94
94
|
if (p.group === 'found') return p.type === 'stage' ? 'Stage: ' + ((p.data && p.data.stage) || '') : p.type === 'docs' ? plural(n, 'document', 'documents') : p.type === 'tasks' ? plural(n, 'task', 'tasks') : p.type === 'link' ? 'Already in another initiative' : p.type === 'maybe' ? 'Maybe this initiative' : plural(n, 'thing', 'things') + ' not placed';
|
|
95
95
|
if (p.group === 'status') return plural(n, 'task looks', 'tasks look') + ' done';
|
|
96
96
|
if (p.group === 'names') return 'Tidy ' + plural(n, 'name', 'names');
|
|
97
|
+
if (p.group === 'description') return p.type === 'enrich' ? 'A fuller description from the PRD' : 'What this initiative is';
|
|
97
98
|
if (p.group === 'outcome') return p.type === 'date' ? 'A date to check the outcome' : 'What should change once it ships';
|
|
98
99
|
return '';
|
|
99
100
|
};
|
|
100
|
-
const iconOf = (p) => (p.group === 'outcome' ? I.goal : p.group === 'found' ? (p.type === 'stage' ? I.stage : p.type === 'docs' ? I.doc : p.type === 'tasks' ? I.tasks : p.type === 'link' ? I.link : I.maybe)
|
|
101
|
+
const iconOf = (p) => (p.group === 'description' ? I.doc : p.group === 'outcome' ? I.goal : p.group === 'found' ? (p.type === 'stage' ? I.stage : p.type === 'docs' ? I.doc : p.type === 'tasks' ? I.tasks : p.type === 'link' ? I.link : I.maybe)
|
|
101
102
|
: p.group === 'dates' ? I.cal : p.group === 'status' ? I.done : p.group === 'names' ? I.name : I.link);
|
|
102
103
|
const ib = (attr, label, icon, cls = '') => `<button type="button" class="pp-ib${cls ? ' ' + cls : ''}" ${attr} aria-label="${esc(label)}" title="${esc(label)}">${SVG(icon, 16, 2.2)}</button>`;
|
|
103
104
|
function itemHtml(p, x) {
|
|
@@ -110,8 +111,9 @@ function itemHtml(p, x) {
|
|
|
110
111
|
} else if (p.group === 'dates') { t = x.title || x.task; m = (x.from && x.from.due ? md(x.from.due) + ' → ' : '') + span(x.to && x.to.start, x.to && x.to.due) + (x.jira ? ' · Jira: a drafted comment' : '') + (x.ms ? ' · ◇ a promise' : ''); }
|
|
111
112
|
else if (p.group === 'status') { t = x.title || taskTitle(p.ini, x.task); m = x.jira ? 'Jira: a drafted comment' : ''; }
|
|
112
113
|
else if (p.group === 'outcome') { t = x.title; m = x.due ? 'Check on ' + md(x.due) : ''; }
|
|
114
|
+
else if (p.group === 'description') { t = (p.data && p.data.text) || x.title; m = p.type === 'enrich' && p.data && p.data.from ? 'Now: ' + clip(p.data.from, 120) : ''; }
|
|
113
115
|
else if (p.group === 'names') { t = x.to; m = 'Was: ' + (x.from || '') + (x.jira ? ' · Jira: a drafted comment' : ''); }
|
|
114
|
-
return `<div class="pp-it"><span class="w"><b>${esc(clip(t, 90))}</b>${m ? `<small>${esc(m)}</small>` : ''}</span>${x.sig ? ib(`data-pp-dropit="${esc(x.sig)}"`, 'Leave this one out', I.x) : ''}</div>`;
|
|
116
|
+
return `<div class="pp-it"><span class="w"><b>${esc(clip(t, p.group === 'description' ? 700 : 90))}</b>${m ? `<small>${esc(m)}</small>` : ''}</span>${x.sig ? ib(`data-pp-dropit="${esc(x.sig)}"`, 'Leave this one out', I.x) : ''}</div>`;
|
|
115
117
|
}
|
|
116
118
|
function linkHtml(p, l) {
|
|
117
119
|
const pick = S.choices[l.key] || l.rec;
|
|
@@ -322,7 +322,7 @@ export function iniView(r, ctx = {}) {
|
|
|
322
322
|
summary.wf = r.wf || { id: PF_ID, name: 'Product feature', icon: 'feature' };
|
|
323
323
|
summary.program = ctx.program || null;
|
|
324
324
|
const today2 = (n) => String(n).padStart(2, '0');
|
|
325
|
-
return { anchor: r.kr || '', vs: r.__vs || null, links: r.__vs && r.__vs.draft ? null : r.links || null, id: r.id || null, key: r.key, title: r.title, today: today.getFullYear() + '-' + today2(today.getMonth() + 1) + '-' + today2(today.getDate()), summary, stage, tasks, docs: ctx.docs != null ? ctx.docs : null, linked: linkedOf(r), jiraAt: jiraIn ? ctx.jiraAt || '' : '', jiraRead: ctx.jiraReadAt || '',
|
|
325
|
+
return { anchor: r.kr || '', vs: r.__vs || null, links: r.__vs && r.__vs.draft ? null : r.links || null, prd: (r.trail && r.trail.links && r.trail.links.prd) || null, desc: (r.trail && r.trail.desc) || r.desc || null, id: r.id || null, key: r.key, title: r.title, today: today.getFullYear() + '-' + today2(today.getMonth() + 1) + '-' + today2(today.getDate()), summary, stage, tasks, docs: ctx.docs != null ? ctx.docs : null, linked: linkedOf(r), jiraAt: jiraIn ? ctx.jiraAt || '' : '', jiraRead: ctx.jiraReadAt || '',
|
|
326
326
|
// split or merge (batch 11 item 14): Claude's suggestion for it, and where its work went when it was merged
|
|
327
327
|
suggest: r.suggest || null, merged: mergedOf(r),
|
|
328
328
|
// what Claude proposes under the one blue ✦, and what a ✓ kept from elsewhere (batch 12 items 2, 3 and 4; pm/ini-props.js)
|
|
@@ -1041,6 +1041,7 @@ button.iv-mt.iv-wf { padding: 0 !important; margin: 0 !important; }
|
|
|
1041
1041
|
@container iv (min-width: 760px) { .iv-ab-b { grid-template-columns: minmax(0, 1fr) minmax(0, 460px); gap: 32px; } }
|
|
1042
1042
|
.ab-d h4, .ab-m h4 { margin: 0 0 6px; font-size: var(--fs-label); font-weight: var(--fw-strong); color: var(--cz-ink3); letter-spacing: .02em; display: flex; gap: 8px; align-items: baseline; flex-wrap: wrap; line-height: var(--lh-label); }
|
|
1043
1043
|
.ab-d h4 span, .ab-m h4 span { font-weight: var(--fw-strong); }
|
|
1044
|
+
.ab-d h5.ab-from { margin: 12px 0 4px; font-size: var(--fs-label); font-weight: var(--fw-strong); color: var(--cz-ink3); line-height: var(--lh-label); }
|
|
1044
1045
|
.ab-p { margin: 0; font-size: var(--fs-body); line-height: 1.55; max-width: 62ch; color: var(--cz-ink); }
|
|
1045
1046
|
.ab-p.q { color: var(--cz-ink3); }
|
|
1046
1047
|
.ab-p.cl { display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; }
|
|
@@ -415,7 +415,7 @@ export function wireView(body, h) {
|
|
|
415
415
|
const drafts = (id) => { const H = body.__iv || {}; if (id && VIEWED.has(id)) MD.want(((H.where && H.where()) || {}).ws || '', 'initiative', id, () => { if (idOf() === id && H.redraw) H.redraw(); }); };
|
|
416
416
|
// the description: the PRD, read once per path through /api/doc; the view draws again when it arrives
|
|
417
417
|
// About draws alone when its description arrives: a full redraw would close a task row the PM has open
|
|
418
|
-
const about = (id) => { const H = body.__iv || {}, v = id && VIEWED.get(id), prd = v && v.links && v.links.prd;
|
|
418
|
+
const about = (id) => { const H = body.__iv || {}, v = id && VIEWED.get(id), prd = v && ((v.links && v.links.prd) || v.prd);
|
|
419
419
|
if (prd) AB.want(((H.where && H.where()) || {}).ws || '', prd, () => { const at = idOf() === id && body.querySelector('.iv-ab'); if (!at) return; const M = MD.measuresOf(id); at.outerHTML = AB.aboutHtml(v, OPTS.get(id) || {}, mcState(id, { proposal: !!(M && M.mode === 'open') }).ab); }); };
|
|
420
420
|
chip();
|
|
421
421
|
G.attach(body, body.__iv); // the layout and the drafted tickets of the initiative shown (batch 11 item 20)
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
* After a reply ends: up to two cards above the box, the one Claude recommends lit (white, blue ring, blue ✦), the other grey. A tap puts its
|
|
4
4
|
* text in the box; nothing sends, and the PM sends with the box's own ↑. No ↵, edit or dismiss icon. No card when there is no clear next step.
|
|
5
5
|
* The cards hide when the PM types or a new reply starts; they have no timeout.
|
|
6
|
-
* While the PM types a rough prompt (3 or more words), after a
|
|
6
|
+
* While the PM types a rough prompt (3 or more words), after a 2 s pause: one quiet line (a small turning mark, "Sharpening…") while Claude
|
|
7
|
+
* sharpens it, then one lit Sharper card. Typing is never held up; an answer for words no longer in the box, or one later than 30 s, is dropped.
|
|
7
8
|
* Its sliders icon opens the style menu: Sharpen as Polish, Prompt engineer (the default) or Your own, and the Sharpen as I type switch.
|
|
8
9
|
* After a tap on the Sharper card, ↶ in the box puts back what the PM wrote (NpB 5).
|
|
9
10
|
* Server: GET /api/next-prompt/prefs, POST /api/next-prompt/next and /sharpen (lib/routes-next-prompt.js, through lib/ai-guard.js); the style
|
|
@@ -15,7 +16,7 @@ const STAR = '<svg aria-hidden="true" width="15" height="15" viewBox="0 0 24 24"
|
|
|
15
16
|
const SLIDERS = 'M4 7h9M17 7h3M4 17h3M11 17h9M13 4v6M7 14v6', CHECK = 'M5 12l5 5L20 7', UNDO = 'M9 14L4 9l5-5M4 9h11a5 5 0 0 1 0 10h-3', SPIN = 'M12 3a9 9 0 1 1-9 9';
|
|
16
17
|
const ic = (d, w = 16) => `<svg aria-hidden="true" width="${w}" height="${w}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="${d}"/></svg>`;
|
|
17
18
|
const STYLES = [['polish', 'Polish', 'Clearer and shorter, your meaning kept'], ['engineer', 'Prompt engineer', 'Builds the best prompt for the job'], ['own', 'Your own', 'Your wording rules, written once in Settings']];
|
|
18
|
-
const PAUSE =
|
|
19
|
+
const PAUSE = 2000, WAIT = 30000;
|
|
19
20
|
export const roughText = (t) => { const s = String(t || '').trim(); return !s.startsWith('/') && s.length >= 12 && s.split(/\s+/).filter(Boolean).length >= 3; };
|
|
20
21
|
|
|
21
22
|
export function nextPromptUi({ el, host, box, redrawFoot }) {
|
|
@@ -36,7 +37,7 @@ export function nextPromptUi({ el, host, box, redrawFoot }) {
|
|
|
36
37
|
if (N.mode === 'cards' && N.cards.length) {
|
|
37
38
|
html = `<div class="np-list" role="group" aria-label="Suggested next prompts">${N.cards.map((c, i) => `<button type="button" class="np-card${c.rec ? ' rec' : ''}" data-np-use="${i}" aria-label="Use this prompt: ${esc(c.text)}"${c.rec ? ' title="Claude recommends this one"' : ''}>${STAR}<span class="np-t">${esc(c.text)}</span></button>`).join('')}</div>`;
|
|
38
39
|
} else if (N.mode === 'spin') {
|
|
39
|
-
html = `<div class="np-spin" role="status"
|
|
40
|
+
html = `<div class="np-spin" role="status"><span class="np-sp">${ic(SPIN, 14)}</span><span>Sharpening…</span></div>`;
|
|
40
41
|
} else if (N.mode === 'sharp' && N.sharp) {
|
|
41
42
|
const P = N.prefs || {}, st = P.style || 'engineer';
|
|
42
43
|
const menu = N.menu ? `<div class="np-menu" role="menu" aria-label="Style of sharper prompts" data-esc-layer><p class="np-mh">Sharpen as</p>${STYLES.map(([k, l, d]) => `<button type="button" role="menuitemradio" aria-checked="${st === k}" class="np-mi" data-np-voice="${k}"><span class="np-ck">${st === k ? ic(CHECK) : ''}</span><span><b>${esc(l)}</b><em>${esc(k === 'own' && !P.rules ? 'Write your rules in Settings first' : d)}</em></span></button>`).join('')}`
|
|
@@ -58,7 +59,9 @@ export function nextPromptUi({ el, host, box, redrawFoot }) {
|
|
|
58
59
|
const my = ++N.seq;
|
|
59
60
|
N.mode = 'spin'; N.sharp = null; draw();
|
|
60
61
|
const c = N.ctx || {};
|
|
62
|
+
const late = setTimeout(() => { if (my === N.seq && N.mode === 'spin') { N.seq++; N.mode = null; draw(); } }, WAIT); // no answer in time: the line goes quietly
|
|
61
63
|
const r = await host.pmPost('/api/next-prompt/sharpen', { key: c.key || '', text: value, reply: c.reply || '', initiative: c.initiative || '', title: c.title || '' }).catch(() => null);
|
|
64
|
+
clearTimeout(late);
|
|
62
65
|
const ta = box();
|
|
63
66
|
if (my !== N.seq || !ta || ta.value !== value) return; // the PM typed on, or sent: this answer is for words no longer in the box
|
|
64
67
|
if (r && r.ok && r.text) { N.mode = 'sharp'; N.sharp = r.text; } else { N.mode = null; }
|
package/workspace/server.js
CHANGED
|
@@ -3106,6 +3106,20 @@ const outcomeProps = require('./lib/outcome-props.js').create({
|
|
|
3106
3106
|
every: TICK_MS,
|
|
3107
3107
|
mayRun: () => { try { return scheduleOwner.mayRun(); } catch { return true; } },
|
|
3108
3108
|
warn: (what, e) => warnWriteFail(what, e) });
|
|
3109
|
+
// initiative-description-field: once a day Claude drafts the description of each initiative that has none (or a fuller one once its PRD says
|
|
3110
|
+
// more) under the initiative's ✦, from the PRD's or the record's own words, cited (lib/description-props.js). No session and no model; through
|
|
3111
|
+
// the shared guard. A ✓ of the PM writes the record. Off with PMOS_DESCRIPTIONS=off or pmos ai off descriptions.
|
|
3112
|
+
const descriptionProps = require('./lib/description-props.js').create({
|
|
3113
|
+
workspaces: () => [...workspaces.values()],
|
|
3114
|
+
isLead: (w) => isPlanNeedsTree(w),
|
|
3115
|
+
stateDirFor: (w) => stateDirFor(w),
|
|
3116
|
+
boardOf: (w, id) => require('./lib/routes-boards.js').boardIndex(proposalCtx(), w).of(id),
|
|
3117
|
+
runtime: () => STATE,
|
|
3118
|
+
open: (now) => !proactiveLib.inQuietHours(now, proSettings().quiet),
|
|
3119
|
+
env: process.env,
|
|
3120
|
+
every: TICK_MS,
|
|
3121
|
+
mayRun: () => { try { return scheduleOwner.mayRun(); } catch { return true; } },
|
|
3122
|
+
warn: (what, e) => warnWriteFail(what, e) });
|
|
3109
3123
|
// batch 16 item 6 ("Claude runs the OKRs"): Claude reads each goal with its recipe (lib/metric-read-runs.js) when its cadence comes round, files the
|
|
3110
3124
|
// value with its source and time, says honestly what it could not read and drafts the fix. A broken limit opens one alert: a bell line only when
|
|
3111
3125
|
// the goal's notify is push. At most 2 runs at once, one per product. PMOS_METRIC_READ=off.
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_comment": "initiative-description-field: every initiative has its own description. The record writer, the cited drafts, the guard, the daily filer under the ✦ and pmos initiative describe (lib/initiative-writer.js setDescription, lib/description-props.js, lib/proposals.js group description, lib/initiative-cli.js describe).",
|
|
3
|
+
"modules": ["descriptions.test.js"],
|
|
4
|
+
"seconds": { "descriptions.test.js": 3 }
|
|
5
|
+
}
|