claude-code-session-manager 0.40.3 → 0.41.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/{TiptapBody-DweEtHks.js → TiptapBody-Cr-O75Yg.js} +1 -1
- package/dist/assets/{index-BxVBtmjA.css → index-CKY3mHgV.css} +1 -1
- package/dist/assets/{index-QRU8uTeq.js → index-CV9TNjxX.js} +631 -624
- package/dist/index.html +2 -2
- package/package.json +3 -1
- package/plugins/session-manager-dev/skills/develop/SKILL.md +5 -5
- package/plugins/session-manager-dev/skills/develop/standards.md +1 -1
- package/plugins/session-manager-dev/skills/explain-to-me/SKILL.md +2 -2
- package/plugins/session-manager-dev/skills/find-opportunity/SKILL.md +1 -1
- package/plugins/session-manager-dev/skills/project-status/SKILL.md +20 -34
- package/plugins/session-manager-dev/skills/propose-epic/SKILL.md +68 -0
- package/scripts/lib/watchdogHelpers.cjs +5 -5
- package/scripts/mint-epic.cjs +28 -0
- package/scripts/propose-epic.cjs +55 -0
- package/src/main/__tests__/epicMint.test.cjs +85 -0
- package/src/main/__tests__/opsErrorLog.test.cjs +87 -0
- package/src/main/__tests__/prdCreate.test.cjs +124 -2
- package/src/main/__tests__/promptSessionEvents.test.cjs +56 -2
- package/src/main/__tests__/promptSessionTranscript.test.cjs +0 -0
- package/src/main/__tests__/rcaFeedbackHook.test.cjs +43 -211
- package/src/main/__tests__/scheduler-archived-twin-guard.test.cjs +63 -0
- package/src/main/__tests__/scheduler-notify-originating-tab-transcript.test.cjs +86 -0
- package/src/main/__tests__/scheduler-notify-originating-tab.test.cjs +21 -0
- package/src/main/__tests__/scheduler-writeprd-epic-rollback.test.cjs +78 -0
- package/src/main/browserView.cjs +1 -1
- package/src/main/chatRunner.cjs +24 -47
- package/src/main/config.cjs +12 -6
- package/src/main/docEdit.cjs +3 -4
- package/src/main/index.cjs +12 -0
- package/src/main/ipcSchemas.cjs +59 -11
- package/src/main/lib/__tests__/opsOwnership.test.cjs +92 -0
- package/src/main/lib/__tests__/projectBriefCore.test.cjs +74 -0
- package/src/main/lib/epicMint.cjs +133 -59
- package/src/main/lib/opsErrorLog.cjs +96 -0
- package/src/main/lib/opsOwnership.cjs +169 -0
- package/src/main/lib/prdCreate.cjs +39 -1
- package/src/main/lib/prdLocations.cjs +21 -0
- package/src/main/lib/projectBriefCore.cjs +70 -4
- package/src/main/lib/queueStore.cjs +3 -0
- package/src/main/lib/rcaFeedbackHook.cjs +41 -55
- package/src/main/logs.cjs +19 -1
- package/src/main/projectBrief.cjs +36 -2
- package/src/main/promptSessionEvents.cjs +24 -2
- package/src/main/promptSessionTranscript.cjs +0 -0
- package/src/main/pty.cjs +15 -0
- package/src/main/queueOps.cjs +1 -1
- package/src/main/scheduler/prdParser.cjs +8 -0
- package/src/main/scheduler.cjs +158 -62
- package/src/main/templates/PRD_AUTHORING.md +1 -1
- package/src/preload/api.d.ts +83 -4
- package/src/preload/index.cjs +26 -5
- package/plugins/session-manager-dev/skills/my-feedback/SKILL.md +0 -140
- package/plugins/session-manager-dev/skills/optimize-kpi/SKILL.md +0 -290
- package/plugins/session-manager-dev/skills/process-feedback/SKILL.md +0 -265
package/dist/index.html
CHANGED
|
@@ -7,10 +7,10 @@
|
|
|
7
7
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
8
8
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
9
9
|
<link href="https://fonts.googleapis.com/css2?family=Newsreader:ital,opsz,wght@0,6..72,400;0,6..72,500;0,6..72,600;0,6..72,700;1,6..72,400&family=Geist:wght@300;400;500;600;700&family=IBM+Plex+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
|
10
|
-
<script type="module" crossorigin src="./assets/index-
|
|
10
|
+
<script type="module" crossorigin src="./assets/index-CV9TNjxX.js"></script>
|
|
11
11
|
<link rel="modulepreload" crossorigin href="./assets/monaco-editor-BW5C4Iv1.js">
|
|
12
12
|
<link rel="stylesheet" crossorigin href="./assets/monaco-editor-BTnBOi8r.css">
|
|
13
|
-
<link rel="stylesheet" crossorigin href="./assets/index-
|
|
13
|
+
<link rel="stylesheet" crossorigin href="./assets/index-CKY3mHgV.css">
|
|
14
14
|
</head>
|
|
15
15
|
<body class="bg-bg text-fg font-sans antialiased">
|
|
16
16
|
<div id="root"></div>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-code-session-manager",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.41.1",
|
|
4
4
|
"description": "Local cockpit for the Claude Code CLI — multi-tab terminal, full config surface, scheduler, voice dictation, and live observability.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/main/index.cjs",
|
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
"plugins/",
|
|
14
14
|
"scripts/postinstall.cjs",
|
|
15
15
|
"scripts/lib/",
|
|
16
|
+
"scripts/propose-epic.cjs",
|
|
17
|
+
"scripts/mint-epic.cjs",
|
|
16
18
|
"src/main/",
|
|
17
19
|
"src/preload/",
|
|
18
20
|
"dist/index.html",
|
|
@@ -20,7 +20,7 @@ description: >-
|
|
|
20
20
|
**Role:** `/develop` owns the *pipeline*: it turns a development request into one or more
|
|
21
21
|
self-contained PRDs, queues them, and tracks them to completion. It is the convergence point
|
|
22
22
|
for both entry paths — an interactive human prompt comes straight here; an agent feedback file
|
|
23
|
-
arrives
|
|
23
|
+
arrives as an approved Epic proposal (`/propose-epic`), which the user approves and then calls this skill. Everything from
|
|
24
24
|
here on is identical regardless of who asked.
|
|
25
25
|
|
|
26
26
|
**Never** hand-implement the work inline in chat, and never restate rules that live elsewhere:
|
|
@@ -66,7 +66,7 @@ can't load skills.
|
|
|
66
66
|
1. **Clarify scope first.** If the prompt has genuine ambiguity (acceptance criteria, target
|
|
67
67
|
repo, framework, edge cases), ask 2–4 focused questions as plain text and wait. Don't use
|
|
68
68
|
the AskUserQuestion tool. Don't guess on decisions that would cost real rework. (When the
|
|
69
|
-
caller is
|
|
69
|
+
caller is an approved Epic proposal, scope is already established by its objective — don't
|
|
70
70
|
re-ask; build from the brief it hands you.)
|
|
71
71
|
|
|
72
72
|
2. **Explore the target repo — broadly, not just the obvious file.** Identify the absolute
|
|
@@ -186,7 +186,7 @@ can't load skills.
|
|
|
186
186
|
(`title`, `cwd`, `estimateMinutes`, `goal`, `acceptanceCriteria[]`, `implementationNotes`,
|
|
187
187
|
`outOfScope[]`) maps directly onto the sections below — pass them straight through. It
|
|
188
188
|
allocates a strictly-unique `NN` atomically (no read-then-write race against another
|
|
189
|
-
concurrent `/develop
|
|
189
|
+
concurrent `/develop` invocation, never reused across the project — PRD
|
|
190
190
|
832), derives and collision-checks the slug, and embeds the standards pointer for you.
|
|
191
191
|
`parallelGroup` is DEPRECATED and ignored — express ordering with the `dependsOn` input
|
|
192
192
|
(slugs that must complete first); independent PRDs simply omit it and may run in parallel.
|
|
@@ -334,7 +334,7 @@ can't load skills.
|
|
|
334
334
|
## Phase 2 — Track to completion (reusable tail)
|
|
335
335
|
|
|
336
336
|
The queued PRDs run headlessly and can take a while. Don't fire-and-forget, and don't block —
|
|
337
|
-
hand off to a recurring check.
|
|
337
|
+
hand off to a recurring check. An approved proposal delegates to this exact phase, so it is the
|
|
338
338
|
single definition of "tracked to done" for both entry paths.
|
|
339
339
|
|
|
340
340
|
7. **Watch the scheduler every ~30 min.** Start a 30-minute monitoring loop (`/loop 30m` over
|
|
@@ -348,7 +348,7 @@ single definition of "tracked to done" for both entry paths.
|
|
|
348
348
|
per `PRD_AUTHORING.md`). Don't silently retry forever. For `needs_review`, the scheduler
|
|
349
349
|
auto-files a Root Cause Analysis into the target project's feedback inbox
|
|
350
350
|
(`rcaFeedbackHook`, filename `<date>-rca-<slug>-<runId>.md`) — reference that file in
|
|
351
|
-
your report rather than re-deriving the analysis, and let
|
|
351
|
+
your report rather than re-deriving the analysis, and let the approving user fold its
|
|
352
352
|
prevention hint back into future PRD authoring. A `rateLimited` exit-1 is the
|
|
353
353
|
scheduler's benign auto-pause (auto-resumes next window) — keep waiting, don't escalate.
|
|
354
354
|
- **All PRDs completed successfully** — go to step 8.
|
|
@@ -82,7 +82,7 @@ Data-driven from 400+ scheduler runs: long hangs (not bad code) are the dominant
|
|
|
82
82
|
- **Verify before done.** Run the acceptance test command once before declaring success. If it's red, fix it or `exit 1` with the failure — never end the run on a failing test (that trips the verifier's `transcript_errors` downgrade).
|
|
83
83
|
- **Fail loud, fail fast.** On any step failure, print one diagnostic line and `exit 1`; don't swallow with `|| true` or spin in a silent retry. A `rateLimited` exit-1 is the scheduler's benign auto-pause (auto-resumes next window) — not a failure to engineer around.
|
|
84
84
|
- **Stay in the AC.** Do not add work past the acceptance checklist ("while we're here" generators/fixtures are the post-AC-overrun incident). Body must be clean UTF-8 — no NUL/control bytes.
|
|
85
|
-
- **You ARE the executor — never re-queue or self-schedule.** A headless PRD run must perform its own acceptance criteria directly. Do NOT invoke `/develop`, `/
|
|
85
|
+
- **You ARE the executor — never re-queue or self-schedule.** A headless PRD run must perform its own acceptance criteria directly. Do NOT invoke `/develop`, `/propose-epic`, or any queue-authoring skill from inside a run — those are interactive main-loop skills that author a *new* PRD and return, so the run exits 0 having done nothing (no commit, no sentinel → `needs_review` with `no_verdict_sentinel`). Do NOT call `ScheduleWakeup`/set a tracking loop either — the process exits when the run ends and nothing re-invokes it. This applies just as much to spawning your own review agents and waiting on them: do NOT invoke `/code-review`, `/security-review`, `requesting-code-review`, or any other skill/subagent as a background/async step and then end your turn with something like "I'll wait for the review agents to complete" — a headless run has no next turn, so that line is the run's last output, no verdict sentinel prints, and the job parks in `needs_review` even though the actual work already landed. If a PRD's acceptance criteria call for a second review pass, run it **synchronously, inline, before the finish protocol** — call the reviewer and read its result in the same turn, don't fire-and-wait. If the PRD's work looks large, decompose and execute it inline within this run; never delegate it back to the queue. (Incidents: PRD 460 invoked `/develop`, spawned a duplicate PRD 461, and exited 0 with no work. PRD 479 landed its commit correctly but then backgrounded `/code-review --fix` + `/security-review` and called `ScheduleWakeup` to "wait" for them — same class of failure, different entry point.)
|
|
86
86
|
- **A shared-repo `cwd` can be occupied by a concurrent job — check before you touch shared state.** When a PRD's `cwd` is a repo other headless runs may also target (a shared team repo like sigma, not a private single-purpose project), a `git checkout`/`gh pr checkout` can land you in another job's live worktree with its own uncommitted WIP. Before running `git stash`, `git reset`, or any command that discards or hides working-tree state, check `git stash list` and `git status` first, and if you must set aside pre-existing uncommitted changes that aren't yours, **stash with a descriptive message** (`git stash push -m "pre-existing WIP found by PRD <NN>, not mine"`) and **restore it before your run ends** (or, if you can't safely restore because your own commit depends on that worktree state, leave it stashed with the message and say so explicitly in your finish output — never let the run end silently dropping someone else's stash). Never `git stash drop`/`git clean -fd` on state you didn't create. (Incident: PRD 477 stashed a concurrent job's rAF-throttle-revert WIP to get its own checkout, finished, and exited without restoring it — orphaning the other job's uncommitted work in `stash@{0}` with no record of whose it was.)
|
|
87
87
|
- **`gh pr edit --body` can fail on repos with legacy GitHub Projects (classic) boards** — the underlying GraphQL query fetches `repository.pullRequest.projectCards`, a field GitHub is sunsetting, and errors with `GraphQL: Projects (classic) is being deprecated ... (repository.pullRequest.projectCards)` even though the edit itself would otherwise succeed. This is a known `gh` CLI quirk, not a defect in your work. Prefer `gh api -X PATCH repos/<owner>/<repo>/pulls/<n> -f body="$(cat body.md)"` for updating a PR description headlessly — it doesn't touch the deprecated field. If you do use `gh pr edit` and it fails this way, don't leave the bare GraphQL error as the last thing in that step (it reads as an unrecovered error in the final-20%-of-transcript verifier heuristic): immediately retry with the `gh api` form and print one line noting the known-bug fallback, so the recovery is adjacent to the error.
|
|
88
88
|
- **`gh pr checks`/`gh run watch` exit non-zero while CI is merely *pending*, not failed — don't let that surface as a bare error.** Polling `gh pr checks <n>` before checks finish returns a non-zero exit (e.g. 8) with output like `check pending 0 <url>` — this is normal, documented `gh` CLI behavior, not a failure. If you retry with a *differently-worded* command (e.g. dropping a `sleep N &&` prefix, or switching to `gh run watch <id> --exit-status`), the verifier's self-recovery detector pairs retries by exact command-description match and may not recognize the differently-worded retry as the same recovery, leaving the original pending-state error looking unrecovered in the transcript (incident: `745-pr188-ci-lint-docs-integrity`, a fully green, committed, pushed run flagged `needs_review` over exactly this). Prefer polling with the *same* command/description each time (e.g. loop `gh pr checks <n>` unchanged, or use `gh run watch <id> --exit-status` from the start rather than switching mid-poll) so a later success is recognized as recovering the earlier pending-state failure.
|
|
@@ -67,9 +67,9 @@ If you can't state it in the present tense without a date or a PRD, cut it.
|
|
|
67
67
|
- **The Skill Map** — `session-manager-operations/HUMAN_LEARN/SKILL_MAP.html`, a
|
|
68
68
|
**separate, dedicated** page (NOT a section of index.html) that visualizes the
|
|
69
69
|
*local-development skill chain*: the two intakes (interactive human prompt;
|
|
70
|
-
agent
|
|
70
|
+
agent proposals via `/propose-epic`, approved by the user) converging on `/develop`, which owns
|
|
71
71
|
PRD authoring + reads `standards.md`, queues onto the scheduler, and gates with
|
|
72
|
-
review/verify
|
|
72
|
+
review/verify. It is the "how I build on this
|
|
73
73
|
project" companion to index.html's "how this project works." index.html links
|
|
74
74
|
to it from the nav; it links back.
|
|
75
75
|
|
|
@@ -74,7 +74,7 @@ blocking PR number — never a silent omission.
|
|
|
74
74
|
|
|
75
75
|
- This skill only *reads* the tracker (issues/PRs) and git state — it makes no commits,
|
|
76
76
|
opens nothing, and files nothing. If the loop surfaces a genuine follow-up that can't be
|
|
77
|
-
picked now, note it in the report; do not auto-file a new issue or `/
|
|
77
|
+
picked now, note it in the report; do not auto-file a new issue or `/propose-epic` item
|
|
78
78
|
for it. Filing anything public/outward is a human decision, not this skill's default.
|
|
79
79
|
- Don't rank an item you couldn't verify is actually unclaimed and non-conflicting — when
|
|
80
80
|
in doubt, put it in the skip list with the uncertainty stated, rather than ranking a
|
|
@@ -134,51 +134,37 @@ A status check shouldn't dead-end at a report. Once the four dimensions are in,
|
|
|
134
134
|
self-contained *bootstrap → evaluate → improve* loop, usable even on a brand-new
|
|
135
135
|
project.
|
|
136
136
|
|
|
137
|
-
**First, work the
|
|
138
|
-
audit,
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
against `queue.json` and (a) flip any **🛠** row whose PRD now shows `completed`
|
|
150
|
-
to **✅** with the landing evidence, and (b) surface any `failed` / `needs_review`
|
|
151
|
-
/ stuck Burrow PRD behind a still-🛠 row as a finding to route. The file is
|
|
152
|
-
already in `processed/`; only the row's status is reconciled here. A `-local` may
|
|
153
|
-
supply the exact cross-reference command (grep 🛠 rows → look up their PRD id in
|
|
154
|
-
the queue); if it doesn't, do it inline. Never expect `/process-feedback` to have
|
|
155
|
-
delivery-gated the archive — that's the anti-pattern this split removes.
|
|
137
|
+
**First, work the pending proposals — decide before generating new work.** Right
|
|
138
|
+
after the audit, look at the project's **proposed Epics** (status `proposed` in
|
|
139
|
+
`session-manager-operations/prompt-sessions/active-index.json`): these are the
|
|
140
|
+
items agents filed for a human decision. Report them so the user can approve
|
|
141
|
+
or discard; do not
|
|
142
|
+
approve them yourself, and do not stack new levers on top of an undecided
|
|
143
|
+
backlog. (If there are none, say so and move on.)
|
|
144
|
+
|
|
145
|
+
There is no inbox to reconcile and no archive convention to keep in step: an
|
|
146
|
+
approved proposal becomes an active Epic whose PRDs and runs are already visible
|
|
147
|
+
in the Scheduler, so its status is read from the queue directly rather than
|
|
148
|
+
tracked in a parallel status log.
|
|
156
149
|
|
|
157
150
|
Then route **this audit's own findings**, by ownership:
|
|
158
151
|
|
|
159
152
|
1. **A finding that belongs to another project** (an upstream/downstream service
|
|
160
153
|
in the stack — e.g. the data source is stale, a contract drifted): do NOT
|
|
161
|
-
reach across the boundary. File it with **`/
|
|
162
|
-
|
|
163
|
-
|
|
154
|
+
reach across the boundary. File it with **`/propose-epic <that project's cwd>`**,
|
|
155
|
+
and note it in the report. (Same rule as everywhere: service boundaries
|
|
156
|
+
outrank convenience.)
|
|
164
157
|
2. **A finding that belongs to THIS project** (a real bug, a missing guard, an
|
|
165
158
|
enhancement the audit revealed — a cold sold-surface, a recurring error in the
|
|
166
159
|
logs, a usage gap): queue it as a scheduled PRD via **`/develop`** — never
|
|
167
160
|
implement it inline here. Record the queued PRD id in the report.
|
|
168
161
|
3. **Nothing actionable** — just report.
|
|
169
162
|
|
|
170
|
-
Then
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
green, trigger the optimizer. State explicitly in the report whether the gate
|
|
176
|
-
opened (green → optimize queued) or stayed shut (degraded/down → health findings
|
|
177
|
-
routed first).
|
|
178
|
-
|
|
179
|
-
This sequence — audit → route findings (my-feedback / develop) → optimize-when-green
|
|
180
|
-
— is enough to bootstrap, evaluate, and continuously improve a project from a
|
|
181
|
-
single `/project-status` invocation. The `-local` owns the *specifics* of each
|
|
163
|
+
Then stop. There is no automatic improvement gate: findings are routed as
|
|
164
|
+
proposals and PRDs above, and the user decides what gets worked on next.
|
|
165
|
+
|
|
166
|
+
This sequence — audit → route findings (propose-epic / develop) — is enough to
|
|
167
|
+
bootstrap and evaluate a project from a single `/project-status` invocation. The `-local` owns the *specifics* of each
|
|
182
168
|
step; this framework owns the *loop*.
|
|
183
169
|
|
|
184
170
|
## Model expectation (every local must mirror this)
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: propose-epic
|
|
3
|
+
description: >-
|
|
4
|
+
File work you think should happen as a PROPOSED Epic — an Epic that does not
|
|
5
|
+
start until a human presses Approve. Replaces the retired /process-feedback
|
|
6
|
+
and /my-feedback intake pipeline: there is no feedback folder, no triage
|
|
7
|
+
pass, and no archiving convention, because the proposal IS the work item.
|
|
8
|
+
Use whenever you would previously have written a feedback file — for this
|
|
9
|
+
project or another one — or whenever the user says "/propose-epic",
|
|
10
|
+
"propose this", "file this for later", "suggest we do X", "send feedback to
|
|
11
|
+
X". Keywords: propose, proposal, feedback, enhancement request, file for
|
|
12
|
+
approval, cross-project request, backlog.
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
# propose-epic
|
|
16
|
+
|
|
17
|
+
**Role:** the one way an agent asks for work to happen that it is not doing right now.
|
|
18
|
+
|
|
19
|
+
You do not implement, and you do not queue anything that runs. You file a **proposed
|
|
20
|
+
Epic** and stop. A human approves it in the Epics workspace, at which point its objective
|
|
21
|
+
becomes the first prompt of its own claude session — the same start path every hand-created
|
|
22
|
+
Epic takes.
|
|
23
|
+
|
|
24
|
+
## Why this replaced the feedback folder
|
|
25
|
+
|
|
26
|
+
The old flow was: write a markdown file into `session-manager-operations/feedback/` →
|
|
27
|
+
`/process-feedback` later reads it, evaluates it, dedupes it, queues PRDs via `/develop`,
|
|
28
|
+
archives the file to `processed/`, and updates a README with lessons. Four moving parts and
|
|
29
|
+
~400 lines of instructions existed to move an idea from "written down" to "being worked on".
|
|
30
|
+
|
|
31
|
+
An Epic already is that thing. It has a goal, an intent tag, its own session, its own PRD
|
|
32
|
+
directory, and a place in the UI. The only piece it was missing was "don't start yet" —
|
|
33
|
+
that is the `proposed` status. So the intake pipeline collapses into one command.
|
|
34
|
+
|
|
35
|
+
## How to file one
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
node "$SM_ROOT/scripts/propose-epic.cjs" <project-cwd> "<one-line title>" [feature|bug|discussion] <<'BODY'
|
|
39
|
+
<the full objective — this is sent verbatim as the first prompt on approval>
|
|
40
|
+
BODY
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
`$SM_ROOT` is session-manager's own package root. Resolve it once, in this order:
|
|
44
|
+
1. You are working inside the session-manager repo → `.` (use `scripts/propose-epic.cjs`).
|
|
45
|
+
2. Otherwise the installed package — this skill file lives at
|
|
46
|
+
`<SM_ROOT>/plugins/session-manager-dev/skills/propose-epic/SKILL.md`, so
|
|
47
|
+
`SM_ROOT` is four directories up from this file's own path.
|
|
48
|
+
|
|
49
|
+
- **`<project-cwd>`** — the project the work belongs to. Cross-project requests are just a
|
|
50
|
+
proposal filed into *that* project's cwd; there is no separate outbound skill.
|
|
51
|
+
- **title** — one line. It is what the Epics list shows. Keep it specific: `Scheduler rows
|
|
52
|
+
don't show their Epic`, not `Scheduler bug`.
|
|
53
|
+
- **body** — write it as an instruction to the agent that will pick it up, not as a report
|
|
54
|
+
to a human. Include what you observed, where in the code it lives (file:line if you know
|
|
55
|
+
it), and what done looks like. It is the first prompt, so it should be enough to act on.
|
|
56
|
+
- Re-proposing the same title into the same project **joins** the existing proposal and
|
|
57
|
+
enriches it rather than duplicating it. Say more in the body and re-run; that is the
|
|
58
|
+
update mechanism.
|
|
59
|
+
|
|
60
|
+
Report back the Epic id and the title you filed. Do not queue PRDs, do not edit code, and
|
|
61
|
+
do not ask whether you should file it — filing is the default action, and the approval gate
|
|
62
|
+
is the user's control, not your prompt.
|
|
63
|
+
|
|
64
|
+
## When NOT to propose
|
|
65
|
+
|
|
66
|
+
- The user asked you to do the work now → do it (or `/develop` it), don't propose it.
|
|
67
|
+
- It is a one-line fix you are already in the file for → just fix it.
|
|
68
|
+
- Nothing actionable → say so plainly and file nothing.
|
|
@@ -294,7 +294,7 @@ function hasOpenFeedback(cwd) {
|
|
|
294
294
|
*
|
|
295
295
|
* Complexity: O(P) over PRD files in prdsDir + O(J) over queue jobs.
|
|
296
296
|
*/
|
|
297
|
-
function emitFeedbackPRD(cwd, {
|
|
297
|
+
async function emitFeedbackPRD(cwd, {
|
|
298
298
|
prdsDir,
|
|
299
299
|
queuePath,
|
|
300
300
|
skillPath,
|
|
@@ -313,7 +313,7 @@ function emitFeedbackPRD(cwd, {
|
|
|
313
313
|
// Every PRD belongs to an Epic (CLAUDE.md domain model). The sweep joins
|
|
314
314
|
// one standing "Inbound feedback" Epic per project (reuseByGoal) so
|
|
315
315
|
// recurring ticks chain into a single Epic rather than minting one each.
|
|
316
|
-
const epic = ensureEpic(cwd, {
|
|
316
|
+
const epic = await ensureEpic(cwd, {
|
|
317
317
|
goalText: 'Inbound feedback processing',
|
|
318
318
|
tag: 'discussion',
|
|
319
319
|
reuseByGoal: true,
|
|
@@ -474,7 +474,7 @@ function emitFeedbackPRD(cwd, {
|
|
|
474
474
|
if (epicId !== null) {
|
|
475
475
|
// Trace the dispatch on the Epic's event chain (prompt → prd_created →
|
|
476
476
|
// ...) so the Epic conversation shows the PRD it spawned.
|
|
477
|
-
try { appendPrdCreatedEvent(cwd, epicId, slug, `Feedback sweep emitted ${slug}`); } catch { /* trace is best-effort */ }
|
|
477
|
+
try { await appendPrdCreatedEvent(cwd, epicId, slug, `Feedback sweep emitted ${slug}`); } catch { /* trace is best-effort */ }
|
|
478
478
|
}
|
|
479
479
|
} catch (e) {
|
|
480
480
|
try { fs.unlinkSync(tmpPath); } catch { /* best-effort cleanup */ }
|
|
@@ -655,7 +655,7 @@ function maybeRelaunchApp({
|
|
|
655
655
|
* projectsDir — forwarded to activeProjectCwds for testing
|
|
656
656
|
* prdsDir, queuePath, skillPath, standardsPath — forwarded to emitFeedbackPRD
|
|
657
657
|
*/
|
|
658
|
-
function sweep({
|
|
658
|
+
async function sweep({
|
|
659
659
|
projectsDir,
|
|
660
660
|
prdsDir,
|
|
661
661
|
queuePath,
|
|
@@ -679,7 +679,7 @@ function sweep({
|
|
|
679
679
|
// dir); an explicit override (tests) is forwarded verbatim to every cwd.
|
|
680
680
|
let result;
|
|
681
681
|
try {
|
|
682
|
-
result = emitFeedbackPRD(cwd, { prdsDir, queuePath, skillPath, standardsPath });
|
|
682
|
+
result = await emitFeedbackPRD(cwd, { prdsDir, queuePath, skillPath, standardsPath });
|
|
683
683
|
} catch (e) {
|
|
684
684
|
process.stderr.write(`[sweep] error emitting PRD for ${cwd}: ${e?.message}\n`);
|
|
685
685
|
continue;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* mint-epic.cjs — CLI wrapper over lib/epicMint.cjs for PRD authors working
|
|
4
|
+
* without the app's admin API (the /develop skill's manual-write fallback).
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* node scripts/mint-epic.cjs <cwd> "<goal text>" [feature|bug|discussion]
|
|
8
|
+
*
|
|
9
|
+
* Prints the Epic's prds/ write directory on stdout (last line), minting the
|
|
10
|
+
* Epic (and registering it in prompt-sessions/active-index.json) if no active
|
|
11
|
+
* Epic with the same goal text exists — reuse-by-goal, so re-running for the
|
|
12
|
+
* same goal is idempotent and returns the same directory.
|
|
13
|
+
*/
|
|
14
|
+
'use strict';
|
|
15
|
+
|
|
16
|
+
const { ensureEpic } = require('../src/main/lib/epicMint.cjs');
|
|
17
|
+
|
|
18
|
+
const [cwd, goalText, tag] = process.argv.slice(2);
|
|
19
|
+
if (!cwd || !goalText) {
|
|
20
|
+
process.stderr.write('usage: mint-epic.cjs <cwd> "<goal text>" [feature|bug|discussion]\n');
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
(async () => {
|
|
25
|
+
const { epicId, prdDir, created } = await ensureEpic(cwd, { goalText, tag, reuseByGoal: true });
|
|
26
|
+
process.stderr.write(`${created ? 'minted' : 'joined'} epic ${epicId}\n`);
|
|
27
|
+
process.stdout.write(`${prdDir}\n`);
|
|
28
|
+
})();
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* propose-epic.cjs — file an Epic that does NOT start.
|
|
4
|
+
*
|
|
5
|
+
* This is the replacement for the feedback-folder intake. Instead of writing
|
|
6
|
+
* a markdown file into `session-manager-operations/feedback/` for a separate
|
|
7
|
+
* skill to later parse, triage, dedupe and re-queue, an agent files a
|
|
8
|
+
* `proposed` Epic: it appears in the Epics workspace with an Approve & start
|
|
9
|
+
* gate, and nothing runs or is spent until a human presses it.
|
|
10
|
+
*
|
|
11
|
+
* One mechanism replaces the whole intake pipeline — the proposal IS the
|
|
12
|
+
* work item, already in the place work is done, already carrying its own
|
|
13
|
+
* session and PRD directory.
|
|
14
|
+
*
|
|
15
|
+
* Usage:
|
|
16
|
+
* node scripts/propose-epic.cjs <cwd> "<one-line title>" [feature|bug|discussion]
|
|
17
|
+
* # full body (the first prompt sent on approval) is read from stdin;
|
|
18
|
+
* # when stdin is empty the title is used as the body.
|
|
19
|
+
*
|
|
20
|
+
* Prints the Epic id on stdout. Reuse-by-title: re-proposing the same title
|
|
21
|
+
* into the same project joins the existing proposal instead of duplicating it.
|
|
22
|
+
*/
|
|
23
|
+
'use strict';
|
|
24
|
+
|
|
25
|
+
const { ensureEpic } = require('../src/main/lib/epicMint.cjs');
|
|
26
|
+
|
|
27
|
+
const [cwd, title, tag] = process.argv.slice(2);
|
|
28
|
+
if (!cwd || !title) {
|
|
29
|
+
process.stderr.write('usage: propose-epic.cjs <cwd> "<title>" [feature|bug|discussion] (body on stdin)\n');
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function readStdin() {
|
|
34
|
+
return new Promise((resolve) => {
|
|
35
|
+
if (process.stdin.isTTY) { resolve(''); return; }
|
|
36
|
+
let buf = '';
|
|
37
|
+
process.stdin.setEncoding('utf8');
|
|
38
|
+
process.stdin.on('data', (d) => { buf += d; });
|
|
39
|
+
process.stdin.on('end', () => resolve(buf));
|
|
40
|
+
process.stdin.on('error', () => resolve(''));
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
(async () => {
|
|
45
|
+
const body = (await readStdin()).trim();
|
|
46
|
+
const { epicId, created } = await ensureEpic(cwd, {
|
|
47
|
+
goalText: title,
|
|
48
|
+
tag: tag || 'feature',
|
|
49
|
+
status: 'proposed',
|
|
50
|
+
reuseByGoal: true,
|
|
51
|
+
openingPrompt: body || title,
|
|
52
|
+
});
|
|
53
|
+
process.stderr.write(`${created ? 'proposed' : 'joined existing proposal'} epic ${epicId}\n`);
|
|
54
|
+
process.stdout.write(`${epicId}\n`);
|
|
55
|
+
})();
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* epicMint.test.cjs — unit tests for ensureEpic's sourcePromptId join
|
|
3
|
+
* semantics (see PRD authoring notes: sourcePromptId must be an existing
|
|
4
|
+
* Epic's promptSessionId, i.e. its active-index.json sessions key — NOT a
|
|
5
|
+
* PromptTicket.id).
|
|
6
|
+
*
|
|
7
|
+
* Run: timeout 120 npx vitest run src/main/__tests__/epicMint.test.cjs
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
'use strict';
|
|
11
|
+
|
|
12
|
+
import { test, expect, afterEach } from 'vitest';
|
|
13
|
+
const fs = require('node:fs');
|
|
14
|
+
const fsp = require('node:fs/promises');
|
|
15
|
+
const os = require('node:os');
|
|
16
|
+
const path = require('node:path');
|
|
17
|
+
const { ensureEpic, removeEpic, readActiveIndex } = require('../lib/epicMint.cjs');
|
|
18
|
+
|
|
19
|
+
const tmpDirs = [];
|
|
20
|
+
afterEach(async () => {
|
|
21
|
+
while (tmpDirs.length) {
|
|
22
|
+
const d = tmpDirs.pop();
|
|
23
|
+
await fsp.rm(d, { recursive: true, force: true });
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
async function mkCwd() {
|
|
28
|
+
const cwd = await fsp.mkdtemp(path.join(os.tmpdir(), 'sm-epicmint-cwd-'));
|
|
29
|
+
tmpDirs.push(cwd);
|
|
30
|
+
return cwd;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
test('ensureEpic mints a new Epic when explicit epicId matches no existing Epic (documents current behavior for a stray PromptTicket.id)', async () => {
|
|
34
|
+
const cwd = await mkCwd();
|
|
35
|
+
const notAPromptSessionId = 'ticket-abc123';
|
|
36
|
+
|
|
37
|
+
const result = await ensureEpic(cwd, { goalText: 'do the thing', epicId: notAPromptSessionId });
|
|
38
|
+
|
|
39
|
+
expect(result.created).toBe(true);
|
|
40
|
+
expect(result.epicId).not.toBe(notAPromptSessionId);
|
|
41
|
+
|
|
42
|
+
const index = readActiveIndex(cwd);
|
|
43
|
+
expect(index.sessions[notAPromptSessionId]).toBeUndefined();
|
|
44
|
+
expect(index.sessions[result.epicId]).toBeDefined();
|
|
45
|
+
expect(Object.keys(index.sessions)).toHaveLength(1);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test('ensureEpic joins the existing Epic when explicit epicId equals its promptSessionId', async () => {
|
|
49
|
+
const cwd = await mkCwd();
|
|
50
|
+
|
|
51
|
+
const first = await ensureEpic(cwd, { goalText: 'initial epic goal' });
|
|
52
|
+
expect(first.created).toBe(true);
|
|
53
|
+
|
|
54
|
+
const second = await ensureEpic(cwd, { goalText: 'a different PRD title', epicId: first.epicId });
|
|
55
|
+
|
|
56
|
+
expect(second.created).toBe(false);
|
|
57
|
+
expect(second.epicId).toBe(first.epicId);
|
|
58
|
+
expect(second.prdDir).toBe(first.prdDir);
|
|
59
|
+
|
|
60
|
+
const index = readActiveIndex(cwd);
|
|
61
|
+
expect(Object.keys(index.sessions)).toHaveLength(1);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('removeEpic deletes a minted Epic from both sessions and events maps', async () => {
|
|
65
|
+
const cwd = await mkCwd();
|
|
66
|
+
const minted = await ensureEpic(cwd, { goalText: 'to be rolled back' });
|
|
67
|
+
expect(readActiveIndex(cwd).sessions[minted.epicId]).toBeDefined();
|
|
68
|
+
|
|
69
|
+
const removed = removeEpic(cwd, minted.epicId);
|
|
70
|
+
|
|
71
|
+
expect(removed).toBe(true);
|
|
72
|
+
const index = readActiveIndex(cwd);
|
|
73
|
+
expect(index.sessions[minted.epicId]).toBeUndefined();
|
|
74
|
+
expect(index.events[minted.epicId]).toBeUndefined();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('removeEpic is a no-op (returns false) for an unknown epicId', async () => {
|
|
78
|
+
const cwd = await mkCwd();
|
|
79
|
+
await ensureEpic(cwd, { goalText: 'unrelated epic' });
|
|
80
|
+
|
|
81
|
+
const removed = removeEpic(cwd, 'nonexistent-epic-id');
|
|
82
|
+
|
|
83
|
+
expect(removed).toBe(false);
|
|
84
|
+
expect(Object.keys(readActiveIndex(cwd).sessions)).toHaveLength(1);
|
|
85
|
+
});
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* opsErrorLog.test.cjs — appendError() is the sole writer of the `logs` ops
|
|
3
|
+
* namespace (single-writer law, opsOwnership.cjs). Verifies: writes a tagged
|
|
4
|
+
* JSONL line to <cwd>/session-manager-operations/logs/errors-<date>.jsonl,
|
|
5
|
+
* redacts sensitive meta keys, and no-ops (never throws) with no cwd.
|
|
6
|
+
*
|
|
7
|
+
* Run: timeout 120 npx vitest run src/main/__tests__/opsErrorLog.test.cjs
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
'use strict';
|
|
11
|
+
|
|
12
|
+
import { test } from 'vitest';
|
|
13
|
+
import assert from 'node:assert/strict';
|
|
14
|
+
const fs = require('node:fs');
|
|
15
|
+
const os = require('node:os');
|
|
16
|
+
const path = require('node:path');
|
|
17
|
+
|
|
18
|
+
const opsErrorLog = require('../lib/opsErrorLog.cjs');
|
|
19
|
+
|
|
20
|
+
function mkTmpProject() {
|
|
21
|
+
return fs.mkdtempSync(path.join(os.tmpdir(), 'sm-opslog-test-'));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function readLines(cwd) {
|
|
25
|
+
const dir = opsErrorLog.logsDir(cwd);
|
|
26
|
+
const file = opsErrorLog.todayFile(cwd);
|
|
27
|
+
if (!fs.existsSync(file)) return [];
|
|
28
|
+
return fs.readFileSync(file, 'utf8').trim().split('\n').filter(Boolean).map((l) => JSON.parse(l));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
test('appendError writes a tagged JSONL line', () => {
|
|
32
|
+
const cwd = mkTmpProject();
|
|
33
|
+
opsErrorLog.appendError({
|
|
34
|
+
cwd,
|
|
35
|
+
scope: 'pty',
|
|
36
|
+
tabId: 'tab-123',
|
|
37
|
+
tags: ['spawn-failed'],
|
|
38
|
+
message: 'pty.spawn threw: boom',
|
|
39
|
+
});
|
|
40
|
+
const lines = readLines(cwd);
|
|
41
|
+
assert.equal(lines.length, 1);
|
|
42
|
+
const [line] = lines;
|
|
43
|
+
assert.equal(line.level, 'error');
|
|
44
|
+
assert.equal(line.scope, 'pty');
|
|
45
|
+
assert.equal(line.tabId, 'tab-123');
|
|
46
|
+
assert.equal(line.message, 'pty.spawn threw: boom');
|
|
47
|
+
assert.ok(line.tags.includes('level:error'));
|
|
48
|
+
assert.ok(line.tags.includes('scope:pty'));
|
|
49
|
+
assert.ok(line.tags.includes('tab:tab-123'));
|
|
50
|
+
assert.ok(line.tags.includes('spawn-failed'));
|
|
51
|
+
assert.ok(typeof line.ts === 'string' && !Number.isNaN(Date.parse(line.ts)));
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('appendError redacts sensitive meta keys', () => {
|
|
55
|
+
const cwd = mkTmpProject();
|
|
56
|
+
opsErrorLog.appendError({
|
|
57
|
+
cwd,
|
|
58
|
+
scope: 'chatRunner',
|
|
59
|
+
message: 'run failed',
|
|
60
|
+
meta: { token: 'sk-secret', sessionId: 'abc', password: 'hunter2' },
|
|
61
|
+
});
|
|
62
|
+
const [line] = readLines(cwd);
|
|
63
|
+
assert.equal(line.meta.token, '[redacted]');
|
|
64
|
+
assert.equal(line.meta.password, '[redacted]');
|
|
65
|
+
assert.equal(line.meta.sessionId, 'abc');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test('appendError is a no-op with no cwd', () => {
|
|
69
|
+
// Must not throw, and there is nowhere to assert a file was NOT written
|
|
70
|
+
// since no cwd means no path — this just proves it never throws.
|
|
71
|
+
assert.doesNotThrow(() => opsErrorLog.appendError({ scope: 'x', message: 'y' }));
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test('appendError merges caller tags without duplicating auto-derived ones', () => {
|
|
75
|
+
const cwd = mkTmpProject();
|
|
76
|
+
opsErrorLog.appendError({
|
|
77
|
+
cwd,
|
|
78
|
+
scope: 'chatRunner',
|
|
79
|
+
tabId: 'tab-9',
|
|
80
|
+
tags: ['level:error', 'silent-probe'], // deliberately duplicates an auto tag
|
|
81
|
+
message: 'probe failed',
|
|
82
|
+
});
|
|
83
|
+
const [line] = readLines(cwd);
|
|
84
|
+
const count = line.tags.filter((t) => t === 'level:error').length;
|
|
85
|
+
assert.equal(count, 1);
|
|
86
|
+
assert.ok(line.tags.includes('silent-probe'));
|
|
87
|
+
});
|