claude-code-session-manager 0.38.7 → 0.39.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-C74mBmMU.js → TiptapBody-CtuLATFR.js} +1 -1
- package/dist/assets/{index-BGTU12rl.css → index-B6JNrbpD.css} +1 -1
- package/dist/assets/{index-DPSmbyU_.js → index-CefVSjmm.js} +559 -559
- package/dist/index.html +2 -2
- package/package.json +1 -1
- package/plugins/session-manager-dev/skills/develop/SKILL.md +24 -17
- package/plugins/session-manager-dev/skills/develop/standards.md +1 -0
- package/src/main/__tests__/prdLocations.test.cjs +56 -0
- package/src/main/__tests__/prdMigration.test.cjs +126 -0
- package/src/main/__tests__/rcaFeedbackHook.test.cjs +8 -8
- package/src/main/config.cjs +9 -0
- package/src/main/health.cjs +26 -20
- package/src/main/ipcSchemas.cjs +3 -0
- package/src/main/lib/__tests__/importReferences.spec.cjs +56 -0
- package/src/main/lib/definitionOfDone.cjs +8 -9
- package/src/main/lib/importReferences.cjs +34 -0
- package/src/main/lib/prdCreate.cjs +3 -3
- package/src/main/lib/prdLocations.cjs +46 -0
- package/src/main/lib/prdMigration.cjs +83 -0
- package/src/main/lib/rcaFeedbackHook.cjs +5 -5
- package/src/main/queueOps.cjs +44 -15
- package/src/main/scheduler.cjs +191 -60
- package/src/main/templates/PRD_AUTHORING.md +2 -2
- package/src/preload/api.d.ts +16 -0
- package/src/preload/index.cjs +1 -0
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-CefVSjmm.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-B6JNrbpD.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.39.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",
|
|
@@ -77,8 +77,9 @@ can't load skills.
|
|
|
77
77
|
do they actually share the same shape, or only look similar — confirm by reading, don't
|
|
78
78
|
assume: a wrong assumption here means an inaccurate PRD, discovered only after the executor
|
|
79
79
|
runs it), check existing tests for the area, and check whether a prior PRD already touched
|
|
80
|
-
this subsystem (`ls
|
|
81
|
-
duplicating or contradicting a still-queued PRD is a real failure mode,
|
|
80
|
+
this subsystem (`ls <cwd>/session-manager-operations/scheduler/prds/` for related slugs, in
|
|
81
|
+
the target repo) — duplicating or contradicting a still-queued PRD is a real failure mode,
|
|
82
|
+
not a hypothetical one.
|
|
82
83
|
|
|
83
84
|
3. **Analyze the request through five named lenses, draft a candidate decomposition, then run
|
|
84
85
|
a completeness pass before finalizing it.** This step exists because small, bounded individual
|
|
@@ -193,17 +194,19 @@ can't load skills.
|
|
|
193
194
|
(the session-manager Electron app must be running for this MCP tool to work; if it isn't,
|
|
194
195
|
don't block on it): compute the highest in-use number deterministically yourself — never
|
|
195
196
|
eyeball or narrow-grep the `ls` (a narrowed pattern like `'^10[0-9]'` silently misses `110+`
|
|
196
|
-
and collides)
|
|
197
|
+
and collides). PRDs are stored per-project, so `NN` allocation for a given PRD only needs
|
|
198
|
+
*that project's own* prds directory scanned — not every project's:
|
|
197
199
|
```bash
|
|
198
|
-
ls
|
|
200
|
+
ls <cwd>/session-manager-operations/scheduler/prds/ | grep -oE '^[0-9]+' | sort -n | uniq | tail -5
|
|
199
201
|
```
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
202
|
+
(`<cwd>` is the target repo's absolute path — the same one this PRD's `cwd` field will use.)
|
|
203
|
+
The last line is the current max within that project. Then: same `NN` as a logically
|
|
204
|
+
independent sibling that can run in parallel; **next free `NN` = max+1** when this PRD
|
|
205
|
+
hard-depends on prior work or is unrelated to every existing group. This manual path has a
|
|
206
|
+
small, accepted race (two concurrent authors could compute the same "next free" `NN`) —
|
|
207
|
+
cosmetic (two unrelated groups end up sharing a number) rather than destructive, and only
|
|
208
|
+
reachable when the atomic tool path above isn't available. Record each cross-PRD dependency
|
|
209
|
+
in the dependent PRD's notes either way.
|
|
207
210
|
|
|
208
211
|
### PRD structure and location
|
|
209
212
|
|
|
@@ -215,15 +218,19 @@ can't load skills.
|
|
|
215
218
|
system that runs `claude -p <prd-body> --dangerously-skip-permissions` jobs around 5-hour
|
|
216
219
|
token-window resets, with auto-pause on rate-limit and auto-resume.
|
|
217
220
|
|
|
218
|
-
**Canonical location — non-negotiable.** PRDs MUST be written to:
|
|
221
|
+
**Canonical location — non-negotiable.** PRDs MUST be written to, inside the target repo:
|
|
219
222
|
```
|
|
220
|
-
|
|
223
|
+
<cwd>/session-manager-operations/scheduler/prds/<NN>-<kebab-slug>.md
|
|
221
224
|
```
|
|
222
225
|
**Anywhere else doesn't get scheduled.** If you write to `data/prds/`, `docs/prds/`, or the
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
`
|
|
226
|
+
old, now-retired global `prds/` dir under `~/.claude/session-manager/scheduled-plans/`,
|
|
227
|
+
the scheduler will not see it and the user loses their token-budget-managed execution. PRD
|
|
228
|
+
*source* files are per-project — each active project's own
|
|
229
|
+
`session-manager-operations/scheduler/prds/` directory, resolved at runtime from active
|
|
230
|
+
session cwds (`src/main/lib/prdLocations.cjs`) — but there is still exactly one **queue**
|
|
231
|
+
across all projects (`queue.json`/`history.jsonl`/`runs/` under
|
|
232
|
+
`~/.claude/session-manager/scheduled-plans/` remain global bookkeeping), because the 5-hour
|
|
233
|
+
token budget is global across all of the user's Claude work.
|
|
227
234
|
|
|
228
235
|
**Filename rules.** `NN` is the 2-digit zero-padded parallel group (picked per the `ls`
|
|
229
236
|
command above — same `NN` as an independent sibling, or next free `NN` = max+1 when this PRD
|
|
@@ -85,6 +85,7 @@ Data-driven from 400+ scheduler runs: long hangs (not bad code) are the dominant
|
|
|
85
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`, `/process-feedback`, 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
|
+
- **`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.
|
|
88
89
|
- **Negative-assertion checks must exit 0 when clean.** A check that verifies the *absence* of something (a `grep` that should find nothing, "no leftover X", `diff` expecting no change) must return exit 0 on the clean case. A bare `grep` exits **1 on no-match** — so the *success* path surfaces as `is_error=true` and the verifier downgrades a perfect run to `needs_review`. Always invert: `if <detector>; then echo "HALT: <what was found>"; exit 1; fi; echo clean`. Never let the no-match/empty path carry the non-zero exit.
|
|
89
90
|
- **Recover or annotate every error — don't strand a Traceback in the transcript.** The verifier downgrades an otherwise-perfect run to `needs_review` when a `Traceback`/`Error` appears with *no visible recovery within ~10 lines* (the `transcript_errors` heuristic — the single most common false-positive on green deliverables). Two executor habits cause it: (1) **throwaway probes that error** — an inline `python -c` with a quoting/f-string slip, a wrong kwarg, a bad path. When a probe errors, immediately re-run the corrected version *or* print one line `# expected/handled: <why>` right after, so recovery is adjacent. Don't move on leaving a bare error as the last thing in that step. Prefer a small temp `.py` file over a fragile multi-quote `python -c` one-liner (inline f-string errors are the top source of stranded tracebacks). (2) See the timeout rule below.
|
|
90
91
|
- **An *expected* bounded-timeout (exit 124) must be annotated, not bare.** `timeout`-capping a genuinely long task you expect to hit the cap (a full-universe ingest, a long scan) is correct — but a bare `Exit code 124` reads as a failure to the verifier. Wrap it so the cap is a success-with-note: `timeout 120 <cmd> || { rc=$?; [ $rc -eq 124 ] && echo "hit time cap — idempotent/partial, rows persist incrementally; OK" || { echo "HALT: <cmd> failed rc=$rc"; exit 1; }; }`. (Distinguish 124 = expected cap from a real non-zero.) For work that legitimately needs longer than a safe cap, run it in the background and poll a bounded number of times rather than capping the foreground command.
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* prdLocations.test.cjs — unit tests for prdLocations.cjs's per-project PRD
|
|
3
|
+
* directory resolution (PRD 808).
|
|
4
|
+
*
|
|
5
|
+
* Run: timeout 120 npx vitest run src/main/__tests__/prdLocations.test.cjs
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
'use strict';
|
|
9
|
+
|
|
10
|
+
import { test, expect, afterEach } from 'vitest';
|
|
11
|
+
const fs = require('node:fs');
|
|
12
|
+
const fsp = require('node:fs/promises');
|
|
13
|
+
const os = require('node:os');
|
|
14
|
+
const path = require('node:path');
|
|
15
|
+
const { resolvePrdWriteDir, resolvePrdsDirs } = require('../lib/prdLocations.cjs');
|
|
16
|
+
|
|
17
|
+
const tmpDirs = [];
|
|
18
|
+
afterEach(async () => {
|
|
19
|
+
while (tmpDirs.length) {
|
|
20
|
+
const d = tmpDirs.pop();
|
|
21
|
+
await fsp.rm(d, { recursive: true, force: true });
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test('resolvePrdWriteDir joins the project-scoped PRDs subpath onto cwd', () => {
|
|
26
|
+
expect(resolvePrdWriteDir('/home/user/Projects/foo')).toBe(
|
|
27
|
+
path.join('/home/user/Projects/foo', 'session-manager-operations', 'scheduler', 'prds'),
|
|
28
|
+
);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('resolvePrdWriteDir throws on a missing cwd', () => {
|
|
32
|
+
expect(() => resolvePrdWriteDir()).toThrow();
|
|
33
|
+
expect(() => resolvePrdWriteDir('')).toThrow();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('resolvePrdsDirs maps each active project cwd to its own PRDs dir', async () => {
|
|
37
|
+
const projectsDir = await fsp.mkdtemp(path.join(os.tmpdir(), 'sm-prdlocations-projects-'));
|
|
38
|
+
tmpDirs.push(projectsDir);
|
|
39
|
+
const projectCwd = await fsp.mkdtemp(path.join(os.tmpdir(), 'sm-prdlocations-cwd-'));
|
|
40
|
+
tmpDirs.push(projectCwd);
|
|
41
|
+
|
|
42
|
+
const slug = 'abc123';
|
|
43
|
+
const projDir = path.join(projectsDir, slug);
|
|
44
|
+
fs.mkdirSync(projDir, { recursive: true });
|
|
45
|
+
const transcript = path.join(projDir, 'session1.jsonl');
|
|
46
|
+
fs.writeFileSync(transcript, `${JSON.stringify({ cwd: projectCwd })}\n`);
|
|
47
|
+
|
|
48
|
+
const dirs = resolvePrdsDirs(90, { projectsDir });
|
|
49
|
+
expect(dirs).toEqual([resolvePrdWriteDir(projectCwd)]);
|
|
50
|
+
expect(dirs[0].endsWith(path.join('session-manager-operations', 'scheduler', 'prds'))).toBe(true);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('resolvePrdsDirs returns [] when no project has a recent transcript', () => {
|
|
54
|
+
const dirs = resolvePrdsDirs(90, { projectsDir: '/nonexistent-projects-dir' });
|
|
55
|
+
expect(dirs).toEqual([]);
|
|
56
|
+
});
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* prdMigration.test.cjs — unit tests for the one-time PRD-relocation
|
|
3
|
+
* migration (PRD 808): moves legacy-global PRD .md files into their own
|
|
4
|
+
* project's `<cwd>/session-manager-operations/scheduler/prds/`, based on
|
|
5
|
+
* each file's frontmatter `cwd`.
|
|
6
|
+
*
|
|
7
|
+
* Run: timeout 120 npx vitest run src/main/__tests__/prdMigration.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 { migratePrds } = require('../lib/prdMigration.cjs');
|
|
18
|
+
const { resolvePrdWriteDir } = require('../lib/prdLocations.cjs');
|
|
19
|
+
|
|
20
|
+
const tmpDirs = [];
|
|
21
|
+
afterEach(async () => {
|
|
22
|
+
while (tmpDirs.length) {
|
|
23
|
+
const d = tmpDirs.pop();
|
|
24
|
+
await fsp.rm(d, { recursive: true, force: true });
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
async function mkLegacyDir() {
|
|
29
|
+
const dir = await fsp.mkdtemp(path.join(os.tmpdir(), 'sm-prdmigration-legacy-'));
|
|
30
|
+
tmpDirs.push(dir);
|
|
31
|
+
return dir;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function mkProjectCwd() {
|
|
35
|
+
const dir = await fsp.mkdtemp(path.join(os.tmpdir(), 'sm-prdmigration-cwd-'));
|
|
36
|
+
tmpDirs.push(dir);
|
|
37
|
+
return dir;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
test('migratePrds moves a PRD whose frontmatter cwd resolves to an on-disk project', async () => {
|
|
41
|
+
const legacyDir = await mkLegacyDir();
|
|
42
|
+
const projectCwd = await mkProjectCwd();
|
|
43
|
+
const body = `---\ntitle: Do the thing\ncwd: ${projectCwd}\nestimateMinutes: 15\n---\n\n# Goal\n\nDo it.\n`;
|
|
44
|
+
await fsp.writeFile(path.join(legacyDir, '01-do-thing.md'), body, 'utf8');
|
|
45
|
+
|
|
46
|
+
const result = await migratePrds(legacyDir);
|
|
47
|
+
|
|
48
|
+
expect(result.moved).toBe(1);
|
|
49
|
+
expect(result.unresolved).toEqual([]);
|
|
50
|
+
expect(fs.existsSync(path.join(legacyDir, '01-do-thing.md'))).toBe(false);
|
|
51
|
+
const dest = path.join(resolvePrdWriteDir(projectCwd), '01-do-thing.md');
|
|
52
|
+
expect(fs.existsSync(dest)).toBe(true);
|
|
53
|
+
expect(await fsp.readFile(dest, 'utf8')).toBe(body);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('migratePrds is idempotent — a second run over an already-migrated dir is a no-op', async () => {
|
|
57
|
+
const legacyDir = await mkLegacyDir();
|
|
58
|
+
const projectCwd = await mkProjectCwd();
|
|
59
|
+
const body = `---\ntitle: Do the thing\ncwd: ${projectCwd}\nestimateMinutes: 15\n---\n\nBody.\n`;
|
|
60
|
+
await fsp.writeFile(path.join(legacyDir, '02-do-thing.md'), body, 'utf8');
|
|
61
|
+
|
|
62
|
+
await migratePrds(legacyDir);
|
|
63
|
+
const second = await migratePrds(legacyDir);
|
|
64
|
+
|
|
65
|
+
expect(second.moved).toBe(0);
|
|
66
|
+
expect(second.unresolved).toEqual([]);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('migratePrds leaves a file with no cwd in frontmatter in place and reports it unresolved', async () => {
|
|
70
|
+
const legacyDir = await mkLegacyDir();
|
|
71
|
+
const body = `---\ntitle: No cwd here\nestimateMinutes: 15\n---\n\nBody.\n`;
|
|
72
|
+
await fsp.writeFile(path.join(legacyDir, '03-no-cwd.md'), body, 'utf8');
|
|
73
|
+
|
|
74
|
+
const result = await migratePrds(legacyDir);
|
|
75
|
+
|
|
76
|
+
expect(result.moved).toBe(0);
|
|
77
|
+
expect(result.unresolved).toEqual([{ file: '03-no-cwd.md', reason: 'no cwd in frontmatter' }]);
|
|
78
|
+
expect(fs.existsSync(path.join(legacyDir, '03-no-cwd.md'))).toBe(true);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test('migratePrds leaves a file whose cwd does not exist on disk in place and reports it unresolved', async () => {
|
|
82
|
+
const legacyDir = await mkLegacyDir();
|
|
83
|
+
const body = `---\ntitle: Bad cwd\ncwd: /this/path/does/not/exist/anywhere\nestimateMinutes: 15\n---\n\nBody.\n`;
|
|
84
|
+
await fsp.writeFile(path.join(legacyDir, '04-bad-cwd.md'), body, 'utf8');
|
|
85
|
+
|
|
86
|
+
const result = await migratePrds(legacyDir);
|
|
87
|
+
|
|
88
|
+
expect(result.moved).toBe(0);
|
|
89
|
+
expect(result.unresolved).toHaveLength(1);
|
|
90
|
+
expect(result.unresolved[0].file).toBe('04-bad-cwd.md');
|
|
91
|
+
expect(fs.existsSync(path.join(legacyDir, '04-bad-cwd.md'))).toBe(true);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test('migratePrds ignores non-.md and dotfiles, and no-ops on a missing legacy dir', async () => {
|
|
95
|
+
const legacyDir = await mkLegacyDir();
|
|
96
|
+
await fsp.writeFile(path.join(legacyDir, 'notes.txt'), 'not a prd', 'utf8');
|
|
97
|
+
await fsp.writeFile(path.join(legacyDir, '.hidden.md'), 'ignored', 'utf8');
|
|
98
|
+
|
|
99
|
+
const result = await migratePrds(legacyDir);
|
|
100
|
+
expect(result).toEqual({ moved: 0, skipped: 0, unresolved: [] });
|
|
101
|
+
|
|
102
|
+
const missing = await migratePrds(path.join(legacyDir, 'nope'));
|
|
103
|
+
expect(missing).toEqual({ moved: 0, skipped: 0, unresolved: [] });
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test('migratePrds moves multiple PRDs to different projects independently', async () => {
|
|
107
|
+
const legacyDir = await mkLegacyDir();
|
|
108
|
+
const cwdA = await mkProjectCwd();
|
|
109
|
+
const cwdB = await mkProjectCwd();
|
|
110
|
+
await fsp.writeFile(
|
|
111
|
+
path.join(legacyDir, '05-a.md'),
|
|
112
|
+
`---\ntitle: A\ncwd: ${cwdA}\nestimateMinutes: 5\n---\n\nA.\n`,
|
|
113
|
+
'utf8',
|
|
114
|
+
);
|
|
115
|
+
await fsp.writeFile(
|
|
116
|
+
path.join(legacyDir, '06-b.md'),
|
|
117
|
+
`---\ntitle: B\ncwd: ${cwdB}\nestimateMinutes: 5\n---\n\nB.\n`,
|
|
118
|
+
'utf8',
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
const result = await migratePrds(legacyDir);
|
|
122
|
+
|
|
123
|
+
expect(result.moved).toBe(2);
|
|
124
|
+
expect(fs.existsSync(path.join(resolvePrdWriteDir(cwdA), '05-a.md'))).toBe(true);
|
|
125
|
+
expect(fs.existsSync(path.join(resolvePrdWriteDir(cwdB), '06-b.md'))).toBe(true);
|
|
126
|
+
});
|
|
@@ -16,7 +16,7 @@ let realHome;
|
|
|
16
16
|
let rcaFeedbackHook;
|
|
17
17
|
|
|
18
18
|
// config.cjs and rcaFeedbackHook.cjs both bake os.homedir() into top-level
|
|
19
|
-
// consts at first require (allowedRoots/WRITE_PREFIXES,
|
|
19
|
+
// consts at first require (allowedRoots/WRITE_PREFIXES, SM_REPO_ROOT).
|
|
20
20
|
// Node's require cache must be purged per test so each test's tmpHome takes
|
|
21
21
|
// effect — vi.resetModules() only resets vitest's ESM graph, not require().
|
|
22
22
|
const MODULES_TO_RELOAD = ['../lib/rcaFeedbackHook.cjs', '../config.cjs'];
|
|
@@ -65,8 +65,8 @@ function writeRun(runDir, slug, { logLines = ['line1', 'line2'], exitCode = 1, d
|
|
|
65
65
|
fs.writeFileSync(path.join(runDir, `${slug}.meta.json`), JSON.stringify({ exitCode, durationMs }));
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
function writePrd(slug, { acBody = '- [ ] `timeout 10 echo ok` passes.' } = {}) {
|
|
69
|
-
const prdsDir = path.join(
|
|
68
|
+
function writePrd(cwd, slug, { acBody = '- [ ] `timeout 10 echo ok` passes.' } = {}) {
|
|
69
|
+
const prdsDir = path.join(cwd, 'session-manager-operations', 'scheduler', 'prds');
|
|
70
70
|
fs.mkdirSync(prdsDir, { recursive: true });
|
|
71
71
|
const body = ['---', `title: ${slug}`, '---', '', '# Goal', '', 'Do the thing.', '', '# Acceptance criteria', '', acBody, '', '# Out of scope', '', '- N/A'].join('\n');
|
|
72
72
|
fs.writeFileSync(path.join(prdsDir, `${slug}.md`), body);
|
|
@@ -82,7 +82,7 @@ test('fileRcaFeedback: second call with same (slug, runId) is a no-op', async ()
|
|
|
82
82
|
const cwd = makeProjectWithInbox();
|
|
83
83
|
const runDir = path.join(tmpHome, 'run1');
|
|
84
84
|
writeRun(runDir, 'testslug');
|
|
85
|
-
writePrd('testslug');
|
|
85
|
+
writePrd(cwd, 'testslug');
|
|
86
86
|
const job = baseJob({ cwd });
|
|
87
87
|
|
|
88
88
|
const first = await rcaFeedbackHook.fileRcaFeedback({ job, runDir, verdict: 'transcript_errors' });
|
|
@@ -102,7 +102,7 @@ test('fileRcaFeedback: new runId for the same slug files a new RCA', async () =>
|
|
|
102
102
|
const runDir2 = path.join(tmpHome, 'run2');
|
|
103
103
|
writeRun(runDir1, 'testslug');
|
|
104
104
|
writeRun(runDir2, 'testslug');
|
|
105
|
-
writePrd('testslug');
|
|
105
|
+
writePrd(cwd, 'testslug');
|
|
106
106
|
|
|
107
107
|
const r1 = await rcaFeedbackHook.fileRcaFeedback({ job: baseJob({ cwd, runId: 'run-aaa' }), runDir: runDir1, verdict: 'transcript_errors' });
|
|
108
108
|
const r2 = await rcaFeedbackHook.fileRcaFeedback({ job: baseJob({ cwd, runId: 'run-bbb' }), runDir: runDir2, verdict: 'transcript_errors' });
|
|
@@ -195,7 +195,7 @@ test('fileRcaFeedback: actually writes into the fallback dir when the target inb
|
|
|
195
195
|
const cwd = makeProjectWithoutInbox();
|
|
196
196
|
const runDir = path.join(tmpHome, 'run1');
|
|
197
197
|
writeRun(runDir, 'testslug');
|
|
198
|
-
writePrd('testslug');
|
|
198
|
+
writePrd(cwd, 'testslug');
|
|
199
199
|
|
|
200
200
|
const res = await rcaFeedbackHook.fileRcaFeedback({ job: baseJob({ cwd }), runDir, verdict: 'transcript_errors' });
|
|
201
201
|
expect(res.filed).toBe(true);
|
|
@@ -221,7 +221,7 @@ test('fileRcaFeedback: SM_RCA_DISABLE=1 skips without writing', async () => {
|
|
|
221
221
|
const cwd = makeProjectWithInbox();
|
|
222
222
|
const runDir = path.join(tmpHome, 'run1');
|
|
223
223
|
writeRun(runDir, 'testslug');
|
|
224
|
-
writePrd('testslug');
|
|
224
|
+
writePrd(cwd, 'testslug');
|
|
225
225
|
|
|
226
226
|
const res = await rcaFeedbackHook.fileRcaFeedback({ job: baseJob({ cwd }), runDir, verdict: 'transcript_errors' });
|
|
227
227
|
expect(res.filed).toBe(false);
|
|
@@ -235,7 +235,7 @@ test('fileRcaFeedback: investigationText updates the existing RCA instead of dup
|
|
|
235
235
|
const cwd = makeProjectWithInbox();
|
|
236
236
|
const runDir = path.join(tmpHome, 'run1');
|
|
237
237
|
writeRun(runDir, 'testslug');
|
|
238
|
-
writePrd('testslug');
|
|
238
|
+
writePrd(cwd, 'testslug');
|
|
239
239
|
const job = baseJob({ cwd });
|
|
240
240
|
|
|
241
241
|
const first = await rcaFeedbackHook.fileRcaFeedback({ job, runDir, verdict: 'transcript_errors' });
|
package/src/main/config.cjs
CHANGED
|
@@ -23,6 +23,7 @@ const chokidar = require('chokidar');
|
|
|
23
23
|
const logs = require('./logs.cjs');
|
|
24
24
|
const { sendIfAlive } = require('./lib/sendToRenderer.cjs');
|
|
25
25
|
const { expandHome } = require('./lib/expandHome.cjs');
|
|
26
|
+
const { listReferencedFiles } = require('./lib/importReferences.cjs');
|
|
26
27
|
|
|
27
28
|
/** Map<absPath, {watcher, refCount}> — one chokidar watcher per path. */
|
|
28
29
|
const watchers = new Map();
|
|
@@ -409,6 +410,14 @@ function registerConfigHandlers() {
|
|
|
409
410
|
ipcMain.handle('config:write-text', v(s.configWriteText, ({ path: p, text }) => writeTextAtomic(p, text)));
|
|
410
411
|
ipcMain.handle('config:list-dir', v(s.configListDir, ({ path: p, opts }) => listDir(p, opts || {})));
|
|
411
412
|
ipcMain.handle('config:exists', v(s.configPath, ({ path: p }) => exists(p)));
|
|
413
|
+
ipcMain.handle('config:parse-imports', v(s.configParseImports, async ({ path: p }) => {
|
|
414
|
+
try {
|
|
415
|
+
const real = validatePath(expandHome(p));
|
|
416
|
+
return { ok: true, imports: listReferencedFiles(real) };
|
|
417
|
+
} catch (e) {
|
|
418
|
+
return { ok: false, error: e.message };
|
|
419
|
+
}
|
|
420
|
+
}));
|
|
412
421
|
ipcMain.on('config:watch', (e, { paths }) => { try { s.configWatch.parse(paths); watch(paths, e.sender.id); } catch (err) { logs.writeLine({ level: 'warn', scope: 'config', message: 'config:watch schema reject', meta: { error: err?.message } }); } });
|
|
413
422
|
ipcMain.on('config:unwatch', (e, { paths }) => { try { s.configWatch.parse(paths); unwatch(paths, e.sender.id); } catch (err) { logs.writeLine({ level: 'warn', scope: 'config', message: 'config:unwatch schema reject', meta: { error: err?.message } }); } });
|
|
414
423
|
}
|
package/src/main/health.cjs
CHANGED
|
@@ -11,6 +11,7 @@ const os = require('node:os');
|
|
|
11
11
|
const { execFileSync } = require('node:child_process');
|
|
12
12
|
const { POLL_INTERVAL_MS } = require('./lib/schedulerConfig.cjs');
|
|
13
13
|
const { checkPersonaImports } = require('./lib/personaImportHealth.cjs');
|
|
14
|
+
const { resolvePrdsDirs } = require('./lib/prdLocations.cjs');
|
|
14
15
|
|
|
15
16
|
const MAX_LOG_AGE_MS = 5 * 60_000; // 5 min — warn if no logs this old
|
|
16
17
|
const PROJECT_ROOT = path.resolve(__dirname, '../..');
|
|
@@ -237,28 +238,33 @@ async function check() {
|
|
|
237
238
|
};
|
|
238
239
|
}
|
|
239
240
|
|
|
240
|
-
// 4. Check PRDs
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
241
|
+
// 4. Check PRDs directories — one per active project
|
|
242
|
+
// (session-manager-operations/scheduler/prds/), summed. PRD 809.
|
|
243
|
+
const prdsDirs = resolvePrdsDirs();
|
|
244
|
+
let totalPrdCount = 0;
|
|
245
|
+
let anyPrdsDirAccessible = false;
|
|
246
|
+
const prdsDirErrors = [];
|
|
247
|
+
for (const dir of prdsDirs) {
|
|
248
|
+
try {
|
|
249
|
+
await fsp.access(dir, fs.constants.R_OK);
|
|
250
|
+
const files = await fsp.readdir(dir);
|
|
251
|
+
totalPrdCount += files.filter((f) => f.endsWith('.md')).length;
|
|
252
|
+
anyPrdsDirAccessible = true;
|
|
253
|
+
} catch (e) {
|
|
254
|
+
if (e.code !== 'ENOENT') {
|
|
255
|
+
prdsDirErrors.push(`${dir}: ${e.message}`);
|
|
256
|
+
}
|
|
254
257
|
}
|
|
255
|
-
status.components.scheduler_prds = {
|
|
256
|
-
ok: e.code === 'ENOENT', // ok if not yet created
|
|
257
|
-
path: prdsDir,
|
|
258
|
-
exists: false,
|
|
259
|
-
error: e.code === 'ENOENT' ? 'not yet created' : e.message,
|
|
260
|
-
};
|
|
261
258
|
}
|
|
259
|
+
if (prdsDirErrors.length > 0) {
|
|
260
|
+
status.issues.push(`PRDs directory not accessible: ${prdsDirErrors.join('; ')}`);
|
|
261
|
+
}
|
|
262
|
+
status.components.scheduler_prds = {
|
|
263
|
+
ok: prdsDirErrors.length === 0,
|
|
264
|
+
dirs: prdsDirs,
|
|
265
|
+
count: totalPrdCount,
|
|
266
|
+
...(prdsDirs.length === 0 || anyPrdsDirAccessible ? {} : { exists: false }),
|
|
267
|
+
};
|
|
262
268
|
|
|
263
269
|
// 5. Check transcripts directory (where live session logs are tailed).
|
|
264
270
|
const projectsDir = path.join(os.homedir(), '.claude/projects');
|
package/src/main/ipcSchemas.cjs
CHANGED
|
@@ -192,6 +192,8 @@ const configListDir = z.object({
|
|
|
192
192
|
|
|
193
193
|
const configWatch = z.array(z.string().min(1).max(4096));
|
|
194
194
|
|
|
195
|
+
const configParseImports = z.object({ path: z.string().min(1).max(4096) });
|
|
196
|
+
|
|
195
197
|
// ──────────────────────────────────────────── Sessions
|
|
196
198
|
const sessionsPayload = z.object({
|
|
197
199
|
tabs: z.array(z.object({
|
|
@@ -748,6 +750,7 @@ module.exports = {
|
|
|
748
750
|
configWriteText,
|
|
749
751
|
configListDir,
|
|
750
752
|
configWatch,
|
|
753
|
+
configParseImports,
|
|
751
754
|
sessionsPayload,
|
|
752
755
|
layoutEnvelope,
|
|
753
756
|
scheduleSlug,
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* importReferences.spec.cjs — unit tests for listReferencedFiles, the IPC-facing
|
|
3
|
+
* wrapper around personaImportHealth's `@path` import-chain walker.
|
|
4
|
+
*
|
|
5
|
+
* Run: timeout 120 npx vitest run src/main/lib/__tests__/importReferences.spec.cjs
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
'use strict';
|
|
9
|
+
|
|
10
|
+
import { test, expect } from 'vitest';
|
|
11
|
+
const fs = require('node:fs');
|
|
12
|
+
const os = require('node:os');
|
|
13
|
+
const path = require('node:path');
|
|
14
|
+
const { listReferencedFiles } = require('../importReferences.cjs');
|
|
15
|
+
|
|
16
|
+
function mkTmpDir() {
|
|
17
|
+
return fs.mkdtempSync(path.join(os.tmpdir(), 'import-refs-test-'));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
test('listReferencedFiles: 2 valid @imports returns 2 entries with correct sizeBytes/tokenEstimate/ok', () => {
|
|
21
|
+
const dir = mkTmpDir();
|
|
22
|
+
const a = path.join(dir, 'a.md');
|
|
23
|
+
const b = path.join(dir, 'b.md');
|
|
24
|
+
const root = path.join(dir, 'root.md');
|
|
25
|
+
fs.writeFileSync(a, 'x'.repeat(40));
|
|
26
|
+
fs.writeFileSync(b, 'y'.repeat(8));
|
|
27
|
+
fs.writeFileSync(root, `@${a}\n@${b}\n`);
|
|
28
|
+
|
|
29
|
+
const result = listReferencedFiles(root);
|
|
30
|
+
|
|
31
|
+
expect(result).toHaveLength(2);
|
|
32
|
+
const byPath = Object.fromEntries(result.map((r) => [r.path, r]));
|
|
33
|
+
expect(byPath[a]).toEqual({ path: a, exists: true, sizeBytes: 40, tokenEstimate: 10, ok: true });
|
|
34
|
+
expect(byPath[b]).toEqual({ path: b, exists: true, sizeBytes: 8, tokenEstimate: 2, ok: true });
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test('listReferencedFiles: missing import returns ok=false with sizeBytes 0', () => {
|
|
38
|
+
const dir = mkTmpDir();
|
|
39
|
+
const missing = path.join(dir, 'does-not-exist.md');
|
|
40
|
+
const root = path.join(dir, 'root.md');
|
|
41
|
+
fs.writeFileSync(root, `@${missing}\n`);
|
|
42
|
+
|
|
43
|
+
const result = listReferencedFiles(root);
|
|
44
|
+
|
|
45
|
+
expect(result).toEqual([
|
|
46
|
+
{ path: missing, exists: false, sizeBytes: 0, tokenEstimate: 0, ok: false },
|
|
47
|
+
]);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('listReferencedFiles: zero @import lines returns an empty array', () => {
|
|
51
|
+
const dir = mkTmpDir();
|
|
52
|
+
const root = path.join(dir, 'root.md');
|
|
53
|
+
fs.writeFileSync(root, 'no imports here, just prose.\n');
|
|
54
|
+
|
|
55
|
+
expect(listReferencedFiles(root)).toEqual([]);
|
|
56
|
+
});
|
|
@@ -13,6 +13,7 @@ const os = require('node:os');
|
|
|
13
13
|
const path = require('node:path');
|
|
14
14
|
const { spawn, spawnSync } = require('node:child_process');
|
|
15
15
|
const { splitFrontmatter } = require('./prdFrontmatter.cjs');
|
|
16
|
+
const { resolvePrdWriteDir } = require('./prdLocations.cjs');
|
|
16
17
|
|
|
17
18
|
// Regex identifying meta/dod slugs that must NOT influence the batchKey.
|
|
18
19
|
// This is the load-bearing loop-avoidance filter: when the gate job itself
|
|
@@ -96,11 +97,6 @@ function reportExists(key, runsDir = RUNS_DIR) {
|
|
|
96
97
|
// watchers.cjs and app:test-fire-hook only.
|
|
97
98
|
const SHELL_META_RE = /[|>&;<`]|\$[({]/;
|
|
98
99
|
|
|
99
|
-
const PRDS_DIR = path.join(
|
|
100
|
-
os.homedir(),
|
|
101
|
-
'.claude', 'session-manager', 'scheduled-plans', 'prds'
|
|
102
|
-
);
|
|
103
|
-
|
|
104
100
|
/**
|
|
105
101
|
* Extract the first bounded AC test command from a PRD body.
|
|
106
102
|
*
|
|
@@ -185,11 +181,12 @@ function extractAcCommand(prdBody) {
|
|
|
185
181
|
* @param {{ slug: string, cwd: string }} job
|
|
186
182
|
* @param {{ timeoutMs?: number, prdsDir?: string }} opts
|
|
187
183
|
* timeoutMs Hard kill ceiling for the child (default 60s).
|
|
188
|
-
* prdsDir Override PRD directory (for tests).
|
|
184
|
+
* prdsDir Override PRD directory (for tests). Defaults to the job's own
|
|
185
|
+
* per-project PRDs dir (resolvePrdWriteDir(job.cwd)).
|
|
189
186
|
* @returns {Promise<{ slug: string, status: 'pass'|'fail'|'unverifiable', code: number|null, ms: number }>}
|
|
190
187
|
*/
|
|
191
188
|
function reverifyAc(job, { timeoutMs = 60_000, prdsDir } = {}) {
|
|
192
|
-
const resolvedPrdsDir = prdsDir ??
|
|
189
|
+
const resolvedPrdsDir = prdsDir ?? (job.cwd ? resolvePrdWriteDir(job.cwd) : null);
|
|
193
190
|
const startNs = process.hrtime.bigint();
|
|
194
191
|
|
|
195
192
|
function elapsedMs() {
|
|
@@ -371,14 +368,16 @@ function _extractSection(body, headingText) {
|
|
|
371
368
|
*
|
|
372
369
|
* @param {Array<{ slug: string, cwd: string, landedCommit?: string }>} jobs
|
|
373
370
|
* @param {{ prdsDir?: string, gitTimeoutMs?: number }} opts
|
|
371
|
+
* prdsDir Override PRD directory for every job (for tests). Defaults per-job to
|
|
372
|
+
* the job's own per-project PRDs dir (resolvePrdWriteDir(job.cwd)).
|
|
374
373
|
* @returns {Array<{ slug: string, surfaces: string[] }>} Only jobs with ≥1 hit.
|
|
375
374
|
*/
|
|
376
375
|
function flagRiskySurfaces(jobs, { prdsDir, gitTimeoutMs = 10_000 } = {}) {
|
|
377
|
-
const resolvedPrdsDir = prdsDir ?? PRDS_DIR;
|
|
378
376
|
const results = [];
|
|
379
377
|
|
|
380
378
|
for (const job of jobs) {
|
|
381
379
|
let candidateText = '';
|
|
380
|
+
const resolvedPrdsDir = prdsDir ?? (job.cwd ? resolvePrdWriteDir(job.cwd) : null);
|
|
382
381
|
|
|
383
382
|
// 1. Try git commit — bounded by gitTimeoutMs.
|
|
384
383
|
if (job.landedCommit && job.cwd) {
|
|
@@ -395,7 +394,7 @@ function flagRiskySurfaces(jobs, { prdsDir, gitTimeoutMs = 10_000 } = {}) {
|
|
|
395
394
|
// 2. Fallback: PRD # Implementation notes section.
|
|
396
395
|
// Guard: slug must contain only safe chars to prevent path traversal via a
|
|
397
396
|
// corrupted queue.json entry (e.g. slug: "../../.ssh/id_rsa").
|
|
398
|
-
if (!candidateText && job.slug && /^[\w-]+$/.test(job.slug)) {
|
|
397
|
+
if (!candidateText && resolvedPrdsDir && job.slug && /^[\w-]+$/.test(job.slug)) {
|
|
399
398
|
try {
|
|
400
399
|
const raw = fs.readFileSync(path.join(resolvedPrdsDir, `${job.slug}.md`), 'utf8');
|
|
401
400
|
const body = splitFrontmatter(raw).body;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* importReferences.cjs — IPC-facing wrapper around personaImportHealth's
|
|
5
|
+
* import-chain walker, adding the size/token-estimate fields the "referenced
|
|
6
|
+
* files" panel needs. Reuses walkImports rather than reparsing `@path` syntax.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const fs = require('node:fs');
|
|
10
|
+
const { walkImports } = require('./personaImportHealth.cjs');
|
|
11
|
+
|
|
12
|
+
// Maps walkImports' result shape to what the renderer panel needs per file:
|
|
13
|
+
// resolved path, existence, size, a rough token estimate, and overall health.
|
|
14
|
+
function listReferencedFiles(rootPath) {
|
|
15
|
+
return walkImports(rootPath).map(({ importPath, exists, ok }) => {
|
|
16
|
+
let sizeBytes = 0;
|
|
17
|
+
if (exists) {
|
|
18
|
+
try {
|
|
19
|
+
sizeBytes = fs.statSync(importPath).size;
|
|
20
|
+
} catch {
|
|
21
|
+
sizeBytes = 0;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return {
|
|
25
|
+
path: importPath,
|
|
26
|
+
exists,
|
|
27
|
+
sizeBytes,
|
|
28
|
+
tokenEstimate: Math.round(sizeBytes / 4),
|
|
29
|
+
ok,
|
|
30
|
+
};
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
module.exports = { listReferencedFiles };
|
|
@@ -122,7 +122,7 @@ async function createPrd(input, remote) {
|
|
|
122
122
|
// NN allocation is delegated to allocateParallelGroup() (PRD 548) via
|
|
123
123
|
// the injected remote — never re-derived here — unless the caller
|
|
124
124
|
// opted into an existing group explicitly.
|
|
125
|
-
const nn = input.parallelGroup ?? await remote.allocateParallelGroup();
|
|
125
|
+
const nn = input.parallelGroup ?? await remote.allocateParallelGroup(input.cwd);
|
|
126
126
|
const filenameSlug = `${nn}-${slug}`;
|
|
127
127
|
|
|
128
128
|
// An explicit `parallelGroup` bypasses allocateParallelGroup()'s
|
|
@@ -131,13 +131,13 @@ async function createPrd(input, remote) {
|
|
|
131
131
|
// no existence guard (by design, it doubles as the edit-in-place
|
|
132
132
|
// path for Scheduler UI PRD edits), so "create" must not silently
|
|
133
133
|
// clobber an existing job's PRD.
|
|
134
|
-
const existing = await remote.readPrd(filenameSlug);
|
|
134
|
+
const existing = await remote.readPrd(filenameSlug, input.cwd);
|
|
135
135
|
if (existing?.ok) {
|
|
136
136
|
return { ok: false, status: 409, error: `PRD already exists: ${filenameSlug}.md` };
|
|
137
137
|
}
|
|
138
138
|
|
|
139
139
|
const body = buildPrdBody(input);
|
|
140
|
-
const writeResult = await remote.writePrd(filenameSlug, body);
|
|
140
|
+
const writeResult = await remote.writePrd(filenameSlug, body, input.cwd);
|
|
141
141
|
if (!writeResult?.ok) {
|
|
142
142
|
return { ok: false, status: 500, error: writeResult?.error ?? 'write failed' };
|
|
143
143
|
}
|