@webpresso/agent-kit 3.1.28 → 3.1.30
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/catalog/AGENTS.md.tpl +4 -4
- package/catalog/agent/rules/ci-cost-local-first.md +139 -0
- package/catalog/agent/rules/ci-test-perf.md +5 -1
- package/catalog/agent/rules/cmd-execution.md +9 -0
- package/catalog/agent/rules/pre-implementation.md +2 -1
- package/catalog/agent/rules/test-scan-perf.md +80 -0
- package/catalog/agent/rules/workflow-skills-routing.md +4 -2
- package/catalog/agent/skills/fix/SKILL.md +3 -0
- package/catalog/agent/skills/verify/SKILL.md +18 -0
- package/dist/esm/audit/blueprint-pr-coverage.js +68 -7
- package/dist/esm/audit/registry.d.ts +4 -4
- package/dist/esm/audit/registry.js +3 -0
- package/dist/esm/audit/roadmap-links.js +6 -0
- package/dist/esm/audit/test-scan-perf.d.ts +18 -0
- package/dist/esm/audit/test-scan-perf.js +275 -0
- package/dist/esm/blueprint/lifecycle/engine.d.ts +9 -0
- package/dist/esm/blueprint/lifecycle/engine.js +53 -6
- package/dist/esm/blueprint/lifecycle/review-provenance.d.ts +6 -0
- package/dist/esm/blueprint/lifecycle/review-provenance.js +93 -12
- package/dist/esm/blueprint/markdown/helpers.js +6 -2
- package/dist/esm/ci/act-replay.js +110 -7
- package/dist/esm/ci/act-runner.d.ts +4 -0
- package/dist/esm/ci/act-runner.js +85 -26
- package/dist/esm/ci/act-worktree-git.d.ts +11 -0
- package/dist/esm/ci/act-worktree-git.js +219 -0
- package/dist/esm/cli/commands/audit.js +3 -0
- package/dist/esm/cli/commands/blueprint/mutations.js +35 -18
- package/dist/esm/cli/commands/blueprint/router.js +27 -2
- package/dist/esm/cli/commands/ci.d.ts +8 -1
- package/dist/esm/cli/commands/ci.js +80 -21
- package/dist/esm/cli/commands/dash/keymap.js +1 -0
- package/dist/esm/cli/commands/dash/launch-preferences.d.ts +15 -0
- package/dist/esm/cli/commands/dash/launch-preferences.js +46 -17
- package/dist/esm/cli/commands/dash/plan-once-label.d.ts +7 -0
- package/dist/esm/cli/commands/dash/plan-once-label.js +5 -0
- package/dist/esm/cli/commands/dash/plan-once-membership.d.ts +67 -0
- package/dist/esm/cli/commands/dash/plan-once-membership.js +151 -0
- package/dist/esm/cli/commands/dash/plan-once-merge.d.ts +143 -0
- package/dist/esm/cli/commands/dash/plan-once-merge.js +238 -0
- package/dist/esm/cli/commands/dash/plan-once.d.ts +22 -2
- package/dist/esm/cli/commands/dash/plan-once.js +41 -4
- package/dist/esm/cli/commands/dash/tui/attention-hud.d.ts +9 -0
- package/dist/esm/cli/commands/dash/tui/attention-hud.js +9 -0
- package/dist/esm/cli/commands/dash/tui/chat-bridge.d.ts +41 -0
- package/dist/esm/cli/commands/dash/tui/chat-bridge.js +73 -0
- package/dist/esm/cli/commands/dash/tui/chat-mode.d.ts +47 -0
- package/dist/esm/cli/commands/dash/tui/chat-mode.js +98 -0
- package/dist/esm/cli/commands/dash/tui/frame.d.ts +3 -0
- package/dist/esm/cli/commands/dash/tui/frame.js +3 -1
- package/dist/esm/cli/commands/dash/tui/palette.d.ts +1 -1
- package/dist/esm/cli/commands/dash/tui/palette.js +14 -0
- package/dist/esm/cli/commands/dash/tui/runtime.js +239 -4
- package/dist/esm/cli/commands/dash/tui/state.d.ts +41 -0
- package/dist/esm/cli/commands/dash/tui/state.js +257 -4
- package/dist/esm/cli/commands/format.js +0 -2
- package/dist/esm/cli/commands/init/config.d.ts +0 -1
- package/dist/esm/cli/commands/init/config.js +1 -4
- package/dist/esm/cli/commands/init/scaffolders/agent-hooks/emitters/claude.d.ts +5 -6
- package/dist/esm/cli/commands/init/scaffolders/agent-hooks/emitters/claude.js +6 -13
- package/dist/esm/cli/commands/init/scaffolders/agent-hooks/ir.d.ts +0 -2
- package/dist/esm/cli/commands/init/scaffolders/agent-hooks/ir.js +0 -2
- package/dist/esm/cli/commands/init/scaffolders/agent-hooks/merge.d.ts +6 -3
- package/dist/esm/cli/commands/init/scaffolders/agent-hooks/merge.js +39 -26
- package/dist/esm/cli/commands/init/scaffolders/codex-mcp/index.js +61 -23
- package/dist/esm/cli/commands/logs.d.ts +15 -0
- package/dist/esm/cli/commands/logs.js +192 -3
- package/dist/esm/cli/commands/quality-log-store.d.ts +1 -0
- package/dist/esm/cli/commands/quality-log-store.js +187 -51
- package/dist/esm/cli/commands/review.js +5 -8
- package/dist/esm/cli/commands/secrets.d.ts +1 -0
- package/dist/esm/cli/commands/secrets.js +4 -1
- package/dist/esm/cli/direct-provider-launch.js +25 -1
- package/dist/esm/hooks/doctor.d.ts +15 -0
- package/dist/esm/hooks/doctor.js +56 -0
- package/dist/esm/hooks/pretool-guard/validators/forbidden-commands.js +4 -4
- package/dist/esm/hooks/pretool-guard/validators/mcp-redirect.d.ts +0 -1
- package/dist/esm/hooks/pretool-guard/validators/mcp-redirect.js +8 -5
- package/dist/esm/hooks/pretool-guard/validators/worktree-discipline.d.ts +9 -7
- package/dist/esm/hooks/pretool-guard/validators/worktree-discipline.js +126 -14
- package/dist/esm/mcp/tools/_shared/audit-kinds.d.ts +1 -1
- package/dist/esm/mcp/tools/audit.d.ts +2 -1
- package/dist/esm/mcp/tools/audit.js +10 -0
- package/dist/esm/mcp/tools/audits.d.ts +1 -0
- package/dist/esm/mcp/tools/ci-act.js +9 -11
- package/dist/esm/paths/state-root.js +3 -0
- package/dist/esm/review/authority.js +17 -2
- package/dist/esm/review/availability.js +18 -5
- package/dist/esm/review/subject.d.ts +15 -0
- package/dist/esm/review/subject.js +105 -10
- package/dist/esm/runtime/executor.d.ts +1 -0
- package/dist/esm/runtime/executor.js +2 -1
- package/dist/esm/secret-gate/runner.d.ts +1 -0
- package/dist/esm/secret-gate/runner.js +4 -1
- package/dist/esm/worktrees/current.d.ts +1 -0
- package/dist/esm/worktrees/current.js +1 -0
- package/dist/esm/worktrees/project-agent-surfaces.d.ts +1 -0
- package/dist/esm/worktrees/project-agent-surfaces.js +2 -1
- package/package.json +14 -14
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { randomUUID } from "node:crypto";
|
|
2
|
-
import {
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { createWriteStream, mkdirSync, openSync, readdirSync, readFileSync, statSync, renameSync, utimesSync, rmSync, writeFileSync, } from "node:fs";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
|
-
import { dirname, join } from "node:path";
|
|
4
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
5
5
|
import { getSurfacePath } from "#paths/state-root.js";
|
|
6
6
|
import { readTrustedJsonFile } from "#shared-utils/read-json-file.js";
|
|
7
7
|
import { shortId } from "#shared-utils/short-id.js";
|
|
@@ -16,6 +16,7 @@ export const CLI_LOG_COMMANDS = [
|
|
|
16
16
|
"format",
|
|
17
17
|
];
|
|
18
18
|
const ORPHAN_LOG_GRACE_MS = 60_000;
|
|
19
|
+
const INDEX_LOCK_STALE_MS = 5_000;
|
|
19
20
|
export function isCliLogCommandName(value) {
|
|
20
21
|
return CLI_LOG_COMMANDS.includes(value);
|
|
21
22
|
}
|
|
@@ -63,39 +64,39 @@ function openCliLogSink(commandDir, command, cwd) {
|
|
|
63
64
|
stream.write(chunk);
|
|
64
65
|
},
|
|
65
66
|
async finalize(metadata) {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
reject(streamError);
|
|
69
|
-
return;
|
|
70
|
-
}
|
|
71
|
-
rejectPendingFinalize = reject;
|
|
72
|
-
stream.end(() => {
|
|
73
|
-
rejectPendingFinalize = undefined;
|
|
67
|
+
try {
|
|
68
|
+
await new Promise((resolve, reject) => {
|
|
74
69
|
if (streamError) {
|
|
75
70
|
reject(streamError);
|
|
76
71
|
return;
|
|
77
72
|
}
|
|
78
|
-
|
|
73
|
+
rejectPendingFinalize = reject;
|
|
74
|
+
stream.end(() => {
|
|
75
|
+
rejectPendingFinalize = undefined;
|
|
76
|
+
if (streamError) {
|
|
77
|
+
reject(streamError);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
resolve();
|
|
81
|
+
});
|
|
79
82
|
});
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
};
|
|
90
|
-
try {
|
|
83
|
+
const entry = {
|
|
84
|
+
id,
|
|
85
|
+
command,
|
|
86
|
+
timestamp: metadata.timestamp ?? new Date().toISOString(),
|
|
87
|
+
exitCode: metadata.exitCode,
|
|
88
|
+
logPath: absoluteLogPath,
|
|
89
|
+
...(metadata.options ? { options: metadata.options } : {}),
|
|
90
|
+
...(metadata.summary ? { summary: metadata.summary } : {}),
|
|
91
|
+
};
|
|
91
92
|
markLogRecentlyFinalized(absoluteLogPath);
|
|
92
93
|
// Index/lock live next to the log file so fallback dirs stay consistent.
|
|
93
94
|
writeLogEntryInDir(entry, commandDir);
|
|
95
|
+
return entry;
|
|
94
96
|
}
|
|
95
97
|
finally {
|
|
96
98
|
rmSync(activeMarkerPath, { force: true });
|
|
97
99
|
}
|
|
98
|
-
return entry;
|
|
99
100
|
},
|
|
100
101
|
};
|
|
101
102
|
}
|
|
@@ -113,7 +114,7 @@ export function createCliLogSink(command, cwd = process.cwd()) {
|
|
|
113
114
|
catch (error) {
|
|
114
115
|
if (!isUnwritableFsError(error))
|
|
115
116
|
throw error;
|
|
116
|
-
const fallbackDir =
|
|
117
|
+
const fallbackDir = getFallbackCommandLogDir(command, cwd);
|
|
117
118
|
const code = typeof error === "object" && error !== null && "code" in error
|
|
118
119
|
? String(error.code)
|
|
119
120
|
: "unknown";
|
|
@@ -122,13 +123,37 @@ export function createCliLogSink(command, cwd = process.cwd()) {
|
|
|
122
123
|
}
|
|
123
124
|
}
|
|
124
125
|
export function readCliLogEntries(command, cwd = process.cwd()) {
|
|
125
|
-
const
|
|
126
|
+
const entries = [
|
|
127
|
+
...readCliLogEntriesFromCommandDir(command, getCommandLogDir(command, cwd)),
|
|
128
|
+
...readCliLogEntriesFromCommandDir(command, getFallbackCommandLogDir(command, cwd)),
|
|
129
|
+
];
|
|
130
|
+
const byLogPath = new Map();
|
|
131
|
+
for (const entry of entries) {
|
|
132
|
+
byLogPath.set(entry.logPath, entry);
|
|
133
|
+
}
|
|
134
|
+
return [...byLogPath.values()]
|
|
135
|
+
.sort((a, b) => {
|
|
136
|
+
if (a.active === true && b.active !== true)
|
|
137
|
+
return -1;
|
|
138
|
+
if (b.active === true && a.active !== true)
|
|
139
|
+
return 1;
|
|
140
|
+
return b.timestamp.localeCompare(a.timestamp);
|
|
141
|
+
})
|
|
142
|
+
.slice(0, 10);
|
|
143
|
+
}
|
|
144
|
+
function readCliLogEntriesFromCommandDir(command, commandDir) {
|
|
145
|
+
const activeEntries = readActiveCliLogEntriesFromDir(command, commandDir);
|
|
146
|
+
const indexPath = join(commandDir, "index.json");
|
|
126
147
|
try {
|
|
127
148
|
const parsed = readTrustedJsonFile(indexPath);
|
|
128
|
-
|
|
149
|
+
const finalizedEntries = Array.isArray(parsed.entries) ? parsed.entries : [];
|
|
150
|
+
return [
|
|
151
|
+
...activeEntries,
|
|
152
|
+
...finalizedEntries.filter((entry) => !activeEntries.some((active) => active.id === entry.id)),
|
|
153
|
+
].slice(0, 10);
|
|
129
154
|
}
|
|
130
155
|
catch {
|
|
131
|
-
return
|
|
156
|
+
return activeEntries;
|
|
132
157
|
}
|
|
133
158
|
}
|
|
134
159
|
export function readCliLogEntry(command, ordinal = 1, cwd = process.cwd()) {
|
|
@@ -146,6 +171,45 @@ function readCliLogEntriesFromDir(commandDir) {
|
|
|
146
171
|
return [];
|
|
147
172
|
}
|
|
148
173
|
}
|
|
174
|
+
function readActiveCliLogEntriesFromDir(command, commandDir) {
|
|
175
|
+
let files;
|
|
176
|
+
try {
|
|
177
|
+
files = readdirSync(commandDir);
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
return [];
|
|
181
|
+
}
|
|
182
|
+
return files
|
|
183
|
+
.filter((file) => file.endsWith(".log.active"))
|
|
184
|
+
.map((file) => readActiveCliLogEntryFromMarker(command, join(commandDir, file)))
|
|
185
|
+
.filter((entry) => entry !== null)
|
|
186
|
+
.sort((a, b) => b.timestamp.localeCompare(a.timestamp));
|
|
187
|
+
}
|
|
188
|
+
function readActiveCliLogEntryFromMarker(command, markerPath) {
|
|
189
|
+
try {
|
|
190
|
+
const markerStat = statSync(markerPath);
|
|
191
|
+
const marker = readFileSync(markerPath, "utf8");
|
|
192
|
+
const id = basename(markerPath).slice(0, -".log.active".length);
|
|
193
|
+
if (!id)
|
|
194
|
+
return null;
|
|
195
|
+
const markerLines = marker.split("\n");
|
|
196
|
+
const live = isActiveMarkerSnapshotLive(marker, markerStat.mtimeMs);
|
|
197
|
+
return {
|
|
198
|
+
id,
|
|
199
|
+
command,
|
|
200
|
+
timestamp: markerLines[1]?.trim() || new Date(markerStat.mtimeMs).toISOString(),
|
|
201
|
+
exitCode: -1,
|
|
202
|
+
logPath: markerPath.slice(0, -".active".length),
|
|
203
|
+
...(live ? { active: true } : {}),
|
|
204
|
+
summary: live ? "active log" : "orphaned log from exited process",
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
catch (error) {
|
|
208
|
+
if (isMissingFileError(error))
|
|
209
|
+
return null;
|
|
210
|
+
throw error;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
149
213
|
function writeLogEntryInDir(entry, commandDir) {
|
|
150
214
|
withCommandIndexLockInDir(commandDir, () => {
|
|
151
215
|
const indexPath = join(commandDir, "index.json");
|
|
@@ -169,28 +233,86 @@ function writeLogEntryInDir(entry, commandDir) {
|
|
|
169
233
|
}
|
|
170
234
|
function withCommandIndexLockInDir(commandDir, fn) {
|
|
171
235
|
mkdirSync(commandDir, { recursive: true });
|
|
172
|
-
const
|
|
236
|
+
const lockDir = join(commandDir, "index.lock.d");
|
|
237
|
+
mkdirSync(lockDir, { recursive: true });
|
|
238
|
+
const ownerPath = join(lockDir, `${Date.now()}-${process.pid}-${randomUUID()}.owner`);
|
|
239
|
+
writeFileSync(ownerPath, `${process.pid}\n${new Date().toISOString()}\n`, {
|
|
240
|
+
encoding: "utf8",
|
|
241
|
+
flag: "wx",
|
|
242
|
+
});
|
|
173
243
|
const started = Date.now();
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
}
|
|
179
|
-
catch (error) {
|
|
180
|
-
if (!isFileExistsError(error))
|
|
181
|
-
throw error;
|
|
182
|
-
if (Date.now() - started > 5_000) {
|
|
183
|
-
throw new Error(`Timed out waiting for CLI log index lock: ${lockPath}`);
|
|
244
|
+
try {
|
|
245
|
+
while (!isCurrentIndexLockOwner(lockDir, ownerPath)) {
|
|
246
|
+
if (Date.now() - started > INDEX_LOCK_STALE_MS) {
|
|
247
|
+
throw new Error(`Timed out waiting for CLI log index lock: ${lockDir}`);
|
|
184
248
|
}
|
|
185
249
|
sleepSync(10);
|
|
186
250
|
}
|
|
187
|
-
}
|
|
188
|
-
try {
|
|
189
251
|
return fn();
|
|
190
252
|
}
|
|
191
253
|
finally {
|
|
192
|
-
|
|
193
|
-
|
|
254
|
+
rmSync(ownerPath, { force: true });
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
function isCurrentIndexLockOwner(lockDir, ownerPath) {
|
|
258
|
+
const liveOwners = readLiveIndexLockOwners(lockDir);
|
|
259
|
+
const firstOwner = liveOwners[0];
|
|
260
|
+
if (firstOwner === undefined)
|
|
261
|
+
return false;
|
|
262
|
+
return firstOwner.path === ownerPath;
|
|
263
|
+
}
|
|
264
|
+
function readLiveIndexLockOwners(lockDir) {
|
|
265
|
+
let files;
|
|
266
|
+
try {
|
|
267
|
+
files = readdirSync(lockDir);
|
|
268
|
+
}
|
|
269
|
+
catch (error) {
|
|
270
|
+
if (isMissingFileError(error))
|
|
271
|
+
return [];
|
|
272
|
+
throw error;
|
|
273
|
+
}
|
|
274
|
+
return files
|
|
275
|
+
.filter((file) => file.endsWith(".owner"))
|
|
276
|
+
.map((file) => readIndexLockOwner(join(lockDir, file)))
|
|
277
|
+
.filter((owner) => owner !== null)
|
|
278
|
+
.sort((a, b) => a.timestampMs - b.timestampMs || a.path.localeCompare(b.path));
|
|
279
|
+
}
|
|
280
|
+
function readIndexLockOwner(ownerPath) {
|
|
281
|
+
let marker = "";
|
|
282
|
+
let markerMtimeMs = 0;
|
|
283
|
+
try {
|
|
284
|
+
const markerStat = statSync(ownerPath);
|
|
285
|
+
markerMtimeMs = markerStat.mtimeMs;
|
|
286
|
+
marker = readFileSync(ownerPath, "utf8");
|
|
287
|
+
}
|
|
288
|
+
catch (error) {
|
|
289
|
+
if (isMissingFileError(error))
|
|
290
|
+
return null;
|
|
291
|
+
throw error;
|
|
292
|
+
}
|
|
293
|
+
const markerLines = marker.split("\n");
|
|
294
|
+
const pid = Number(markerLines[0]);
|
|
295
|
+
const hasOwner = Number.isInteger(pid) && pid > 0;
|
|
296
|
+
const timestamp = Date.parse(markerLines[1] ?? "");
|
|
297
|
+
const timestampMs = Number.isFinite(timestamp) ? timestamp : markerMtimeMs;
|
|
298
|
+
const stale = (hasOwner && !isProcessLive(pid)) ||
|
|
299
|
+
(!hasOwner && Date.now() - markerMtimeMs > INDEX_LOCK_STALE_MS);
|
|
300
|
+
if (stale) {
|
|
301
|
+
rmSync(ownerPath, { force: true });
|
|
302
|
+
return null;
|
|
303
|
+
}
|
|
304
|
+
return { path: ownerPath, timestampMs };
|
|
305
|
+
}
|
|
306
|
+
function isProcessLive(pid) {
|
|
307
|
+
try {
|
|
308
|
+
process.kill(pid, 0);
|
|
309
|
+
return true;
|
|
310
|
+
}
|
|
311
|
+
catch (error) {
|
|
312
|
+
if (typeof error === "object" && error !== null && "code" in error) {
|
|
313
|
+
return error.code === "EPERM";
|
|
314
|
+
}
|
|
315
|
+
return false;
|
|
194
316
|
}
|
|
195
317
|
}
|
|
196
318
|
function writeIndexAtomically(indexPath, index) {
|
|
@@ -233,31 +355,45 @@ function canPruneLogPath(logPath) {
|
|
|
233
355
|
}
|
|
234
356
|
}
|
|
235
357
|
function isActiveLogPath(logPath) {
|
|
358
|
+
const activeMarkerPath = getActiveMarkerPath(logPath);
|
|
236
359
|
try {
|
|
237
|
-
|
|
238
|
-
|
|
360
|
+
const markerStat = statSync(activeMarkerPath);
|
|
361
|
+
const marker = readFileSync(activeMarkerPath, "utf8");
|
|
362
|
+
const live = isActiveMarkerSnapshotLive(marker, markerStat.mtimeMs);
|
|
363
|
+
if (!live)
|
|
364
|
+
rmSync(activeMarkerPath, { force: true });
|
|
365
|
+
return live;
|
|
239
366
|
}
|
|
240
|
-
catch {
|
|
367
|
+
catch (error) {
|
|
368
|
+
if (!isMissingFileError(error))
|
|
369
|
+
throw error;
|
|
241
370
|
return false;
|
|
242
371
|
}
|
|
243
372
|
}
|
|
373
|
+
function isActiveMarkerSnapshotLive(marker, markerMtimeMs) {
|
|
374
|
+
const pid = Number(marker.split("\n")[0]);
|
|
375
|
+
if (Number.isInteger(pid) && pid > 0) {
|
|
376
|
+
return isProcessLive(pid);
|
|
377
|
+
}
|
|
378
|
+
return Date.now() - markerMtimeMs <= ORPHAN_LOG_GRACE_MS;
|
|
379
|
+
}
|
|
244
380
|
function getActiveMarkerPath(logPath) {
|
|
245
381
|
return `${logPath}.active`;
|
|
246
382
|
}
|
|
247
383
|
function sleepSync(ms) {
|
|
248
384
|
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
249
385
|
}
|
|
250
|
-
function isFileExistsError(error) {
|
|
251
|
-
return typeof error === "object" && error !== null && "code" in error && error.code === "EEXIST";
|
|
252
|
-
}
|
|
253
386
|
function isMissingFileError(error) {
|
|
254
387
|
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
255
388
|
}
|
|
256
389
|
function getCommandLogDir(command, cwd) {
|
|
257
390
|
return getSurfacePath(join("cli-logs", command), "repo", cwd);
|
|
258
391
|
}
|
|
259
|
-
function
|
|
260
|
-
return join(
|
|
392
|
+
function getFallbackCommandLogDir(command, cwd) {
|
|
393
|
+
return join(tmpdir(), "webpresso", "cli-logs", fallbackScopeId(cwd), command);
|
|
394
|
+
}
|
|
395
|
+
function fallbackScopeId(cwd) {
|
|
396
|
+
return createHash("sha256").update(resolve(cwd)).digest("hex").slice(0, 12);
|
|
261
397
|
}
|
|
262
398
|
function createLogId(now = new Date()) {
|
|
263
399
|
const iso = now.toISOString().replaceAll(":", "-").replaceAll(".", "-");
|
|
@@ -7,6 +7,7 @@ import { z } from "zod";
|
|
|
7
7
|
import { MAX_PORTABLE_REPOSITORY_PATH_LENGTH } from "#audit/absolute-path-policy.js";
|
|
8
8
|
import { getProjectRoot } from "#cli/utils";
|
|
9
9
|
import { relativeBlueprintSlug, resolveBlueprintFile } from "#lifecycle/local.js";
|
|
10
|
+
import { readBlueprintOverviewAtRef } from "#lifecycle/review-provenance.js";
|
|
10
11
|
import { scanBlueprintDirectory } from "#service/scanner.js";
|
|
11
12
|
import { resolveBlueprintRoot } from "#utils/blueprint-root.js";
|
|
12
13
|
import { CLAUDE_DEFAULT_REVIEW_MODEL, CODEX_DEFAULT_REVIEW_MODEL, GROK_DEFAULT_REVIEW_MODEL, DEFAULT_REVIEW_MAX_PROGRESS_EVENTS, runReviewExecution, } from "#review/execution/index.js";
|
|
@@ -860,14 +861,10 @@ function readBlueprintAtTarget(projectRoot, blueprintPath, targetSha) {
|
|
|
860
861
|
throw new Error(`Blueprint path is outside Git toplevel: ${blueprintPath}`);
|
|
861
862
|
}
|
|
862
863
|
const relative = gitRelativePath.replace(/\\/gu, "/");
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
throw new Error(`Blueprint ${relative} is not tracked at target ${targetSha}.`, {
|
|
868
|
-
cause: error,
|
|
869
|
-
});
|
|
870
|
-
}
|
|
864
|
+
const content = readBlueprintOverviewAtRef(gitRoot, targetSha, absoluteBlueprintPath);
|
|
865
|
+
if (content)
|
|
866
|
+
return content.toString("utf8");
|
|
867
|
+
throw new Error(`Blueprint ${relative} is not tracked at target ${targetSha}.`);
|
|
871
868
|
}
|
|
872
869
|
function blueprintPathAtGitRoot(projectRoot, blueprintPath) {
|
|
873
870
|
const gitRoot = resolveReviewGitToplevel(projectRoot);
|
|
@@ -2,6 +2,7 @@ import type { CAC } from "cac";
|
|
|
2
2
|
import { spawnRuntimeCommandSync } from "#runtime/executor.js";
|
|
3
3
|
export interface SecretsCommandOptions {
|
|
4
4
|
readonly cwd?: string;
|
|
5
|
+
readonly commandCwd?: string;
|
|
5
6
|
readonly profile?: string;
|
|
6
7
|
readonly sink?: string;
|
|
7
8
|
readonly json?: boolean;
|
|
@@ -14,6 +14,7 @@ export function registerSecretsCommand(cli) {
|
|
|
14
14
|
.command("secrets <action> [target]", "Secret orchestration commands (doctor, bootstrap github)")
|
|
15
15
|
.option("--profile <name>", "Secret profile name", { default: "preview" })
|
|
16
16
|
.option("--sink <id>", "Secret sink id")
|
|
17
|
+
.option("--command-cwd <dir>", "Run the scoped command from this directory")
|
|
17
18
|
.option("--json", "Emit machine-readable JSON")
|
|
18
19
|
.option("--lane <lane>", "Lane to bootstrap (repeatable)", { default: [] })
|
|
19
20
|
.option("--apply", "Apply GitHub bootstrap changes instead of dry-run planning")
|
|
@@ -22,6 +23,7 @@ export function registerSecretsCommand(cli) {
|
|
|
22
23
|
cwd: process.cwd(),
|
|
23
24
|
profile: flags.profile,
|
|
24
25
|
sink: flags.sink,
|
|
26
|
+
commandCwd: flags.commandCwd,
|
|
25
27
|
json: Boolean(flags.json),
|
|
26
28
|
lanes: toArray(flags.lane),
|
|
27
29
|
apply: Boolean(flags.apply),
|
|
@@ -109,7 +111,8 @@ export function runSecretsRun(options = {}, deps = {}) {
|
|
|
109
111
|
const result = (deps.runSecretScopedCommand ?? spawnRuntimeCommandSync)({
|
|
110
112
|
command,
|
|
111
113
|
args,
|
|
112
|
-
cwd: options.cwd,
|
|
114
|
+
cwd: options.commandCwd ?? options.cwd,
|
|
115
|
+
envCwd: options.cwd,
|
|
113
116
|
profile: plan.runtimeProfile,
|
|
114
117
|
secretProfile: plan.profile,
|
|
115
118
|
environment: plan.environment,
|
|
@@ -11,6 +11,7 @@ import { canonicalizeWorktreePath } from "#worktrees/identity.js";
|
|
|
11
11
|
import { dashProviderOwnershipName, parseDashPermissionMode, } from "#cli/commands/dash/provider-capabilities.js";
|
|
12
12
|
import { optionalToolCanonicalCommand } from "#cli/optional-tools.js";
|
|
13
13
|
import { commandExists } from "#runtime/command-exists.js";
|
|
14
|
+
import { ensureCodexProjectMcpServers, ensureCodexWebpressoMcp, } from "#cli/commands/init/scaffolders/codex-mcp/index.js";
|
|
14
15
|
export class DirectProviderLaunchPreparationError extends Error {
|
|
15
16
|
action;
|
|
16
17
|
constructor(message, action) {
|
|
@@ -90,6 +91,25 @@ function validateDirectProviderLaunch(provider, cwd) {
|
|
|
90
91
|
vpCommand,
|
|
91
92
|
};
|
|
92
93
|
}
|
|
94
|
+
function ensureCodexSessionTooling(repoRoot) {
|
|
95
|
+
const options = { dryRun: false, overwrite: false };
|
|
96
|
+
const webpresso = ensureCodexWebpressoMcp({ options });
|
|
97
|
+
if (webpresso.kind === "codex-webpresso-mcp-not-installed") {
|
|
98
|
+
throw new DirectProviderLaunchPreparationError(`Codex webpresso MCP is not configured because no usable agent-kit install root was found. Checked: ${webpresso.checked.join(", ") || "(none)"}`, "Run `wp setup --yes-global` from this repo, then retry `wp codex`.");
|
|
99
|
+
}
|
|
100
|
+
const project = ensureCodexProjectMcpServers({ repoRoot, options });
|
|
101
|
+
if (project.kind === "codex-project-mcp-invalid") {
|
|
102
|
+
throw new DirectProviderLaunchPreparationError(`Codex project MCP config is invalid: ${project.reason}`, "Fix .mcp.json or run `wp setup --yes-global` for detailed setup diagnostics.");
|
|
103
|
+
}
|
|
104
|
+
if (project.kind === "codex-project-mcp-unavailable") {
|
|
105
|
+
throw new DirectProviderLaunchPreparationError(`Codex project MCP server ${project.server} is unavailable: ${project.reason}`, "Run `wp setup --yes-global` from this repo to provision project MCP servers, then retry.");
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
function ensureProviderSessionTooling(provider, cwd) {
|
|
109
|
+
if (provider !== "codex")
|
|
110
|
+
return;
|
|
111
|
+
ensureCodexSessionTooling(cwd);
|
|
112
|
+
}
|
|
93
113
|
export function isBareInteractiveProviderLaunch(args) {
|
|
94
114
|
return parseDashPermissionMode(args).args.length === 0;
|
|
95
115
|
}
|
|
@@ -136,14 +156,17 @@ export async function prepareDirectProviderLaunch(provider, args, cwd) {
|
|
|
136
156
|
return { cwd, warnings: [] };
|
|
137
157
|
}
|
|
138
158
|
const { managedCwd, warnings } = await prepareLaunchEnvironment(provider, cwd);
|
|
139
|
-
if (managedCwd)
|
|
159
|
+
if (managedCwd) {
|
|
160
|
+
ensureProviderSessionTooling(provider, managedCwd);
|
|
140
161
|
return { cwd: managedCwd, warnings };
|
|
162
|
+
}
|
|
141
163
|
const created = await createManagedWorktree({
|
|
142
164
|
cwd,
|
|
143
165
|
prefix: provider,
|
|
144
166
|
base: "origin/main",
|
|
145
167
|
dependencyHydration: dependencyHydrationForProvider(provider),
|
|
146
168
|
});
|
|
169
|
+
ensureProviderSessionTooling(provider, created.path);
|
|
147
170
|
return { cwd: created.path, warnings };
|
|
148
171
|
}
|
|
149
172
|
/**
|
|
@@ -163,5 +186,6 @@ export function dependencyHydrationForProvider(provider) {
|
|
|
163
186
|
*/
|
|
164
187
|
export async function prepareDirectProviderLaunchAt(provider, cwd) {
|
|
165
188
|
const { warnings } = await prepareLaunchEnvironment(provider, cwd);
|
|
189
|
+
ensureProviderSessionTooling(provider, cwd);
|
|
166
190
|
return { cwd, warnings };
|
|
167
191
|
}
|
|
@@ -116,6 +116,21 @@ export declare function checkCodexConfigKeys(configFilePath?: string, deps?: Hos
|
|
|
116
116
|
* (exact `vp dlx` pins, launcher rules) at doctor time instead of only at setup.
|
|
117
117
|
*/
|
|
118
118
|
export declare function checkProjectMcpPins(cwd?: string, deps?: HostConfigDoctorDeps): DoctorCheck;
|
|
119
|
+
export interface ClaudeMcpDuplicateRegistrationOptions {
|
|
120
|
+
readonly home?: string;
|
|
121
|
+
readonly exists?: (path: string) => boolean;
|
|
122
|
+
readonly readFile?: (path: string) => string;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* `claude-mcp-duplicate-registration` — advisory. A machine can register the
|
|
126
|
+
* webpresso MCP server twice for the same Claude Code session: a manual
|
|
127
|
+
* `claude mcp add webpresso` entry in `~/.claude.json` (user scope or a
|
|
128
|
+
* project scope) plus the agent-kit marketplace plugin's bundled server
|
|
129
|
+
* (plugin cache under `~/.claude/plugins/`). Both spawn `wp mcp`, so affected
|
|
130
|
+
* sessions run two server processes and carry a duplicated wp_* tool list.
|
|
131
|
+
* Degrades to ok on missing/unreadable config — loud only on real duplication.
|
|
132
|
+
*/
|
|
133
|
+
export declare function checkClaudeMcpDuplicateRegistration(options?: ClaudeMcpDuplicateRegistrationOptions): DoctorCheck;
|
|
119
134
|
export interface DanglingHostPathsOptions {
|
|
120
135
|
readonly configFilePath?: string;
|
|
121
136
|
readonly home?: string;
|
package/dist/esm/hooks/doctor.js
CHANGED
|
@@ -1790,6 +1790,61 @@ export function checkProjectMcpPins(cwd = process.cwd(), deps = {}) {
|
|
|
1790
1790
|
: `${parsed.definitions.length} exact-pinned vp dlx server(s) validated`,
|
|
1791
1791
|
};
|
|
1792
1792
|
}
|
|
1793
|
+
/**
|
|
1794
|
+
* `claude-mcp-duplicate-registration` — advisory. A machine can register the
|
|
1795
|
+
* webpresso MCP server twice for the same Claude Code session: a manual
|
|
1796
|
+
* `claude mcp add webpresso` entry in `~/.claude.json` (user scope or a
|
|
1797
|
+
* project scope) plus the agent-kit marketplace plugin's bundled server
|
|
1798
|
+
* (plugin cache under `~/.claude/plugins/`). Both spawn `wp mcp`, so affected
|
|
1799
|
+
* sessions run two server processes and carry a duplicated wp_* tool list.
|
|
1800
|
+
* Degrades to ok on missing/unreadable config — loud only on real duplication.
|
|
1801
|
+
*/
|
|
1802
|
+
export function checkClaudeMcpDuplicateRegistration(options = {}) {
|
|
1803
|
+
const name = "claude MCP duplicate registration";
|
|
1804
|
+
const home = options.home ?? homedir();
|
|
1805
|
+
const exists = options.exists ?? existsSync;
|
|
1806
|
+
const readFile = options.readFile ?? ((candidate) => readFileSync(candidate, "utf8"));
|
|
1807
|
+
const pluginCacheDir = join(home, ".claude", "plugins", "cache", "webpresso", "agent-kit");
|
|
1808
|
+
if (!exists(pluginCacheDir)) {
|
|
1809
|
+
return { name, ok: true, detail: "skipped (agent-kit Claude plugin not installed)" };
|
|
1810
|
+
}
|
|
1811
|
+
const claudeJsonPath = join(home, ".claude.json");
|
|
1812
|
+
if (!exists(claudeJsonPath)) {
|
|
1813
|
+
return { name, ok: true, detail: "skipped (no ~/.claude.json)" };
|
|
1814
|
+
}
|
|
1815
|
+
let parsed;
|
|
1816
|
+
try {
|
|
1817
|
+
parsed = JSON.parse(readFile(claudeJsonPath));
|
|
1818
|
+
}
|
|
1819
|
+
catch {
|
|
1820
|
+
return { name, ok: true, detail: "skipped (~/.claude.json unreadable)" };
|
|
1821
|
+
}
|
|
1822
|
+
if (parsed === null || typeof parsed !== "object") {
|
|
1823
|
+
return { name, ok: true, detail: "skipped (~/.claude.json is not an object)" };
|
|
1824
|
+
}
|
|
1825
|
+
const config = parsed;
|
|
1826
|
+
const manualSources = [];
|
|
1827
|
+
if (config.mcpServers && "webpresso" in config.mcpServers) {
|
|
1828
|
+
manualSources.push("user scope");
|
|
1829
|
+
}
|
|
1830
|
+
for (const [projectDir, project] of Object.entries(config.projects ?? {})) {
|
|
1831
|
+
if (project?.mcpServers && "webpresso" in project.mcpServers) {
|
|
1832
|
+
manualSources.push(`project scope ${projectDir}`);
|
|
1833
|
+
}
|
|
1834
|
+
}
|
|
1835
|
+
if (manualSources.length === 0) {
|
|
1836
|
+
return { name, ok: true, detail: "plugin-scoped registration only" };
|
|
1837
|
+
}
|
|
1838
|
+
return {
|
|
1839
|
+
name,
|
|
1840
|
+
ok: false,
|
|
1841
|
+
advisory: true,
|
|
1842
|
+
detail: `the agent-kit Claude plugin bundles the webpresso MCP server, but ~/.claude.json also ` +
|
|
1843
|
+
`registers "webpresso" manually (${manualSources.join(", ")}). Affected sessions run two ` +
|
|
1844
|
+
`wp mcp processes with a duplicated wp_* tool list. Remove the manual entry: ` +
|
|
1845
|
+
`claude mcp remove webpresso (run inside the project directory for project-scope entries).`,
|
|
1846
|
+
};
|
|
1847
|
+
}
|
|
1793
1848
|
const QUOTED_TOML_STRING = /"((?:[^"\\]|\\.)*)"/gu;
|
|
1794
1849
|
const DANGLING_SYMLINK_SCAN_DEPTH = 3;
|
|
1795
1850
|
function collectDanglingWorktreePaths(raw, worktreesRoot, exists, alreadyReported) {
|
|
@@ -1994,6 +2049,7 @@ export async function runHooksDoctor(opts = {}) {
|
|
|
1994
2049
|
checks.push(checkCodexConfigKeys());
|
|
1995
2050
|
}
|
|
1996
2051
|
checks.push(checkProjectMcpPins(opts.cwd));
|
|
2052
|
+
checks.push(checkClaudeMcpDuplicateRegistration());
|
|
1997
2053
|
checks.push({ advisory: true, ...checkDanglingHostPaths() });
|
|
1998
2054
|
checks.push({ advisory: true, ...checkRootLauncherContract() });
|
|
1999
2055
|
checks.push({ advisory: true, ...checkNativePluginRuntime() });
|
|
@@ -371,7 +371,7 @@ function findWpAuditRedirect(command) {
|
|
|
371
371
|
for (const variant of getCommandVariants(command)) {
|
|
372
372
|
const kind = WP_AUDIT_RE.exec(variant)?.[1];
|
|
373
373
|
if (kind === "guardrails") {
|
|
374
|
-
return `"${variant}" denied — use the MCP batch audit tool:
|
|
374
|
+
return `"${variant}" denied — use the webpresso MCP batch audit tool: wp_audits(preset="guardrails"). Returns aggregate structured, summary-first results.`;
|
|
375
375
|
}
|
|
376
376
|
if (!kind || !AUDIT_KIND_SET.has(kind))
|
|
377
377
|
continue;
|
|
@@ -381,11 +381,11 @@ function findWpAuditRedirect(command) {
|
|
|
381
381
|
knownKinds.push(kind);
|
|
382
382
|
}
|
|
383
383
|
if (knownKinds.length > 1) {
|
|
384
|
-
return `Multiple wp audit commands denied — use the MCP batch audit tool:
|
|
384
|
+
return `Multiple wp audit commands denied — use the webpresso MCP batch audit tool: wp_audits(kinds=[${knownKinds.map((kind) => `"${kind}"`).join(", ")}]). Returns aggregate structured, summary-first results.`;
|
|
385
385
|
}
|
|
386
386
|
const kind = knownKinds[0];
|
|
387
387
|
if (kind && firstKnownVariant) {
|
|
388
|
-
return `"${firstKnownVariant}" denied — use the MCP audit tool:
|
|
388
|
+
return `"${firstKnownVariant}" denied — use the webpresso MCP audit tool: wp_audit(kind="${kind}"). Returns structured, summary-first results.`;
|
|
389
389
|
}
|
|
390
390
|
return undefined;
|
|
391
391
|
}
|
|
@@ -402,7 +402,7 @@ function findScriptRouteRedirect(command, routes) {
|
|
|
402
402
|
process.stderr.write(`[forbidden-commands] guard.scriptRoutes["${script}"] -> "${kind}" is not a known audit kind; ignoring\n`);
|
|
403
403
|
continue;
|
|
404
404
|
}
|
|
405
|
-
return `"${variant}" denied — this repo routes \`${script}\` to an audit:
|
|
405
|
+
return `"${variant}" denied — this repo routes \`${script}\` to an audit: use the webpresso MCP tool wp_audit(kind="${kind}").`;
|
|
406
406
|
}
|
|
407
407
|
return undefined;
|
|
408
408
|
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { isMcpReady } from "#hooks/shared/mcp-sentinel";
|
|
2
|
-
const DEFAULT_SERVER_NAME = "webpresso";
|
|
3
2
|
const DEFAULT_TOOL_PREFIX = "wp_";
|
|
4
3
|
let cachedDefaultMcpReady;
|
|
5
4
|
export function resetMcpReadyCache() {
|
|
@@ -22,7 +21,6 @@ export function resolveMcpReady(value) {
|
|
|
22
21
|
}
|
|
23
22
|
export function resolveMcpConfig(mcp) {
|
|
24
23
|
return {
|
|
25
|
-
serverName: mcp?.serverName?.trim() || DEFAULT_SERVER_NAME,
|
|
26
24
|
toolPrefix: mcp?.toolPrefix?.trim() || DEFAULT_TOOL_PREFIX,
|
|
27
25
|
};
|
|
28
26
|
}
|
|
@@ -50,10 +48,15 @@ export function buildRedirectMessage(ctx) {
|
|
|
50
48
|
if (!resolveMcpReady(ctx.mcpReady)) {
|
|
51
49
|
return `"${ctx.command}" denied — MCP not ready. Use: ${fallbackHint}`;
|
|
52
50
|
}
|
|
53
|
-
|
|
54
|
-
|
|
51
|
+
// Suffix-only tool names: hosts register the webpresso MCP server under
|
|
52
|
+
// different prefixes (manual `mcp__webpresso__*` vs plugin-scoped
|
|
53
|
+
// `mcp__plugin_agent-kit_webpresso__*`), so a hardcoded full name resolves in
|
|
54
|
+
// some sessions and dangles in others. The `wp_<suffix>` identity is stable
|
|
55
|
+
// across every registration shape.
|
|
56
|
+
const { toolPrefix } = resolveMcpConfig(ctx.mcp);
|
|
57
|
+
const toolMatchers = toolSuffixesForCategory(ctx.category).map((suffix) => `${toolPrefix}${suffix}(...)`);
|
|
55
58
|
return [
|
|
56
|
-
`"${ctx.command}" denied — use
|
|
59
|
+
`"${ctx.command}" denied — use the webpresso MCP tool:`,
|
|
57
60
|
...toolMatchers.map((matcher) => ` ${matcher}`),
|
|
58
61
|
"Returns structured, summary-first results. Raw output is clipped; overflow may include a log path for deeper investigation.",
|
|
59
62
|
`Fallback if MCP unavailable: ${fallbackHint}`,
|
|
@@ -17,13 +17,15 @@ import type { ToolInput, ValidationResult } from "#hooks/shared/types";
|
|
|
17
17
|
* effective cwd elsewhere (e.g. CI) → ALLOW (rule scoped to local ~/repos primaries)
|
|
18
18
|
* ```
|
|
19
19
|
*
|
|
20
|
-
* The "effective cwd" is the tool's ambient `input.cwd`, lowered
|
|
21
|
-
* success-gated
|
|
22
|
-
* `cd <worktree> && git commit
|
|
23
|
-
* EVERY `git -C <dir>` flag on the
|
|
24
|
-
* cumulatively).
|
|
25
|
-
*
|
|
26
|
-
*
|
|
20
|
+
* The "effective cwd" is the tool's ambient `input.cwd`, lowered by a
|
|
21
|
+
* success-gated `&&` chain that governs the forbidden git op, e.g.
|
|
22
|
+
* `cd <worktree> && git commit`, `cd <worktree> && ./bin/wp … && git commit`,
|
|
23
|
+
* or `(cd <worktree> && git commit)`, then by EVERY `git -C <dir>` flag on the
|
|
24
|
+
* matched invocation (git applies them cumulatively). Intermediate non-cwd
|
|
25
|
+
* commands after a proven `cd` leave the chain cwd unchanged. Unsupported
|
|
26
|
+
* control flow (`;` / `|` / braces), option-bearing `cd`, `cd -`, or
|
|
27
|
+
* unresolvable `cd`/`-C` targets fail closed because they could hide a
|
|
28
|
+
* primary-checkout mutation. Prefer `git -C <worktree> ...` or running the
|
|
27
29
|
* command with the tool cwd already set to the worktree.
|
|
28
30
|
*
|
|
29
31
|
* Known best-effort limits (acceptable for an agent guard with a documented
|