@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.
Files changed (98) hide show
  1. package/catalog/AGENTS.md.tpl +4 -4
  2. package/catalog/agent/rules/ci-cost-local-first.md +139 -0
  3. package/catalog/agent/rules/ci-test-perf.md +5 -1
  4. package/catalog/agent/rules/cmd-execution.md +9 -0
  5. package/catalog/agent/rules/pre-implementation.md +2 -1
  6. package/catalog/agent/rules/test-scan-perf.md +80 -0
  7. package/catalog/agent/rules/workflow-skills-routing.md +4 -2
  8. package/catalog/agent/skills/fix/SKILL.md +3 -0
  9. package/catalog/agent/skills/verify/SKILL.md +18 -0
  10. package/dist/esm/audit/blueprint-pr-coverage.js +68 -7
  11. package/dist/esm/audit/registry.d.ts +4 -4
  12. package/dist/esm/audit/registry.js +3 -0
  13. package/dist/esm/audit/roadmap-links.js +6 -0
  14. package/dist/esm/audit/test-scan-perf.d.ts +18 -0
  15. package/dist/esm/audit/test-scan-perf.js +275 -0
  16. package/dist/esm/blueprint/lifecycle/engine.d.ts +9 -0
  17. package/dist/esm/blueprint/lifecycle/engine.js +53 -6
  18. package/dist/esm/blueprint/lifecycle/review-provenance.d.ts +6 -0
  19. package/dist/esm/blueprint/lifecycle/review-provenance.js +93 -12
  20. package/dist/esm/blueprint/markdown/helpers.js +6 -2
  21. package/dist/esm/ci/act-replay.js +110 -7
  22. package/dist/esm/ci/act-runner.d.ts +4 -0
  23. package/dist/esm/ci/act-runner.js +85 -26
  24. package/dist/esm/ci/act-worktree-git.d.ts +11 -0
  25. package/dist/esm/ci/act-worktree-git.js +219 -0
  26. package/dist/esm/cli/commands/audit.js +3 -0
  27. package/dist/esm/cli/commands/blueprint/mutations.js +35 -18
  28. package/dist/esm/cli/commands/blueprint/router.js +27 -2
  29. package/dist/esm/cli/commands/ci.d.ts +8 -1
  30. package/dist/esm/cli/commands/ci.js +80 -21
  31. package/dist/esm/cli/commands/dash/keymap.js +1 -0
  32. package/dist/esm/cli/commands/dash/launch-preferences.d.ts +15 -0
  33. package/dist/esm/cli/commands/dash/launch-preferences.js +46 -17
  34. package/dist/esm/cli/commands/dash/plan-once-label.d.ts +7 -0
  35. package/dist/esm/cli/commands/dash/plan-once-label.js +5 -0
  36. package/dist/esm/cli/commands/dash/plan-once-membership.d.ts +67 -0
  37. package/dist/esm/cli/commands/dash/plan-once-membership.js +151 -0
  38. package/dist/esm/cli/commands/dash/plan-once-merge.d.ts +143 -0
  39. package/dist/esm/cli/commands/dash/plan-once-merge.js +238 -0
  40. package/dist/esm/cli/commands/dash/plan-once.d.ts +22 -2
  41. package/dist/esm/cli/commands/dash/plan-once.js +41 -4
  42. package/dist/esm/cli/commands/dash/tui/attention-hud.d.ts +9 -0
  43. package/dist/esm/cli/commands/dash/tui/attention-hud.js +9 -0
  44. package/dist/esm/cli/commands/dash/tui/chat-bridge.d.ts +41 -0
  45. package/dist/esm/cli/commands/dash/tui/chat-bridge.js +73 -0
  46. package/dist/esm/cli/commands/dash/tui/chat-mode.d.ts +47 -0
  47. package/dist/esm/cli/commands/dash/tui/chat-mode.js +98 -0
  48. package/dist/esm/cli/commands/dash/tui/frame.d.ts +3 -0
  49. package/dist/esm/cli/commands/dash/tui/frame.js +3 -1
  50. package/dist/esm/cli/commands/dash/tui/palette.d.ts +1 -1
  51. package/dist/esm/cli/commands/dash/tui/palette.js +14 -0
  52. package/dist/esm/cli/commands/dash/tui/runtime.js +239 -4
  53. package/dist/esm/cli/commands/dash/tui/state.d.ts +41 -0
  54. package/dist/esm/cli/commands/dash/tui/state.js +257 -4
  55. package/dist/esm/cli/commands/format.js +0 -2
  56. package/dist/esm/cli/commands/init/config.d.ts +0 -1
  57. package/dist/esm/cli/commands/init/config.js +1 -4
  58. package/dist/esm/cli/commands/init/scaffolders/agent-hooks/emitters/claude.d.ts +5 -6
  59. package/dist/esm/cli/commands/init/scaffolders/agent-hooks/emitters/claude.js +6 -13
  60. package/dist/esm/cli/commands/init/scaffolders/agent-hooks/ir.d.ts +0 -2
  61. package/dist/esm/cli/commands/init/scaffolders/agent-hooks/ir.js +0 -2
  62. package/dist/esm/cli/commands/init/scaffolders/agent-hooks/merge.d.ts +6 -3
  63. package/dist/esm/cli/commands/init/scaffolders/agent-hooks/merge.js +39 -26
  64. package/dist/esm/cli/commands/init/scaffolders/codex-mcp/index.js +61 -23
  65. package/dist/esm/cli/commands/logs.d.ts +15 -0
  66. package/dist/esm/cli/commands/logs.js +192 -3
  67. package/dist/esm/cli/commands/quality-log-store.d.ts +1 -0
  68. package/dist/esm/cli/commands/quality-log-store.js +187 -51
  69. package/dist/esm/cli/commands/review.js +5 -8
  70. package/dist/esm/cli/commands/secrets.d.ts +1 -0
  71. package/dist/esm/cli/commands/secrets.js +4 -1
  72. package/dist/esm/cli/direct-provider-launch.js +25 -1
  73. package/dist/esm/hooks/doctor.d.ts +15 -0
  74. package/dist/esm/hooks/doctor.js +56 -0
  75. package/dist/esm/hooks/pretool-guard/validators/forbidden-commands.js +4 -4
  76. package/dist/esm/hooks/pretool-guard/validators/mcp-redirect.d.ts +0 -1
  77. package/dist/esm/hooks/pretool-guard/validators/mcp-redirect.js +8 -5
  78. package/dist/esm/hooks/pretool-guard/validators/worktree-discipline.d.ts +9 -7
  79. package/dist/esm/hooks/pretool-guard/validators/worktree-discipline.js +126 -14
  80. package/dist/esm/mcp/tools/_shared/audit-kinds.d.ts +1 -1
  81. package/dist/esm/mcp/tools/audit.d.ts +2 -1
  82. package/dist/esm/mcp/tools/audit.js +10 -0
  83. package/dist/esm/mcp/tools/audits.d.ts +1 -0
  84. package/dist/esm/mcp/tools/ci-act.js +9 -11
  85. package/dist/esm/paths/state-root.js +3 -0
  86. package/dist/esm/review/authority.js +17 -2
  87. package/dist/esm/review/availability.js +18 -5
  88. package/dist/esm/review/subject.d.ts +15 -0
  89. package/dist/esm/review/subject.js +105 -10
  90. package/dist/esm/runtime/executor.d.ts +1 -0
  91. package/dist/esm/runtime/executor.js +2 -1
  92. package/dist/esm/secret-gate/runner.d.ts +1 -0
  93. package/dist/esm/secret-gate/runner.js +4 -1
  94. package/dist/esm/worktrees/current.d.ts +1 -0
  95. package/dist/esm/worktrees/current.js +1 -0
  96. package/dist/esm/worktrees/project-agent-surfaces.d.ts +1 -0
  97. package/dist/esm/worktrees/project-agent-surfaces.js +2 -1
  98. 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: true,
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
- const mode = resolveCiActExecutionMode(options);
69
- const replay = mode === "replay"
70
- ? createReplayWorkflow({
71
- cwd,
72
- workflowPath: resolveCiActWorkflowPath(options),
73
- eventName: options.eventName ?? "pull_request",
74
- })
75
- : null;
76
- const actArgs = buildPublicCiActArgs({ ...options, cwd }, replay ? replay.workflowPath : undefined);
77
- const secretEnvProfile = resolveCiActSecretEnvProfile({ ...options, cwd }) ?? "preview";
78
- const wrapped = buildSecretGateCommand({
79
- sink: "act",
80
- profile: secretEnvProfile,
81
- envProfile: options.envProfile ?? "secrets-only",
82
- forceSecretGate: true,
83
- command: "act",
84
- args: actArgs,
85
- });
86
- return {
87
- command: { command: wrapped.command, args: wrapped.args, actArgs },
88
- mode,
89
- nonSecurityEquivalent: mode === "replay",
90
- cleanup() {
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
- const target = dbPath(cwd);
323
- if (existsSync(target)) {
324
- const conn = openDb(target);
325
- let openTasks;
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 binding = ensureBlueprintOwnerWorktree(projectRoot, relativeBlueprintSlug(slug));
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): number;
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;