@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
|
@@ -6,6 +6,7 @@ export interface PublicCiActOptions {
|
|
|
6
6
|
readonly workflow?: string;
|
|
7
7
|
readonly workflowPath?: string;
|
|
8
8
|
readonly job?: string;
|
|
9
|
+
readonly matrix?: readonly string[];
|
|
9
10
|
readonly eventName?: CiActEventName;
|
|
10
11
|
readonly eventPath?: string;
|
|
11
12
|
readonly envProfile?: string;
|
|
@@ -22,6 +23,7 @@ export interface PublicCiActCommand {
|
|
|
22
23
|
readonly actArgs: readonly string[];
|
|
23
24
|
}
|
|
24
25
|
export interface PreparedPublicCiActExecution {
|
|
26
|
+
readonly cwd: string;
|
|
25
27
|
readonly command: PublicCiActCommand;
|
|
26
28
|
readonly mode: CiActExecutionMode;
|
|
27
29
|
readonly nonSecurityEquivalent: boolean;
|
|
@@ -36,5 +38,7 @@ export declare function buildPublicCiActArgs(options?: PublicCiActOptions, workf
|
|
|
36
38
|
export declare function buildPublicCiActCommand(options?: PublicCiActOptions): PublicCiActCommand;
|
|
37
39
|
export declare function preparePublicCiActExecution(options?: PublicCiActOptions): PreparedPublicCiActExecution;
|
|
38
40
|
export declare function resolveCiActSecretEnvProfile(options?: PublicCiActOptions): string | undefined;
|
|
41
|
+
export declare function shouldForceCiActSecretGate(envProfile: string | undefined): boolean;
|
|
39
42
|
export declare function sanitizePublicCiActArgv(command: PublicCiActCommand): PublicCiActCommand;
|
|
40
43
|
export declare function assertNoForbiddenCiActArgs(args: readonly string[]): void;
|
|
44
|
+
export declare function normalizeCiActMatrixSelectors(selectors: readonly string[] | undefined): readonly string[];
|
|
@@ -2,6 +2,7 @@ import { resolve } from "node:path";
|
|
|
2
2
|
import { buildSecretGateCommand } from "#secret-gate/runner.js";
|
|
3
3
|
import { resolveSecretsConfigProfileEnvironment } from "#runtime/secrets-config.js";
|
|
4
4
|
import { createReplayWorkflow, GENERATED_REPLAY_WORKFLOW_PLACEHOLDER, } from "./act-replay.js";
|
|
5
|
+
import { prepareActGitWorkspace } from "./act-worktree-git.js";
|
|
5
6
|
export { GENERATED_REPLAY_WORKFLOW_PLACEHOLDER } from "./act-replay.js";
|
|
6
7
|
const DEFAULT_WORKFLOW = "ci-e2e";
|
|
7
8
|
export const DEFAULT_PLATFORM_IMAGE = "ghcr.io/catthehacker/ubuntu:full-latest";
|
|
@@ -43,6 +44,7 @@ export function buildPublicCiActArgs(options = {}, workflowPathOverride) {
|
|
|
43
44
|
"--rm",
|
|
44
45
|
];
|
|
45
46
|
pushOption(args, "-j", options.job);
|
|
47
|
+
pushRepeatableOption(args, "--matrix", normalizeCiActMatrixSelectors(options.matrix));
|
|
46
48
|
pushOption(args, "-e", options.eventPath ? resolve(cwd, options.eventPath) : undefined);
|
|
47
49
|
pushOption(args, "--container-architecture", containerArchitecture);
|
|
48
50
|
return args;
|
|
@@ -57,7 +59,7 @@ export function buildPublicCiActCommand(options = {}) {
|
|
|
57
59
|
sink: "act",
|
|
58
60
|
profile: secretEnvProfile,
|
|
59
61
|
envProfile: options.envProfile ?? "secrets-only",
|
|
60
|
-
forceSecretGate:
|
|
62
|
+
forceSecretGate: shouldForceCiActSecretGate(options.envProfile),
|
|
61
63
|
command: "act",
|
|
62
64
|
args: actArgs,
|
|
63
65
|
});
|
|
@@ -65,32 +67,63 @@ export function buildPublicCiActCommand(options = {}) {
|
|
|
65
67
|
}
|
|
66
68
|
export function preparePublicCiActExecution(options = {}) {
|
|
67
69
|
const cwd = options.cwd ?? process.cwd();
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
70
|
+
let gitWorkspace = null;
|
|
71
|
+
let replay = null;
|
|
72
|
+
try {
|
|
73
|
+
gitWorkspace = prepareActGitWorkspace(cwd);
|
|
74
|
+
const executionCwd = gitWorkspace.cwd;
|
|
75
|
+
const mode = resolveCiActExecutionMode(options);
|
|
76
|
+
const sourceWorkflowPath = resolve(cwd, resolveCiActWorkflowPath(options));
|
|
77
|
+
replay =
|
|
78
|
+
mode === "replay"
|
|
79
|
+
? createReplayWorkflow({
|
|
80
|
+
cwd,
|
|
81
|
+
workflowPath: sourceWorkflowPath,
|
|
82
|
+
eventName: options.eventName ?? "pull_request",
|
|
83
|
+
})
|
|
84
|
+
: null;
|
|
85
|
+
const executionOptions = {
|
|
86
|
+
...options,
|
|
87
|
+
cwd: executionCwd,
|
|
88
|
+
eventPath: options.eventPath ? resolve(cwd, options.eventPath) : undefined,
|
|
89
|
+
};
|
|
90
|
+
const actArgs = buildPublicCiActArgs(executionOptions, replay ? replay.workflowPath : sourceWorkflowPath);
|
|
91
|
+
if (gitWorkspace.prepared) {
|
|
92
|
+
actArgs.push("--bind");
|
|
93
|
+
}
|
|
94
|
+
const secretEnvProfile = resolveCiActSecretEnvProfile({ ...options, cwd }) ?? "preview";
|
|
95
|
+
const wrapped = buildSecretGateCommand({
|
|
96
|
+
sink: "act",
|
|
97
|
+
profile: secretEnvProfile,
|
|
98
|
+
envProfile: options.envProfile ?? "secrets-only",
|
|
99
|
+
forceSecretGate: shouldForceCiActSecretGate(options.envProfile),
|
|
100
|
+
command: "act",
|
|
101
|
+
args: actArgs,
|
|
102
|
+
});
|
|
103
|
+
return {
|
|
104
|
+
cwd: executionCwd,
|
|
105
|
+
command: { command: wrapped.command, args: wrapped.args, actArgs },
|
|
106
|
+
mode,
|
|
107
|
+
nonSecurityEquivalent: mode === "replay",
|
|
108
|
+
cleanup() {
|
|
109
|
+
try {
|
|
110
|
+
replay?.cleanup();
|
|
111
|
+
}
|
|
112
|
+
finally {
|
|
113
|
+
gitWorkspace?.cleanup();
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
try {
|
|
91
120
|
replay?.cleanup();
|
|
92
|
-
}
|
|
93
|
-
|
|
121
|
+
}
|
|
122
|
+
finally {
|
|
123
|
+
gitWorkspace?.cleanup();
|
|
124
|
+
}
|
|
125
|
+
throw error;
|
|
126
|
+
}
|
|
94
127
|
}
|
|
95
128
|
export function resolveCiActSecretEnvProfile(options = {}) {
|
|
96
129
|
const secretEnvProfile = options.secretEnvProfile?.trim();
|
|
@@ -103,6 +136,9 @@ export function resolveCiActSecretEnvProfile(options = {}) {
|
|
|
103
136
|
}
|
|
104
137
|
return undefined;
|
|
105
138
|
}
|
|
139
|
+
export function shouldForceCiActSecretGate(envProfile) {
|
|
140
|
+
return envProfile !== "none" && envProfile !== "public";
|
|
141
|
+
}
|
|
106
142
|
export function sanitizePublicCiActArgv(command) {
|
|
107
143
|
return {
|
|
108
144
|
command: command.command,
|
|
@@ -126,11 +162,34 @@ export function assertNoForbiddenCiActArgs(args) {
|
|
|
126
162
|
if (found)
|
|
127
163
|
throw new Error(`Unsupported unsafe ci act argument: ${found}`);
|
|
128
164
|
}
|
|
165
|
+
export function normalizeCiActMatrixSelectors(selectors) {
|
|
166
|
+
if (!selectors)
|
|
167
|
+
return [];
|
|
168
|
+
return selectors.map((selector) => normalizeCiActMatrixSelector(selector));
|
|
169
|
+
}
|
|
129
170
|
function pushOption(args, flag, value) {
|
|
130
171
|
if (value === undefined || value === "")
|
|
131
172
|
return;
|
|
132
173
|
args.push(flag, String(value));
|
|
133
174
|
}
|
|
175
|
+
function pushRepeatableOption(args, flag, values) {
|
|
176
|
+
for (const value of values) {
|
|
177
|
+
args.push(flag, value);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
function normalizeCiActMatrixSelector(selector) {
|
|
181
|
+
const trimmed = selector.trim();
|
|
182
|
+
const delimiter = trimmed.indexOf(":");
|
|
183
|
+
if (trimmed.length === 0 || delimiter <= 0) {
|
|
184
|
+
throw new Error(`Invalid ci act matrix selector "${selector}". Use key:value with non-empty key and value.`);
|
|
185
|
+
}
|
|
186
|
+
const key = trimmed.slice(0, delimiter).trim();
|
|
187
|
+
const value = trimmed.slice(delimiter + 1).trim();
|
|
188
|
+
if (key.length === 0 || value.length === 0) {
|
|
189
|
+
throw new Error(`Invalid ci act matrix selector "${selector}". Use key:value with non-empty key and value.`);
|
|
190
|
+
}
|
|
191
|
+
return `${key}:${value}`;
|
|
192
|
+
}
|
|
134
193
|
function sanitizeArg(value) {
|
|
135
194
|
if (/wp-ci-act-[^/\\]+[/\\]secrets\.env$/u.test(value))
|
|
136
195
|
return "[INTERNAL_SECRET_FILE]";
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface PreparedActGitWorkspace {
|
|
2
|
+
readonly cwd: string;
|
|
3
|
+
readonly prepared: boolean;
|
|
4
|
+
cleanup(): void;
|
|
5
|
+
}
|
|
6
|
+
interface LinkedWorktreeGitMetadata {
|
|
7
|
+
readonly gitDir: string;
|
|
8
|
+
}
|
|
9
|
+
export declare function prepareActGitWorkspace(cwd: string): PreparedActGitWorkspace;
|
|
10
|
+
export declare function readLinkedWorktreeGitMetadata(cwd: string): LinkedWorktreeGitMetadata | null;
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { cpSync, existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readlinkSync, rmSync, symlinkSync, } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { dirname, join, resolve, sep } from "node:path";
|
|
5
|
+
export function prepareActGitWorkspace(cwd) {
|
|
6
|
+
const sourceCwd = resolve(cwd);
|
|
7
|
+
const linked = readLinkedWorktreeGitMetadata(sourceCwd);
|
|
8
|
+
if (!linked) {
|
|
9
|
+
return {
|
|
10
|
+
cwd: sourceCwd,
|
|
11
|
+
prepared: false,
|
|
12
|
+
cleanup() { },
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
assertExistingDirectory(linked.gitDir, "linked-worktree gitdir");
|
|
16
|
+
const commonDir = resolveGitCommonDir(linked.gitDir);
|
|
17
|
+
assertExistingDirectory(commonDir, "linked-worktree commondir");
|
|
18
|
+
const tempRoot = mkdtempSync(join(tmpdir(), "wp-ci-act-worktree-"));
|
|
19
|
+
const preparedCwd = join(tempRoot, "workspace");
|
|
20
|
+
try {
|
|
21
|
+
const head = git(sourceCwd, ["rev-parse", "HEAD"]);
|
|
22
|
+
git(sourceCwd, ["rev-parse", "--is-inside-work-tree"]);
|
|
23
|
+
execFileSync("git", [
|
|
24
|
+
"clone",
|
|
25
|
+
"--local",
|
|
26
|
+
"--no-hardlinks",
|
|
27
|
+
"--dissociate",
|
|
28
|
+
"--no-checkout",
|
|
29
|
+
sourceCwd,
|
|
30
|
+
preparedCwd,
|
|
31
|
+
], {
|
|
32
|
+
encoding: "utf8",
|
|
33
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
34
|
+
});
|
|
35
|
+
syncOriginRemote(sourceCwd, preparedCwd);
|
|
36
|
+
git(preparedCwd, ["checkout", "--detach", "--quiet", head]);
|
|
37
|
+
overlayWorkingTreeChanges(sourceCwd, preparedCwd);
|
|
38
|
+
assertPreparedGitMetadataIsSelfContained(preparedCwd);
|
|
39
|
+
return {
|
|
40
|
+
cwd: preparedCwd,
|
|
41
|
+
prepared: true,
|
|
42
|
+
cleanup() {
|
|
43
|
+
rmSync(tempRoot, { recursive: true, force: true });
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
rmSync(tempRoot, { recursive: true, force: true });
|
|
49
|
+
throw error;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
export function readLinkedWorktreeGitMetadata(cwd) {
|
|
53
|
+
const gitPath = join(cwd, ".git");
|
|
54
|
+
if (!existsSync(gitPath)) {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
const stat = lstatSync(gitPath);
|
|
58
|
+
if (stat.isDirectory())
|
|
59
|
+
return null;
|
|
60
|
+
if (!stat.isFile()) {
|
|
61
|
+
throw new Error(`Cannot prepare ci act workspace: ${gitPath} is not a file or directory.`);
|
|
62
|
+
}
|
|
63
|
+
const content = readFileSync(gitPath, "utf8").trim();
|
|
64
|
+
const match = /^gitdir:\s*(.+)$/u.exec(content);
|
|
65
|
+
if (!match?.[1]) {
|
|
66
|
+
throw new Error(`Cannot prepare ci act workspace: ${gitPath} is not a valid gitdir file.`);
|
|
67
|
+
}
|
|
68
|
+
return { gitDir: resolve(cwd, match[1]) };
|
|
69
|
+
}
|
|
70
|
+
function resolveGitCommonDir(gitDir) {
|
|
71
|
+
const commonDirPath = join(gitDir, "commondir");
|
|
72
|
+
if (!existsSync(commonDirPath))
|
|
73
|
+
return gitDir;
|
|
74
|
+
const raw = readFileSync(commonDirPath, "utf8").trim();
|
|
75
|
+
if (!raw)
|
|
76
|
+
throw new Error(`Cannot prepare ci act workspace: ${commonDirPath} is empty.`);
|
|
77
|
+
return resolve(gitDir, raw);
|
|
78
|
+
}
|
|
79
|
+
function overlayWorkingTreeChanges(sourceCwd, preparedCwd) {
|
|
80
|
+
const changed = parseGitDiffNameStatus(splitGitNullOutput(gitRaw(sourceCwd, ["diff", "--name-status", "--find-renames", "-z", "HEAD", "--"])));
|
|
81
|
+
const untracked = splitGitNullOutput(gitRaw(sourceCwd, ["ls-files", "--others", "--exclude-standard", "-z"]));
|
|
82
|
+
const copyPaths = new Set([...changed.copyPaths, ...untracked]);
|
|
83
|
+
for (const relativePath of changed.removePaths) {
|
|
84
|
+
assertSafeGitRelativePath(relativePath);
|
|
85
|
+
rmSync(join(preparedCwd, relativePath), { recursive: true, force: true });
|
|
86
|
+
}
|
|
87
|
+
for (const relativePath of copyPaths) {
|
|
88
|
+
assertSafeGitRelativePath(relativePath);
|
|
89
|
+
const sourcePath = join(sourceCwd, relativePath);
|
|
90
|
+
const preparedPath = join(preparedCwd, relativePath);
|
|
91
|
+
const sourceStat = tryLstat(sourcePath);
|
|
92
|
+
if (!sourceStat) {
|
|
93
|
+
rmSync(preparedPath, { recursive: true, force: true });
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
mkdirSync(dirname(preparedPath), { recursive: true });
|
|
97
|
+
if (sourceStat.isSymbolicLink()) {
|
|
98
|
+
rmSync(preparedPath, { recursive: true, force: true });
|
|
99
|
+
symlinkSync(readlinkSync(sourcePath), preparedPath);
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
rmSync(preparedPath, { recursive: true, force: true });
|
|
103
|
+
cpSync(sourcePath, preparedPath, { recursive: true, force: true, errorOnExist: false });
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
function tryLstat(path) {
|
|
107
|
+
try {
|
|
108
|
+
return lstatSync(path);
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
throw error;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
function parseGitDiffNameStatus(entries) {
|
|
118
|
+
const copyPaths = new Set();
|
|
119
|
+
const removePaths = new Set();
|
|
120
|
+
for (let index = 0; index < entries.length;) {
|
|
121
|
+
const status = entries[index++];
|
|
122
|
+
if (!status)
|
|
123
|
+
continue;
|
|
124
|
+
if (status.startsWith("R")) {
|
|
125
|
+
const oldPath = entries[index++];
|
|
126
|
+
const newPath = entries[index++];
|
|
127
|
+
if (oldPath)
|
|
128
|
+
removePaths.add(oldPath);
|
|
129
|
+
if (newPath)
|
|
130
|
+
copyPaths.add(newPath);
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
if (status.startsWith("C")) {
|
|
134
|
+
index++;
|
|
135
|
+
const newPath = entries[index++];
|
|
136
|
+
if (newPath)
|
|
137
|
+
copyPaths.add(newPath);
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
const path = entries[index++];
|
|
141
|
+
if (!path)
|
|
142
|
+
continue;
|
|
143
|
+
if (status.startsWith("D")) {
|
|
144
|
+
removePaths.add(path);
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
copyPaths.add(path);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return { copyPaths, removePaths };
|
|
151
|
+
}
|
|
152
|
+
function syncOriginRemote(sourceCwd, preparedCwd) {
|
|
153
|
+
const originUrl = tryGit(sourceCwd, ["remote", "get-url", "origin"]);
|
|
154
|
+
if (originUrl) {
|
|
155
|
+
git(preparedCwd, ["remote", "set-url", "origin", originUrl]);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
tryGit(preparedCwd, ["remote", "remove", "origin"]);
|
|
159
|
+
}
|
|
160
|
+
function assertPreparedGitMetadataIsSelfContained(preparedCwd) {
|
|
161
|
+
const gitPath = join(preparedCwd, ".git");
|
|
162
|
+
if (!existsSync(gitPath) || !lstatSync(gitPath).isDirectory()) {
|
|
163
|
+
throw new Error("Cannot prepare ci act workspace: prepared .git metadata is not a directory.");
|
|
164
|
+
}
|
|
165
|
+
const commonDir = resolve(preparedCwd, git(preparedCwd, ["rev-parse", "--git-common-dir"]));
|
|
166
|
+
const relative = commonDirRelativeToPrepared(preparedCwd, commonDir);
|
|
167
|
+
if (relative === null) {
|
|
168
|
+
throw new Error(`Cannot prepare ci act workspace: prepared git common dir escaped the execution workspace (${commonDir}).`);
|
|
169
|
+
}
|
|
170
|
+
const alternatesPath = join(commonDir, "objects", "info", "alternates");
|
|
171
|
+
if (existsSync(alternatesPath) && readFileSync(alternatesPath, "utf8").trim().length > 0) {
|
|
172
|
+
throw new Error(`Cannot prepare ci act workspace: prepared git objects still depend on alternates (${alternatesPath}).`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
function commonDirRelativeToPrepared(preparedCwd, commonDir) {
|
|
176
|
+
const normalizedRoot = resolve(preparedCwd);
|
|
177
|
+
const normalizedCommon = resolve(commonDir);
|
|
178
|
+
if (normalizedCommon === normalizedRoot)
|
|
179
|
+
return "";
|
|
180
|
+
if (normalizedCommon.startsWith(`${normalizedRoot}${sep}`)) {
|
|
181
|
+
return normalizedCommon.slice(normalizedRoot.length + 1);
|
|
182
|
+
}
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
function splitGitNullOutput(output) {
|
|
186
|
+
return output.split("\0").filter((entry) => entry.length > 0);
|
|
187
|
+
}
|
|
188
|
+
function assertSafeGitRelativePath(relativePath) {
|
|
189
|
+
if (relativePath.length === 0 ||
|
|
190
|
+
relativePath.startsWith("/") ||
|
|
191
|
+
relativePath === ".." ||
|
|
192
|
+
relativePath.startsWith("../") ||
|
|
193
|
+
relativePath.includes("/../")) {
|
|
194
|
+
throw new Error(`Unsafe git path returned while preparing ci act workspace: ${relativePath}`);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
function assertExistingDirectory(path, label) {
|
|
198
|
+
if (!existsSync(path) || !lstatSync(path).isDirectory()) {
|
|
199
|
+
throw new Error(`Cannot prepare ci act workspace: ${label} is missing or unreadable: ${path}`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
function git(cwd, args) {
|
|
203
|
+
return gitRaw(cwd, args).trim();
|
|
204
|
+
}
|
|
205
|
+
function tryGit(cwd, args) {
|
|
206
|
+
try {
|
|
207
|
+
return git(cwd, args);
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
function gitRaw(cwd, args) {
|
|
214
|
+
return execFileSync("git", [...args], {
|
|
215
|
+
cwd,
|
|
216
|
+
encoding: "utf8",
|
|
217
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
218
|
+
});
|
|
219
|
+
}
|
|
@@ -123,6 +123,9 @@ export const REPO_AUDIT_REGISTRY = {
|
|
|
123
123
|
"secrets-config": async (root) => (await import("#audit/secrets-config")).auditSecretsConfig(root),
|
|
124
124
|
"consumer-agent-kit-dependency": async (root) => (await import("#audit/consumer-agent-kit-dependency")).auditConsumerAgentKitDependency(root),
|
|
125
125
|
"ci-test-perf": async (root) => (await import("#audit/ci-test-perf")).auditCiTestPerf(root),
|
|
126
|
+
"test-scan-perf": async (root, options) => (await import("#audit/test-scan-perf")).auditTestScanPerf(root, {
|
|
127
|
+
affectedFiles: options.affectedFiles,
|
|
128
|
+
}),
|
|
126
129
|
"worktree-main-ownership": async (root) => (await import("#audit/worktree-main-ownership")).auditWorktreeMainOwnership(root),
|
|
127
130
|
sast: async (root, options) => (await import("#audit/sast")).auditSast(root, {
|
|
128
131
|
bin: options.bin,
|
|
@@ -161,6 +161,31 @@ async function reIngestDb(cwd) {
|
|
|
161
161
|
return;
|
|
162
162
|
await reIngestProjection(cwd);
|
|
163
163
|
}
|
|
164
|
+
/**
|
|
165
|
+
* Force reingest of one blueprint document even when the stored content hash
|
|
166
|
+
* still matches. Used on complete so task-table lag cannot block finalize.
|
|
167
|
+
*/
|
|
168
|
+
async function forceReIngestBlueprint(cwd, slug, documentPath) {
|
|
169
|
+
const target = dbPath(cwd);
|
|
170
|
+
if (!existsSync(target))
|
|
171
|
+
return;
|
|
172
|
+
try {
|
|
173
|
+
const conn = openDb(target);
|
|
174
|
+
try {
|
|
175
|
+
// Invalidate hash so ingestAll's skip-if-unchanged path rewrites tasks.
|
|
176
|
+
conn.db.prepare(`UPDATE blueprints SET content_hash = NULL WHERE slug = ?`).run(slug);
|
|
177
|
+
}
|
|
178
|
+
finally {
|
|
179
|
+
conn.close();
|
|
180
|
+
}
|
|
181
|
+
await reIngestProjection(cwd);
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
184
|
+
// Projection is best-effort for complete; markdown gate already passed.
|
|
185
|
+
// Swallow so a broken local projection cannot block lifecycle completion.
|
|
186
|
+
void documentPath;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
164
189
|
/**
|
|
165
190
|
* Update `status:` in YAML frontmatter. Preserves everything else verbatim.
|
|
166
191
|
*/
|
|
@@ -306,7 +331,12 @@ async function promoteBlueprintLocked(cwd, slug, toState) {
|
|
|
306
331
|
throw new Error(`Blueprint "${slug}" not found in any state directory under ${blueprintRoot}`);
|
|
307
332
|
}
|
|
308
333
|
const { dir: currentDir, documentPath: currentDocumentPath, shape, state: currentState } = found;
|
|
309
|
-
// Guard: refuse to complete if any tasks are not done/dropped
|
|
334
|
+
// Guard: refuse to complete if any tasks are not done/dropped.
|
|
335
|
+
// Markdown `_overview.md` is SSOT for task status + verification evidence.
|
|
336
|
+
// The projection DB is derived and can lag when agents edit task Status
|
|
337
|
+
// blocks without going through `task complete` (hash-skipping reingest then
|
|
338
|
+
// leaves stale todo rows). Do not fail completion on projection lag —
|
|
339
|
+
// reingest after the markdown gate so consumers see fresh rows.
|
|
310
340
|
if (toState === "completed") {
|
|
311
341
|
const markdown = readFileSync(currentDocumentPath, "utf8");
|
|
312
342
|
const blueprint = parseBlueprint(markdown, slug);
|
|
@@ -319,23 +349,10 @@ async function promoteBlueprintLocked(cwd, slug, toState) {
|
|
|
319
349
|
throw new Error(`Cannot promote "${slug}" to completed: the following tasks are not done/dropped: ${list}`);
|
|
320
350
|
}
|
|
321
351
|
assertAllTasksHaveCanonicalPassingEvidence(markdown, blueprint.tasks.filter((task) => task.status === "done").map((task) => task.id));
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
try {
|
|
327
|
-
openTasks = conn.db
|
|
328
|
-
.prepare(`SELECT task_id, status FROM tasks WHERE blueprint_slug = ? AND status NOT IN ('done', 'dropped')`)
|
|
329
|
-
.all(slug);
|
|
330
|
-
}
|
|
331
|
-
finally {
|
|
332
|
-
conn.close();
|
|
333
|
-
}
|
|
334
|
-
if (openTasks.length > 0) {
|
|
335
|
-
const list = openTasks.map((t) => `${t.task_id} (${t.status})`).join(", ");
|
|
336
|
-
throw new Error(`Cannot promote "${slug}" to completed: the following tasks are not done: ${list}`);
|
|
337
|
-
}
|
|
338
|
-
}
|
|
352
|
+
// Best-effort projection refresh. Clear content hash first so a lagging
|
|
353
|
+
// tasks table is rewritten even when a prior corrupt UPDATE left the
|
|
354
|
+
// stored hash equal to current markdown.
|
|
355
|
+
await forceReIngestBlueprint(cwd, slug, currentDocumentPath);
|
|
339
356
|
}
|
|
340
357
|
// Platform freshness must be resolved before trust validation so the proof
|
|
341
358
|
// applies to the current replica, not stale local markdown.
|
|
@@ -5,7 +5,9 @@ import { parseBlueprintForDb } from "#db/parser/blueprint-db-parser";
|
|
|
5
5
|
import { blueprintToSpecKit } from "#export/spec-kit/index";
|
|
6
6
|
import { getProjectRoot } from "#cli/utils";
|
|
7
7
|
import { commandExists as defaultCommandExists } from "#runtime/command-exists.js";
|
|
8
|
-
import { clearBlueprintWorktreeOwnership, ensureBlueprintOwnerWorktree, } from "#worktrees/manager.js";
|
|
8
|
+
import { adoptBlueprintOwnerWorktree, clearBlueprintWorktreeOwnership, ensureBlueprintOwnerWorktree, } from "#worktrees/manager.js";
|
|
9
|
+
import { resolveCurrentManagedWorktreeContext } from "#worktrees/current.js";
|
|
10
|
+
import { canonicalizeWorktreePath, WORKTREE_PROBE_TIMEOUT_MS } from "#worktrees/identity.js";
|
|
9
11
|
import { resolveBlueprintRoot } from "#utils/blueprint-root";
|
|
10
12
|
import { getBlueprintDocumentPaths } from "#utils/document-paths.js";
|
|
11
13
|
import { applyBlueprintLifecycleToFile, BlueprintCreationService, BlueprintService, complexitySchema, relativeBlueprintSlug, parseBlueprint, planStatusSchema, runBlueprintAudit, resolveBlueprintFile, serializeBlueprint, validateAllTasksDone, } from "#local";
|
|
@@ -392,7 +394,16 @@ export async function startBlueprint(slug, options = {}) {
|
|
|
392
394
|
if (source.blueprint.status !== "planned") {
|
|
393
395
|
throw new Error(`Blueprint ${source.slug} must be planned before start (current: ${source.blueprint.status}).`);
|
|
394
396
|
}
|
|
395
|
-
const
|
|
397
|
+
const relativeSlug = relativeBlueprintSlug(slug);
|
|
398
|
+
const currentManaged = resolveCurrentManagedWorktreeContext(projectRoot);
|
|
399
|
+
const canonicalProjectRoot = canonicalizeWorktreePath(projectRoot);
|
|
400
|
+
const currentBranch = readCurrentGitBranch(projectRoot);
|
|
401
|
+
const binding = currentManaged &&
|
|
402
|
+
currentManaged.kind === "owner" &&
|
|
403
|
+
currentManaged.worktreePath === canonicalProjectRoot &&
|
|
404
|
+
currentBranch === `bp/${relativeSlug}`
|
|
405
|
+
? adoptBlueprintOwnerWorktree(currentManaged.repoRoot, relativeSlug, currentManaged.worktreePath)
|
|
406
|
+
: ensureBlueprintOwnerWorktree(projectRoot, relativeSlug);
|
|
396
407
|
const result = await applyLifecycleMutation(slug, {
|
|
397
408
|
type: "start",
|
|
398
409
|
worktreeOwnerId: binding.id,
|
|
@@ -404,6 +415,20 @@ export async function startBlueprint(slug, options = {}) {
|
|
|
404
415
|
ownerWorktreePath: binding.path,
|
|
405
416
|
};
|
|
406
417
|
}
|
|
418
|
+
function readCurrentGitBranch(cwd) {
|
|
419
|
+
try {
|
|
420
|
+
const branch = execFileSync("git", ["branch", "--show-current"], {
|
|
421
|
+
cwd,
|
|
422
|
+
encoding: "utf8",
|
|
423
|
+
timeout: WORKTREE_PROBE_TIMEOUT_MS,
|
|
424
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
425
|
+
}).trim();
|
|
426
|
+
return branch.length > 0 ? branch : null;
|
|
427
|
+
}
|
|
428
|
+
catch {
|
|
429
|
+
return null;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
407
432
|
export async function parkBlueprint(slug, options = {}) {
|
|
408
433
|
const projectRoot = resolveProjectRoot(options.projectRoot);
|
|
409
434
|
const result = await applyLifecycleMutation(slug, { type: "park" }, projectRoot);
|
|
@@ -2,12 +2,14 @@ import type { CAC } from "cac";
|
|
|
2
2
|
import type { SecretGateCommandOptions, SecretGateRunResult } from "#secret-gate/runner.js";
|
|
3
3
|
import { type CiActEventName, type CiActExecutionMode } from "#ci/act-runner.js";
|
|
4
4
|
export declare const DEFAULT_CI_ACT_TIMEOUT_MS: number;
|
|
5
|
+
export declare const SMOKE_CI_ACT_TIMEOUT_MS = 30000;
|
|
5
6
|
export declare const MAX_CI_ACT_TIMEOUT_MS: number;
|
|
6
7
|
export declare const CI_COMMAND_HELP: string;
|
|
7
8
|
export interface CiActOptions {
|
|
8
9
|
readonly workflow?: string;
|
|
9
10
|
readonly workflowPath?: string;
|
|
10
11
|
readonly job?: string;
|
|
12
|
+
readonly matrix?: readonly string[];
|
|
11
13
|
readonly eventName?: CiActEventName;
|
|
12
14
|
readonly eventPath?: string;
|
|
13
15
|
readonly envProfile?: string;
|
|
@@ -17,6 +19,7 @@ export interface CiActOptions {
|
|
|
17
19
|
readonly platformImage?: string;
|
|
18
20
|
readonly execute?: boolean;
|
|
19
21
|
readonly timeoutMs?: number;
|
|
22
|
+
readonly smoke?: boolean;
|
|
20
23
|
}
|
|
21
24
|
export interface CiCommandConfig {
|
|
22
25
|
readonly command: string;
|
|
@@ -32,5 +35,9 @@ export declare function registerCiCommand(cli: CAC): void;
|
|
|
32
35
|
export declare function buildCiActCommand(options?: CiActOptions, cwd?: string): CiCommandConfig;
|
|
33
36
|
export declare function validateCiActCommand(..._legacyArgs: readonly unknown[]): string | null;
|
|
34
37
|
export declare function runCiActCommand(options?: CiActOptions, deps?: CiCommandDeps): Promise<number>;
|
|
35
|
-
export declare function normalizeCiActTimeoutMs(value: number | undefined
|
|
38
|
+
export declare function normalizeCiActTimeoutMs(value: number | undefined, options?: {
|
|
39
|
+
readonly smoke?: boolean;
|
|
40
|
+
}): number;
|
|
36
41
|
export declare function parseCiActTimeoutMs(value: unknown): number | undefined;
|
|
42
|
+
export declare function parseCiActExecutionMode(value: unknown): CiActExecutionMode | undefined;
|
|
43
|
+
export declare function parseCiActEventName(value: unknown): CiActEventName | undefined;
|