@webpresso/agent-kit 3.1.26 → 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 (150) hide show
  1. package/catalog/AGENTS.md.tpl +7 -11
  2. package/catalog/agent/commands/verify.md +2 -2
  3. package/catalog/agent/rules/changeset-release.md +4 -2
  4. package/catalog/agent/rules/ci-cost-local-first.md +139 -0
  5. package/catalog/agent/rules/ci-test-perf.md +5 -1
  6. package/catalog/agent/rules/cmd-execution.md +9 -0
  7. package/catalog/agent/rules/pre-implementation.md +2 -1
  8. package/catalog/agent/rules/test-scan-perf.md +80 -0
  9. package/catalog/agent/rules/workflow-skills-routing.md +9 -4
  10. package/catalog/agent/skills/autopilot/SKILL.md +48 -11
  11. package/catalog/agent/skills/claude/SKILL.md +7 -0
  12. package/catalog/agent/skills/codex/SKILL.md +7 -0
  13. package/catalog/agent/skills/fix/SKILL.md +36 -8
  14. package/catalog/agent/skills/grok/SKILL.md +7 -0
  15. package/catalog/agent/skills/investigate/SKILL.md +12 -1
  16. package/catalog/agent/skills/pll/SKILL.md +53 -48
  17. package/catalog/agent/skills/team/SKILL.md +26 -9
  18. package/catalog/agent/skills/ultragoal/SKILL.md +147 -19
  19. package/catalog/agent/skills/verify/SKILL.md +89 -51
  20. package/dist/esm/audit/blueprint-lifecycle-sql.d.ts +8 -0
  21. package/dist/esm/audit/blueprint-lifecycle-sql.js +25 -1
  22. package/dist/esm/audit/blueprint-pr-coverage.js +68 -7
  23. package/dist/esm/audit/registry.d.ts +4 -4
  24. package/dist/esm/audit/registry.js +3 -0
  25. package/dist/esm/audit/roadmap-links.js +6 -0
  26. package/dist/esm/audit/test-scan-perf.d.ts +18 -0
  27. package/dist/esm/audit/test-scan-perf.js +275 -0
  28. package/dist/esm/blueprint/lifecycle/engine.d.ts +9 -0
  29. package/dist/esm/blueprint/lifecycle/engine.js +53 -6
  30. package/dist/esm/blueprint/lifecycle/review-provenance.d.ts +6 -0
  31. package/dist/esm/blueprint/lifecycle/review-provenance.js +93 -12
  32. package/dist/esm/blueprint/markdown/helpers.js +6 -2
  33. package/dist/esm/ci/act-replay.js +110 -7
  34. package/dist/esm/ci/act-runner.d.ts +4 -0
  35. package/dist/esm/ci/act-runner.js +85 -26
  36. package/dist/esm/ci/act-worktree-git.d.ts +11 -0
  37. package/dist/esm/ci/act-worktree-git.js +219 -0
  38. package/dist/esm/ci/native-session-memory-cache.d.ts +9 -1
  39. package/dist/esm/ci/native-session-memory-cache.js +9 -1
  40. package/dist/esm/ci/vitest-ci-shards.d.ts +6 -4
  41. package/dist/esm/ci/vitest-ci-shards.js +6 -4
  42. package/dist/esm/cli/auto-update/guard-tombstone.d.ts +25 -0
  43. package/dist/esm/cli/auto-update/guard-tombstone.js +67 -0
  44. package/dist/esm/cli/auto-update/installer.d.ts +4 -17
  45. package/dist/esm/cli/auto-update/installer.js +6 -57
  46. package/dist/esm/cli/auto-update/self-invocation.d.ts +37 -0
  47. package/dist/esm/cli/auto-update/self-invocation.js +62 -0
  48. package/dist/esm/cli/commands/audit.js +4 -0
  49. package/dist/esm/cli/commands/blueprint/mutations.js +35 -18
  50. package/dist/esm/cli/commands/blueprint/router.js +27 -2
  51. package/dist/esm/cli/commands/ci.d.ts +8 -1
  52. package/dist/esm/cli/commands/ci.js +80 -21
  53. package/dist/esm/cli/commands/dash/index.js +3 -3
  54. package/dist/esm/cli/commands/dash/keymap.js +1 -0
  55. package/dist/esm/cli/commands/dash/launch-preferences.d.ts +15 -0
  56. package/dist/esm/cli/commands/dash/launch-preferences.js +46 -17
  57. package/dist/esm/cli/commands/dash/plan-once-attention.d.ts +32 -0
  58. package/dist/esm/cli/commands/dash/plan-once-attention.js +50 -0
  59. package/dist/esm/cli/commands/dash/plan-once-label.d.ts +7 -0
  60. package/dist/esm/cli/commands/dash/plan-once-label.js +5 -0
  61. package/dist/esm/cli/commands/dash/plan-once-membership.d.ts +67 -0
  62. package/dist/esm/cli/commands/dash/plan-once-membership.js +151 -0
  63. package/dist/esm/cli/commands/dash/plan-once-merge.d.ts +143 -0
  64. package/dist/esm/cli/commands/dash/plan-once-merge.js +238 -0
  65. package/dist/esm/cli/commands/dash/plan-once-schema.d.ts +73 -0
  66. package/dist/esm/cli/commands/dash/plan-once-schema.js +119 -0
  67. package/dist/esm/cli/commands/dash/plan-once.d.ts +142 -0
  68. package/dist/esm/cli/commands/dash/plan-once.js +261 -0
  69. package/dist/esm/cli/commands/dash/tui/attention-hud.d.ts +9 -0
  70. package/dist/esm/cli/commands/dash/tui/attention-hud.js +9 -0
  71. package/dist/esm/cli/commands/dash/tui/chat-bridge.d.ts +41 -0
  72. package/dist/esm/cli/commands/dash/tui/chat-bridge.js +73 -0
  73. package/dist/esm/cli/commands/dash/tui/chat-mode.d.ts +47 -0
  74. package/dist/esm/cli/commands/dash/tui/chat-mode.js +98 -0
  75. package/dist/esm/cli/commands/dash/tui/frame.d.ts +3 -0
  76. package/dist/esm/cli/commands/dash/tui/frame.js +3 -1
  77. package/dist/esm/cli/commands/dash/tui/ipc-protocol.d.ts +87 -0
  78. package/dist/esm/cli/commands/dash/tui/ipc-protocol.js +128 -0
  79. package/dist/esm/cli/commands/dash/tui/native-host.d.ts +34 -0
  80. package/dist/esm/cli/commands/dash/tui/native-host.js +256 -0
  81. package/dist/esm/cli/commands/dash/tui/native-path.d.ts +11 -0
  82. package/dist/esm/cli/commands/dash/tui/native-path.js +41 -0
  83. package/dist/esm/cli/commands/dash/tui/palette.d.ts +1 -1
  84. package/dist/esm/cli/commands/dash/tui/palette.js +14 -0
  85. package/dist/esm/cli/commands/dash/tui/runtime.js +311 -4
  86. package/dist/esm/cli/commands/dash/tui/state.d.ts +41 -0
  87. package/dist/esm/cli/commands/dash/tui/state.js +257 -4
  88. package/dist/esm/cli/commands/format.js +0 -2
  89. package/dist/esm/cli/commands/init/config.d.ts +0 -1
  90. package/dist/esm/cli/commands/init/config.js +1 -4
  91. package/dist/esm/cli/commands/init/scaffolders/agent-hooks/emitters/claude.d.ts +5 -6
  92. package/dist/esm/cli/commands/init/scaffolders/agent-hooks/emitters/claude.js +6 -13
  93. package/dist/esm/cli/commands/init/scaffolders/agent-hooks/ir.d.ts +0 -2
  94. package/dist/esm/cli/commands/init/scaffolders/agent-hooks/ir.js +0 -2
  95. package/dist/esm/cli/commands/init/scaffolders/agent-hooks/merge.d.ts +6 -3
  96. package/dist/esm/cli/commands/init/scaffolders/agent-hooks/merge.js +39 -26
  97. package/dist/esm/cli/commands/init/scaffolders/agent-kit-global/index.js +1 -1
  98. package/dist/esm/cli/commands/init/scaffolders/codex-mcp/index.js +61 -23
  99. package/dist/esm/cli/commands/logs.d.ts +15 -0
  100. package/dist/esm/cli/commands/logs.js +192 -3
  101. package/dist/esm/cli/commands/package-manager.js +2 -1
  102. package/dist/esm/cli/commands/quality-log-store.d.ts +1 -0
  103. package/dist/esm/cli/commands/quality-log-store.js +187 -51
  104. package/dist/esm/cli/commands/review.js +5 -8
  105. package/dist/esm/cli/commands/secrets.d.ts +1 -0
  106. package/dist/esm/cli/commands/secrets.js +4 -1
  107. package/dist/esm/cli/commands/self-install-guard.d.ts +14 -32
  108. package/dist/esm/cli/commands/self-install-guard.js +17 -63
  109. package/dist/esm/cli/direct-provider-launch.js +25 -1
  110. package/dist/esm/hooks/doctor.d.ts +15 -0
  111. package/dist/esm/hooks/doctor.js +56 -0
  112. package/dist/esm/hooks/guard-switch/index.js +8 -4
  113. package/dist/esm/hooks/post-tool/lint-after-edit.js +6 -4
  114. package/dist/esm/hooks/precompact/index.js +5 -3
  115. package/dist/esm/hooks/pretool-guard/validators/forbidden-commands.js +4 -4
  116. package/dist/esm/hooks/pretool-guard/validators/mcp-redirect.d.ts +0 -1
  117. package/dist/esm/hooks/pretool-guard/validators/mcp-redirect.js +8 -5
  118. package/dist/esm/hooks/pretool-guard/validators/worktree-discipline.d.ts +9 -7
  119. package/dist/esm/hooks/pretool-guard/validators/worktree-discipline.js +126 -14
  120. package/dist/esm/hooks/sessionstart/index.d.ts +10 -0
  121. package/dist/esm/hooks/sessionstart/index.js +61 -4
  122. package/dist/esm/hooks/shared/types.d.ts +11 -0
  123. package/dist/esm/hooks/shared/types.js +21 -0
  124. package/dist/esm/mcp/tools/_shared/audit-kinds.d.ts +1 -1
  125. package/dist/esm/mcp/tools/audit.d.ts +2 -1
  126. package/dist/esm/mcp/tools/audit.js +10 -0
  127. package/dist/esm/mcp/tools/audits.d.ts +1 -0
  128. package/dist/esm/mcp/tools/ci-act.js +9 -11
  129. package/dist/esm/paths/state-root.js +9 -0
  130. package/dist/esm/review/authority.js +17 -2
  131. package/dist/esm/review/availability.js +18 -5
  132. package/dist/esm/review/subject.d.ts +15 -0
  133. package/dist/esm/review/subject.js +105 -10
  134. package/dist/esm/runtime/executor.d.ts +1 -0
  135. package/dist/esm/runtime/executor.js +2 -1
  136. package/dist/esm/secret-gate/runner.d.ts +1 -0
  137. package/dist/esm/secret-gate/runner.js +4 -1
  138. package/dist/esm/session-memory/current-session.js +2 -0
  139. package/dist/esm/session-memory/native-runtime.js +5 -3
  140. package/dist/esm/session-memory/native-warm-markers.d.ts +18 -0
  141. package/dist/esm/session-memory/native-warm-markers.js +22 -0
  142. package/dist/esm/session-memory/sync/types.d.ts +4 -0
  143. package/dist/esm/session-memory/types.d.ts +3 -1
  144. package/dist/esm/session-memory/types.js +2 -0
  145. package/dist/esm/ultragoal/runtime.js +44 -2
  146. package/dist/esm/worktrees/current.d.ts +1 -0
  147. package/dist/esm/worktrees/current.js +1 -0
  148. package/dist/esm/worktrees/project-agent-surfaces.d.ts +1 -0
  149. package/dist/esm/worktrees/project-agent-surfaces.js +2 -1
  150. package/package.json +17 -14
@@ -1,9 +1,9 @@
1
1
  import { execFileSync } from "node:child_process";
2
2
  import { existsSync, readFileSync } from "node:fs";
3
3
  import path from "node:path";
4
- import { readReviewAuthorityAtRef } from "#review/authority.js";
5
- import { parseReviewEventLog } from "#review/events.js";
6
- import { normalizePlanSubjectContent } from "#review/subject.js";
4
+ import { readReviewAuthorityAtRef, readWorkingReviewEvents } from "#review/authority.js";
5
+ import { evaluateReviewEvents, parseReviewEventLog } from "#review/events.js";
6
+ import { createPlanSubjectDigestForContent, normalizePlanSubjectContent } from "#review/subject.js";
7
7
  import { isPolicyApprovalVerdict } from "#review/verdict.js";
8
8
  const REVIEW_ENTRY_MARKER = "<!-- wp:review-entry ";
9
9
  function normalizePath(file) {
@@ -52,6 +52,53 @@ function stableSlugForBlueprintFile(file) {
52
52
  }
53
53
  return path.basename(path.dirname(file));
54
54
  }
55
+ /**
56
+ * Read `_overview.md` for a blueprint at `ref`. Prefer the working-tree path;
57
+ * if missing (lifecycle move draft→planned before the promote commit), resolve
58
+ * by stable slug under any lifecycle directory at that ref.
59
+ */
60
+ export function readBlueprintOverviewAtRef(repoRoot, ref, file) {
61
+ const relative = normalizePath(path.relative(repoRoot, file));
62
+ if (relative.startsWith("..") || path.isAbsolute(relative))
63
+ return null;
64
+ try {
65
+ return execFileSync("git", ["show", `${ref}:${relative}`], {
66
+ cwd: repoRoot,
67
+ encoding: "buffer",
68
+ stdio: ["ignore", "pipe", "pipe"],
69
+ timeout: 5_000,
70
+ });
71
+ }
72
+ catch {
73
+ // Path may have moved across lifecycle dirs (draft→planned) while the
74
+ // promote commit is still uncommitted. Stable slug is the authority key.
75
+ const slug = stableSlugForBlueprintFile(file);
76
+ if (!slug || slug.includes(".."))
77
+ return null;
78
+ try {
79
+ const names = execFileSync("git", ["ls-tree", "-r", "--name-only", ref], {
80
+ cwd: repoRoot,
81
+ encoding: "utf8",
82
+ stdio: ["ignore", "pipe", "pipe"],
83
+ timeout: 5_000,
84
+ });
85
+ const escaped = slug.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
86
+ const pattern = new RegExp(`(?:^|/)(?:draft|planned|parked|in-progress|completed|archived)/${escaped}/_overview\\.md$`, "u");
87
+ const hits = names.split("\n").filter((name) => pattern.test(normalizePath(name)));
88
+ if (hits.length !== 1)
89
+ return null;
90
+ return execFileSync("git", ["show", `${ref}:${hits[0]}`], {
91
+ cwd: repoRoot,
92
+ encoding: "buffer",
93
+ stdio: ["ignore", "pipe", "pipe"],
94
+ timeout: 5_000,
95
+ });
96
+ }
97
+ catch {
98
+ return null;
99
+ }
100
+ }
101
+ }
55
102
  function workingContentMatchesRef(repoRoot, file, purpose, ref) {
56
103
  const relative = path.relative(repoRoot, file);
57
104
  if (relative.startsWith("..") || path.isAbsolute(relative))
@@ -63,12 +110,11 @@ function workingContentMatchesRef(repoRoot, file, purpose, ref) {
63
110
  // in the working tree before the promotion commit exists, and that
64
111
  // window must not strip a legitimately promoted blueprint of its plan
65
112
  // authority. Substantive edits still differ after normalization.
66
- const atRef = execFileSync("git", ["show", `${ref}:${normalizePath(relative)}`], {
67
- cwd: repoRoot,
68
- encoding: "buffer",
69
- stdio: ["ignore", "pipe", "pipe"],
70
- timeout: 5_000,
71
- });
113
+ // Also resolve by stable slug when the lifecycle path changed (draft→planned)
114
+ // so pre-commit on a rename does not falsely drop plan authority.
115
+ const atRef = readBlueprintOverviewAtRef(repoRoot, ref, file);
116
+ if (!atRef)
117
+ return false;
72
118
  const working = readFileSync(file);
73
119
  return normalizePlanSubjectContent(working).equals(normalizePlanSubjectContent(atRef));
74
120
  }
@@ -84,27 +130,62 @@ function workingContentMatchesRef(repoRoot, file, purpose, ref) {
84
130
  return false;
85
131
  }
86
132
  }
133
+ // Working-ledger acceptance: pre-commit validates a blueprint whose new review
134
+ // events are staged alongside it and therefore not yet at any git ref. An
135
+ // event still counts only when its subjectDigest matches the digest of the
136
+ // exact working bytes being committed and its artifact hash-verifies on disk —
137
+ // content-addressing keeps this sound before the commit exists.
138
+ function collectWorkingV2ReviewProvenance(file, issues) {
139
+ try {
140
+ const slug = stableSlugForBlueprintFile(file);
141
+ const working = readWorkingReviewEvents(file, slug);
142
+ if (working.events.length === 0)
143
+ return [];
144
+ issues?.push(...working.issues);
145
+ const digest = createPlanSubjectDigestForContent(slug, readFileSync(file));
146
+ const evaluation = evaluateReviewEvents(working.events, {
147
+ purpose: "plan",
148
+ blueprintSlug: slug,
149
+ subjectScheme: "wp-blueprint-v1",
150
+ subjectDigest: digest,
151
+ }, working.compromisedIds);
152
+ return evaluation.approvals
153
+ .filter((event) => event.artifactPath)
154
+ .map((event) => ({
155
+ reviewer: event.reviewer,
156
+ artifact: event.artifactPath,
157
+ targetHash: event.subjectDigest,
158
+ }));
159
+ }
160
+ catch (error) {
161
+ issues?.push(`Working review authority unavailable for ${file}: ${error instanceof Error ? error.message : String(error)}`);
162
+ return [];
163
+ }
164
+ }
87
165
  export function collectV2ReviewProvenance(file, purpose, ref = "HEAD", issues) {
88
166
  const repoRoot = findGitRoot(file);
89
167
  if (!repoRoot)
90
168
  return [];
169
+ const workingApprovals = purpose === "plan" ? collectWorkingV2ReviewProvenance(file, issues) : [];
91
170
  if (!workingContentMatchesRef(repoRoot, file, purpose, ref))
92
- return [];
171
+ return workingApprovals;
93
172
  try {
94
173
  const authority = readReviewAuthorityAtRef(repoRoot, ref, stableSlugForBlueprintFile(file), purpose);
95
174
  issues?.push(...authority.issues);
96
- return authority.approvals.map((event) => ({
175
+ const refApprovals = authority.approvals.map((event) => ({
97
176
  reviewer: event.reviewer,
98
177
  artifact: event.artifactPath,
99
178
  targetHash: event.subjectDigest,
100
179
  }));
180
+ const seen = new Set(refApprovals.map((approval) => approval.reviewer));
181
+ return [...refApprovals, ...workingApprovals.filter((entry) => !seen.has(entry.reviewer))];
101
182
  }
102
183
  catch (error) {
103
184
  // Container-level failure (unparseable ledger, duplicate ids, cross-slug
104
185
  // events, unresolved commit): fail closed for the whole file, but surface
105
186
  // the reason instead of silently reporting zero authority.
106
187
  issues?.push(`Review authority unavailable for ${file}: ${error instanceof Error ? error.message : String(error)}`);
107
- return [];
188
+ return workingApprovals;
108
189
  }
109
190
  }
110
191
  function reviewEventLogsAtRef(cwd, ref) {
@@ -67,10 +67,14 @@ export function updateBlockedReason(content, taskId, reason) {
67
67
  const trimmedReason = reason.trim();
68
68
  const blockedPattern = /\n?\*\*Blocked:\*\*\s*.*(?:\n+)?/i;
69
69
  if (!trimmedReason) {
70
- return section
70
+ // Preserve the section's trailing whitespace: trimEnd on the whole
71
+ // section would also delete the blank line separating this task from the
72
+ // next heading, changing bytes the writer does not own.
73
+ const trailingWhitespace = section.slice(section.trimEnd().length);
74
+ return (section
71
75
  .replace(blockedPattern, "\n")
72
76
  .replace(/\n{3,}/g, "\n\n")
73
- .trimEnd();
77
+ .trimEnd() + trailingWhitespace);
74
78
  }
75
79
  const blockedLine = `**Blocked:** ${trimmedReason}`;
76
80
  if (/\*\*Blocked:\*\*/i.test(section)) {
@@ -31,8 +31,105 @@ export function buildReplayWorkflowSource(sourceYaml, options) {
31
31
  : basename(options.workflowPath);
32
32
  workflow.name = `replay: ${originalName}`;
33
33
  workflow.on = normalizeWorkflowOnSection(workflow.on, options.eventName);
34
+ workflow.jobs = rewriteReplayJobs(workflow.jobs, options.eventName);
34
35
  return stringify(workflow);
35
36
  }
37
+ function rewriteReplayJobs(jobs, eventName) {
38
+ if (typeof jobs !== "object" || jobs === null || Array.isArray(jobs)) {
39
+ return jobs;
40
+ }
41
+ return Object.fromEntries(Object.entries(jobs).map(([jobId, job]) => [
42
+ jobId,
43
+ rewriteReplayJob(job, eventName),
44
+ ]));
45
+ }
46
+ function rewriteReplayJob(job, eventName) {
47
+ if (typeof job !== "object" || job === null || Array.isArray(job)) {
48
+ return job;
49
+ }
50
+ const jobRecord = { ...job };
51
+ if (Array.isArray(jobRecord.steps)) {
52
+ jobRecord.steps = jobRecord.steps.map((step) => rewriteReplayStep(step, eventName));
53
+ }
54
+ return jobRecord;
55
+ }
56
+ function rewriteReplayStep(step, eventName) {
57
+ if (!isRecord(step) || !isSameRepoCheckoutStep(step, eventName)) {
58
+ return step;
59
+ }
60
+ const rewritten = {
61
+ name: typeof step.name === "string" ? step.name : "Use prepared local replay workspace",
62
+ run: [
63
+ 'echo "wp ci act replay: using prepared local workspace instead of actions/checkout"',
64
+ "test -d .git",
65
+ "git rev-parse --is-inside-work-tree",
66
+ "git rev-parse --verify HEAD",
67
+ ].join("\n"),
68
+ "working-directory": "${{ github.workspace }}",
69
+ };
70
+ copyOptionalStepField(step, rewritten, "id");
71
+ copyOptionalStepField(step, rewritten, "if");
72
+ copyOptionalStepField(step, rewritten, "timeout-minutes");
73
+ copyOptionalStepField(step, rewritten, "continue-on-error");
74
+ return rewritten;
75
+ }
76
+ function isSameRepoCheckoutStep(step, eventName) {
77
+ if (typeof step.uses !== "string" || !/^actions\/checkout(?:@|$)/u.test(step.uses)) {
78
+ return false;
79
+ }
80
+ if (step.env !== undefined && step.env !== null) {
81
+ return false;
82
+ }
83
+ const withConfig = step.with;
84
+ if (withConfig === undefined || withConfig === null) {
85
+ return true;
86
+ }
87
+ if (!isRecord(withConfig)) {
88
+ return false;
89
+ }
90
+ if (hasAnyCheckoutInputExcept(withConfig, ["ref"])) {
91
+ return false;
92
+ }
93
+ return isCurrentReplayRef(withConfig.ref, eventName);
94
+ }
95
+ function isCurrentReplayRef(value, eventName) {
96
+ if (value === undefined || value === null || String(value).trim().length === 0) {
97
+ return true;
98
+ }
99
+ const normalized = normalizeGithubExpression(String(value));
100
+ if (normalized === null)
101
+ return false;
102
+ if (normalized === "github.sha") {
103
+ return true;
104
+ }
105
+ if (normalized ===
106
+ "github.event_name=='pull_request'&&github.event.pull_request.base.sha||github.sha") {
107
+ return eventName !== "pull_request";
108
+ }
109
+ return false;
110
+ }
111
+ function normalizeGithubExpression(value) {
112
+ const match = /^\$\{\{\s*([\s\S]*?)\s*\}\}$/u.exec(value.trim());
113
+ if (!match?.[1])
114
+ return null;
115
+ return match[1].replace(/"/gu, "'").replace(/\s+/gu, "");
116
+ }
117
+ function hasAnyCheckoutInputExcept(withConfig, allowedKeys) {
118
+ const allowed = new Set(allowedKeys);
119
+ return Object.entries(withConfig).some(([key, value]) => {
120
+ if (allowed.has(key))
121
+ return false;
122
+ return value !== undefined && value !== null && String(value).trim().length > 0;
123
+ });
124
+ }
125
+ function copyOptionalStepField(source, target, field) {
126
+ if (source[field] !== undefined) {
127
+ target[field] = source[field];
128
+ }
129
+ }
130
+ function isRecord(value) {
131
+ return typeof value === "object" && value !== null && !Array.isArray(value);
132
+ }
36
133
  export function createReplayWorkflow(options) {
37
134
  const sourcePath = resolve(options.cwd, options.workflowPath);
38
135
  const sourceYaml = readFileSync(sourcePath, "utf8");
@@ -42,11 +139,17 @@ export function createReplayWorkflow(options) {
42
139
  });
43
140
  const dir = mkdtempSync(join(tmpdir(), "wp-ci-act-replay-"));
44
141
  const workflowPath = join(dir, "workflow.yml");
45
- writeFileSync(workflowPath, generatedYaml, "utf8");
46
- return {
47
- workflowPath,
48
- cleanup() {
49
- rmSync(dir, { recursive: true, force: true });
50
- },
51
- };
142
+ try {
143
+ writeFileSync(workflowPath, generatedYaml, "utf8");
144
+ return {
145
+ workflowPath,
146
+ cleanup() {
147
+ rmSync(dir, { recursive: true, force: true });
148
+ },
149
+ };
150
+ }
151
+ catch (error) {
152
+ rmSync(dir, { recursive: true, force: true });
153
+ throw error;
154
+ }
52
155
  }
@@ -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 {};