@fitlab-ai/agent-infra 0.9.0 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/README.md +6 -1
  2. package/README.zh-CN.md +5 -1
  3. package/dist/bin/internal-cli.js +5 -0
  4. package/dist/lib/internal/platform-release-notes.js +53 -0
  5. package/dist/lib/internal/release-workflow.js +105 -2
  6. package/dist/lib/platform/github-release-notes.js +96 -0
  7. package/dist/lib/platform/issues.js +4 -2
  8. package/dist/lib/platform/merged-pr-equivalence.js +54 -0
  9. package/dist/lib/platform/pr-checks.js +6 -6
  10. package/dist/lib/platform/pull-requests.js +3 -1
  11. package/dist/lib/platform/release-notes.js +143 -0
  12. package/dist/lib/platform/verification-required.js +29 -5
  13. package/dist/lib/sandbox/build-proxy.js +63 -0
  14. package/dist/lib/sandbox/commands/create.js +57 -9
  15. package/dist/lib/sandbox/commands/rebuild.js +24 -7
  16. package/dist/lib/sandbox/image-build.js +2 -1
  17. package/dist/lib/sandbox/runtimes/ai-tools.dockerfile +24 -3
  18. package/dist/lib/sandbox/runtimes/base.dockerfile +8 -1
  19. package/dist/lib/sandbox/runtimes/java17.dockerfile +8 -1
  20. package/dist/lib/sandbox/runtimes/java21.dockerfile +8 -1
  21. package/dist/lib/sandbox/runtimes/node20.dockerfile +8 -1
  22. package/dist/lib/sandbox/runtimes/node22.dockerfile +8 -1
  23. package/dist/lib/sandbox/runtimes/python3.dockerfile +8 -1
  24. package/dist/lib/sandbox/shell.js +2 -1
  25. package/dist/lib/task/commands/issue-body.js +10 -5
  26. package/dist/lib/task/events.js +1 -1
  27. package/dist/lib/task/issue-form.js +2 -0
  28. package/dist/lib/task/review-fingerprint.js +9 -0
  29. package/dist/lib/task/verification-engine.js +34 -17
  30. package/dist/lib/task/verification-review.js +1 -1
  31. package/dist/lib/task/verification.js +1 -1
  32. package/lib/internal/platform-release-notes.ts +55 -0
  33. package/lib/internal/release-workflow.ts +105 -3
  34. package/lib/platform/github-release-notes.ts +132 -0
  35. package/lib/platform/issues.ts +6 -4
  36. package/lib/platform/merged-pr-equivalence.ts +70 -0
  37. package/lib/platform/pr-checks.ts +8 -7
  38. package/lib/platform/pull-requests.ts +9 -3
  39. package/lib/platform/release-notes.ts +167 -0
  40. package/lib/platform/verification-required.ts +31 -5
  41. package/lib/sandbox/build-proxy.ts +94 -0
  42. package/lib/sandbox/commands/create.ts +77 -11
  43. package/lib/sandbox/commands/rebuild.ts +29 -7
  44. package/lib/sandbox/image-build.ts +4 -1
  45. package/lib/sandbox/runtimes/ai-tools.dockerfile +24 -3
  46. package/lib/sandbox/runtimes/base.dockerfile +8 -1
  47. package/lib/sandbox/runtimes/java17.dockerfile +8 -1
  48. package/lib/sandbox/runtimes/java21.dockerfile +8 -1
  49. package/lib/sandbox/runtimes/node20.dockerfile +8 -1
  50. package/lib/sandbox/runtimes/node22.dockerfile +8 -1
  51. package/lib/sandbox/runtimes/python3.dockerfile +8 -1
  52. package/lib/sandbox/shell.ts +3 -1
  53. package/lib/task/commands/issue-body.ts +10 -5
  54. package/lib/task/events.ts +1 -1
  55. package/lib/task/issue-form.ts +4 -0
  56. package/lib/task/review-fingerprint.ts +10 -1
  57. package/lib/task/verification-engine.ts +33 -17
  58. package/lib/task/verification-review.ts +1 -0
  59. package/lib/task/verification.ts +1 -1
  60. package/package.json +1 -1
  61. package/templates/.agents/rules/release-commands.en.md +2 -2
  62. package/templates/.agents/rules/release-commands.github.en.md +10 -40
  63. package/templates/.agents/rules/release-commands.github.zh-CN.md +9 -39
  64. package/templates/.agents/rules/release-commands.zh-CN.md +2 -2
  65. package/templates/.agents/rules/review-handshake.en.md +2 -1
  66. package/templates/.agents/rules/review-handshake.zh-CN.md +2 -1
  67. package/templates/.agents/rules/testing-discipline.en.md +20 -2
  68. package/templates/.agents/rules/testing-discipline.zh-CN.md +20 -2
  69. package/templates/.agents/skills/analyze-task/SKILL.en.md +4 -1
  70. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +4 -1
  71. package/templates/.agents/skills/complete-task/SKILL.en.md +3 -3
  72. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +3 -3
  73. package/templates/.agents/skills/complete-task/config/verify.en.json +0 -1
  74. package/templates/.agents/skills/complete-task/config/verify.zh-CN.json +0 -1
  75. package/templates/.agents/skills/create-release-note/SKILL.en.md +17 -38
  76. package/templates/.agents/skills/create-release-note/SKILL.zh-CN.md +16 -37
  77. package/templates/.agents/skills/create-task/SKILL.en.md +4 -0
  78. package/templates/.agents/skills/create-task/SKILL.zh-CN.md +4 -0
  79. package/templates/.agents/skills/create-task/reference/context-capture.en.md +29 -0
  80. package/templates/.agents/skills/create-task/reference/context-capture.zh-CN.md +29 -0
  81. package/templates/.agents/templates/task.en.md +18 -0
  82. package/templates/.agents/templates/task.zh-CN.md +18 -0
@@ -3,6 +3,8 @@ import path from "node:path";
3
3
  import { execFileSync } from "node:child_process";
4
4
  import { inspectRequiredChecks } from "./pr-checks.js";
5
5
  import { resolvePlatformContext } from "./context.js";
6
+ import { resolveReviewedHeadRelation } from "./merged-pr-equivalence.js";
7
+ import { loadPostReviewConfig, resolvePostReviewGlobs } from "../task/review-fingerprint.js";
6
8
  const CHECK_TYPE = "required-checks";
7
9
  const SHA_PATTERN = /^[0-9a-f]{7,40}$/i;
8
10
  function validPrNumber(value) {
@@ -44,13 +46,28 @@ export function evaluateRequiredChecks(context, shared) {
44
46
  if (inspection.status === "failed") {
45
47
  return shared.failResult(CHECK_TYPE, inspection.error?.message || "Required checks are unavailable", "check_failed");
46
48
  }
47
- const prHead = inspection.pullRequest?.headSha;
48
- if (!prHead || localHead !== reviewedHead || localHead !== prHead) {
49
- return shared.failResult(CHECK_TYPE, "Local HEAD, last reviewed commit, and PR head must match", "check_failed");
49
+ const prHead = inspection.pullRequest?.head?.sha || inspection.pullRequest?.headSha;
50
+ const relation = context.relation || (context.gitRoot && inspection.pullRequest?.head
51
+ ? resolveReviewedHeadRelation({
52
+ gitRoot: context.gitRoot,
53
+ localHead,
54
+ lastReviewedCommit: reviewedHead,
55
+ pullRequest: inspection.pullRequest,
56
+ pathspecs: context.pathspecs || [":/"]
57
+ })
58
+ : localHead === reviewedHead && localHead === prHead
59
+ ? { status: "strict" }
60
+ : { status: "failed" });
61
+ if (relation.status !== "strict" && relation.status !== "merged-equivalent") {
62
+ const message = relation.message || "Local HEAD, last reviewed commit, and PR head must match";
63
+ return relation.status === "blocked"
64
+ ? shared.blockedResult(CHECK_TYPE, message, "dependency_error")
65
+ : shared.failResult(CHECK_TYPE, message, "check_failed");
50
66
  }
51
67
  const state = inspection.checks?.state;
52
68
  if (state === "passed" || state === "no-required") {
53
- return shared.passResult(CHECK_TYPE, `Required checks are ${state} for PR head ${prHead}`);
69
+ const suffix = relation.status === "merged-equivalent" ? " via verified squash merge equivalence" : "";
70
+ return shared.passResult(CHECK_TYPE, `Required checks are ${state} for PR head ${prHead}${suffix}`);
54
71
  }
55
72
  if (state === "pending") {
56
73
  return shared.blockedResult(CHECK_TYPE, inspection.error?.message || "Required checks are pending", "check_pending");
@@ -73,6 +90,13 @@ export function check({ taskDir }, shared) {
73
90
  if (!localHead)
74
91
  return evaluateRequiredChecks({ metadata: task.metadata, localHead, inspection: null, prFlow }, shared);
75
92
  const inspection = inspectRequiredChecks(task.metadata.id, { cwd: shared.repoRoot });
76
- return evaluateRequiredChecks({ metadata: task.metadata, localHead, inspection, prFlow }, shared);
93
+ return evaluateRequiredChecks({
94
+ metadata: task.metadata,
95
+ localHead,
96
+ inspection,
97
+ prFlow,
98
+ gitRoot: shared.repoRoot,
99
+ pathspecs: resolvePostReviewGlobs({}, loadPostReviewConfig(shared.repoRoot))
100
+ }, shared);
77
101
  }
78
102
  //# sourceMappingURL=verification-required.js.map
@@ -0,0 +1,63 @@
1
+ import semver from 'semver';
2
+ import { engineDisplayName } from './engine.js';
3
+ import { runSafeEngine } from './shell.js';
4
+ export const BUILD_PROXY_ENV_KEYS = ['HTTP_PROXY', 'HTTPS_PROXY', 'NO_PROXY'];
5
+ export const MIN_BUILD_PROXY_DOCKER_VERSION = '20.10.0';
6
+ export const MIN_BUILD_PROXY_BUILDKIT_VERSION = '0.9.0';
7
+ export function prepareBuildProxy(enabled, hostEnv, engine) {
8
+ const env = { ...hostEnv };
9
+ if (!enabled) {
10
+ return { args: [], env, redactionValues: [] };
11
+ }
12
+ const entries = BUILD_PROXY_ENV_KEYS.flatMap((key) => {
13
+ const value = hostEnv[key]?.trim();
14
+ return value ? [[key, value]] : [];
15
+ });
16
+ if (entries.length === 0) {
17
+ throw new Error(`No non-empty build proxy variables are set. Configure one of: ${BUILD_PROXY_ENV_KEYS.join(', ')}.`);
18
+ }
19
+ for (const [key, value] of entries)
20
+ env[key] = value;
21
+ if (engine === 'wsl2') {
22
+ const existing = (env.WSLENV ?? '').split(':').filter(Boolean);
23
+ env.WSLENV = [...new Set([...existing, ...entries.map(([key]) => key)])].join(':');
24
+ }
25
+ return {
26
+ args: entries.flatMap(([key]) => ['--secret', `id=${key},env=${key}`]),
27
+ env,
28
+ redactionValues: entries.map(([, value]) => value)
29
+ };
30
+ }
31
+ function parsedVersion(raw) {
32
+ return semver.coerce(raw.trim())?.version ?? null;
33
+ }
34
+ export function assertBuildProxyCompatibility(engine, runSafeEngineFn = runSafeEngine) {
35
+ const dockerRaw = runSafeEngineFn(engine, 'docker', ['version', '--format', '{{.Server.Version}}']);
36
+ const dockerVersion = parsedVersion(dockerRaw);
37
+ if (!dockerVersion || !semver.gte(dockerVersion, MIN_BUILD_PROXY_DOCKER_VERSION)) {
38
+ throw new Error(`Build proxy requires Docker Engine ${MIN_BUILD_PROXY_DOCKER_VERSION} or newer; `
39
+ + `observed ${dockerVersion ?? 'an unparseable version'}.`);
40
+ }
41
+ const buildxRaw = runSafeEngineFn(engine, 'docker', ['buildx', 'inspect', '--bootstrap']);
42
+ const versions = [...buildxRaw.matchAll(/BuildKit(?:\s+version)?:\s*v?(\d+\.\d+\.\d+(?:[-+][^\s]+)?)/gi)]
43
+ .map((match) => parsedVersion(match[1] ?? ''))
44
+ .filter((version) => version !== null);
45
+ if (versions.length === 0) {
46
+ throw new Error(`Build proxy requires BuildKit ${MIN_BUILD_PROXY_BUILDKIT_VERSION} or newer, `
47
+ + 'but the active builder version could not be determined.');
48
+ }
49
+ const oldVersion = versions.find((version) => !semver.gte(version, MIN_BUILD_PROXY_BUILDKIT_VERSION));
50
+ if (oldVersion) {
51
+ throw new Error(`Build proxy observed BuildKit ${oldVersion}; version ${MIN_BUILD_PROXY_BUILDKIT_VERSION} or newer is required.`);
52
+ }
53
+ }
54
+ export function redactBuildProxyValues(text, values) {
55
+ return [...new Set(values.filter(Boolean))]
56
+ .sort((left, right) => right.length - left.length)
57
+ .reduce((result, value) => result.replaceAll(value, '[REDACTED_BUILD_PROXY]'), text);
58
+ }
59
+ export function buildProxyFailureHint(engine) {
60
+ return `Build-step proxy inheritance is enabled for ${engineDisplayName(engine)}. `
61
+ + 'Image pulls and Dockerfile FROM resolution use the Docker daemon or builder proxy configuration.';
62
+ }
63
+ //# sourceMappingURL=build-proxy.js.map
@@ -10,6 +10,7 @@ import * as toml from 'smol-toml';
10
10
  import { loadConfig } from '../config.js';
11
11
  import { assertValidBranchName, containerName, containerNameCandidates, parsePositiveIntegerOption, sandboxBranchLabel, sandboxImageConfigLabel, sandboxImageRefreshLabel, sandboxLabel, shareBranchDir, shareCommonDir, shellConfigDir, worktreeDirCandidates } from '../constants.js';
12
12
  import { prepareDockerfile } from '../dockerfile.js';
13
+ import { assertBuildProxyCompatibility, buildProxyFailureHint, prepareBuildProxy, redactBuildProxyValues } from '../build-proxy.js';
13
14
  import { detectEngine, ensureDocker } from '../engine.js';
14
15
  import { commandForEngine, execEngine, run, runEngine, runOk, runOkEngine, runSafe, runSafeEngine, runVerboseEngine } from '../shell.js';
15
16
  import { resolveTaskBranch } from '../task-resolver.js';
@@ -49,7 +50,7 @@ alias gy='gemini --yolo; tput ed'
49
50
  `;
50
51
  const CONTAINER_HOME = '/home/devuser';
51
52
  const CONTAINER_SHELL_CONFIG_MOUNT = `${CONTAINER_HOME}/.host-shell-config`;
52
- const USAGE = `Usage: ai sandbox create <branch> [base] [--cpu <n>] [--memory <n>] [--no-refresh] [--inherit-proxy|-P]
53
+ const USAGE = `Usage: ai sandbox create <branch> [base] [--cpu <n>] [--memory <n>] [--no-refresh] [--inherit-proxy|-P] [--inherit-build-proxy|-B]
53
54
 
54
55
  Host aliases:
55
56
  ${'~'}/.agent-infra/aliases/sandbox.sh is auto-created on first run and exposed
@@ -59,7 +60,9 @@ Host aliases:
59
60
 
60
61
  Proxy:
61
62
  --inherit-proxy, -P Copy non-empty standard host proxy variables into the
62
- container environment through the private env file.`;
63
+ container environment through the private env file.
64
+ --inherit-build-proxy, -B Pass uppercase HTTP_PROXY, HTTPS_PROXY, and NO_PROXY
65
+ to managed image build steps for this invocation.`;
63
66
  const HOST_PROXY_ENV_KEYS = [
64
67
  'http_proxy',
65
68
  'HTTP_PROXY',
@@ -70,6 +73,15 @@ const HOST_PROXY_ENV_KEYS = [
70
73
  'no_proxy',
71
74
  'NO_PROXY'
72
75
  ];
76
+ const MANAGED_GITHUB_GIT_CONFIG = [
77
+ '[credential]',
78
+ '\thelper = !gh auth git-credential',
79
+ '[credential "https://github.com"]',
80
+ '\thelper =',
81
+ '\thelper = !gh auth git-credential',
82
+ '[url "https://github.com/"]',
83
+ '\tinsteadOf = git@github.com:'
84
+ ];
73
85
  function resolveToolDirs(config, tools, branch) {
74
86
  return tools.map((tool) => {
75
87
  const candidates = toolConfigDirCandidates(tool, config.project, branch);
@@ -158,6 +170,14 @@ function normalizeContainerHomeSeparators(content) {
158
170
  const containerHomePattern = new RegExp(`${escapeRegExp(CONTAINER_HOME)}\\S*`, 'g');
159
171
  return content.replace(containerHomePattern, (value) => value.replaceAll('\\', '/'));
160
172
  }
173
+ function isGitHubCliCredentialHelper(line) {
174
+ const helperMatch = line.match(/^\s*helper\s*=\s*(.+?)\s*$/i);
175
+ if (!helperMatch) {
176
+ return false;
177
+ }
178
+ const helper = (helperMatch[1] ?? '').trim().replace(/^"(.*)"$/, '$1');
179
+ return /^!\s*(?:"[^"]*[\\/]+gh"|(?:\S*[\\/]+)?gh)\s+auth\s+git-credential(?:\s|$)/i.test(helper);
180
+ }
161
181
  export function sanitizeGitConfig(gitconfig, home, { stripGpg = false, repoRoot = '' } = {}) {
162
182
  const posixHome = home.replaceAll('\\', '/');
163
183
  const normalizedGitconfig = gitconfig
@@ -170,10 +190,12 @@ export function sanitizeGitConfig(gitconfig, home, { stripGpg = false, repoRoot
170
190
  const sanitized = [];
171
191
  let inGpgSection = false;
172
192
  let currentSection = '';
193
+ let currentSectionName = '';
173
194
  for (const line of lines) {
174
195
  const sectionMatch = line.match(/^\s*\[([^\]]+)\]\s*$/);
175
196
  if (sectionMatch) {
176
197
  const sectionName = (sectionMatch[1] ?? '').trim();
198
+ currentSectionName = sectionName;
177
199
  currentSection = ((sectionName.match(/^([^\s"]+)/)?.[1]) ?? '').toLowerCase();
178
200
  inGpgSection = /^gpg(?:\s+"[^"]+")?$/i.test(sectionName);
179
201
  if (stripGpg && inGpgSection) {
@@ -199,9 +221,19 @@ export function sanitizeGitConfig(gitconfig, home, { stripGpg = false, repoRoot
199
221
  if (stripGpg && currentSection === 'user' && /^\s*signingKey\s*=.*$/i.test(line)) {
200
222
  continue;
201
223
  }
224
+ if (currentSection === 'credential' && isGitHubCliCredentialHelper(line)) {
225
+ continue;
226
+ }
227
+ if (/^url\s+"https:\/\/github\.com\/"$/i.test(currentSectionName)
228
+ && /^\s*insteadOf\s*=\s*git@github\.com:\s*$/i.test(line)) {
229
+ continue;
230
+ }
202
231
  sanitized.push(line);
203
232
  }
204
- return appendSafeDirectories(sanitized, repoRoot).join('\n');
233
+ return [
234
+ ...appendSafeDirectories(sanitized, repoRoot),
235
+ ...MANAGED_GITHUB_GIT_CONFIG
236
+ ].join('\n');
205
237
  }
206
238
  export function hostHasGpgKeys(home, execFn = execFileSync) {
207
239
  return currentKeyringFingerprint(home, execFn) !== null;
@@ -928,7 +960,7 @@ export function buildTmpfsRunArgs(containerMount, tmpfs) {
928
960
  const size = tmpfs.size ?? '512m';
929
961
  return ['--tmpfs', `${containerMount}:rw,size=${size}`];
930
962
  }
931
- export function buildImage(config, tools, dockerfilePath, imageSignature, { engine, runFn = runEngine, runSafeFn = runSafeEngine, runVerboseFn = runVerboseEngine, env = process.env, refresh = false, lastRefresh } = {}) {
963
+ export function buildImage(config, tools, dockerfilePath, imageSignature, { engine, runFn = runEngine, runSafeFn = runSafeEngine, runVerboseFn = runVerboseEngine, env = process.env, refresh = false, lastRefresh, buildProxyArgs = [], buildEnv } = {}) {
932
964
  const selectedEngine = engine ?? detectEngine({ engine: config.engine });
933
965
  runVerboseFn(selectedEngine, 'docker', buildSandboxImageArgs(config, tools, dockerfilePath, imageSignature, {
934
966
  engine: selectedEngine,
@@ -936,8 +968,9 @@ export function buildImage(config, tools, dockerfilePath, imageSignature, { engi
936
968
  runSafeFn,
937
969
  env,
938
970
  refresh,
939
- lastRefresh
940
- }), { cwd: config.repoRoot });
971
+ lastRefresh,
972
+ buildProxyArgs
973
+ }), { cwd: config.repoRoot, env: buildEnv });
941
974
  }
942
975
  function readImageLabels(config, engine) {
943
976
  return parseImageLabels(runSafeEngine(engine, 'docker', [
@@ -958,6 +991,7 @@ export async function create(args) {
958
991
  memory: { type: 'string' },
959
992
  'no-refresh': { type: 'boolean' },
960
993
  'inherit-proxy': { type: 'boolean', short: 'P' },
994
+ 'inherit-build-proxy': { type: 'boolean', short: 'B' },
961
995
  help: { type: 'boolean', short: 'h' }
962
996
  }
963
997
  });
@@ -1027,6 +1061,12 @@ export async function create(args) {
1027
1061
  && isRefreshDue(currentLastRefresh, now, effectiveConfig.refreshIntervalDays);
1028
1062
  const needsImageBuild = signatureStale || refreshDue;
1029
1063
  if (needsImageBuild) {
1064
+ if (values['inherit-build-proxy'] && effectiveConfig.dockerfile) {
1065
+ throw new Error('Build proxy inheritance is unavailable with a custom sandbox Dockerfile.');
1066
+ }
1067
+ const buildProxy = prepareBuildProxy(values['inherit-build-proxy'] ?? false, process.env, engine);
1068
+ if (values['inherit-build-proxy'])
1069
+ assertBuildProxyCompatibility(engine);
1030
1070
  const buildRefresh = !imageExists || refreshDue;
1031
1071
  const buildLastRefresh = buildRefresh ? now : currentLastRefresh || 0;
1032
1072
  p.log.step(refreshDue
@@ -1035,7 +1075,13 @@ export async function create(args) {
1035
1075
  ? 'Rebuilding stale image...'
1036
1076
  : 'Building image for first use...');
1037
1077
  try {
1038
- buildImage(effectiveConfig, tools, preparedDockerfile.path, expectedImageSignature, { engine, refresh: buildRefresh, lastRefresh: buildLastRefresh });
1078
+ buildImage(effectiveConfig, tools, preparedDockerfile.path, expectedImageSignature, {
1079
+ engine,
1080
+ refresh: buildRefresh,
1081
+ lastRefresh: buildLastRefresh,
1082
+ buildProxyArgs: buildProxy.args,
1083
+ buildEnv: buildProxy.env
1084
+ });
1039
1085
  p.log.success(refreshDue
1040
1086
  ? 'Image refreshed'
1041
1087
  : imageExists
@@ -1045,10 +1091,12 @@ export async function create(args) {
1045
1091
  catch (error) {
1046
1092
  if (refreshDue && !signatureStale && imageExists) {
1047
1093
  p.log.warn('Scheduled sandbox image refresh failed; continuing with the existing image. ' +
1048
- commandErrorMessage(error));
1094
+ redactBuildProxyValues(commandErrorMessage(error), buildProxy.redactionValues)
1095
+ + (values['inherit-build-proxy'] ? ` ${buildProxyFailureHint(engine)}` : ''));
1049
1096
  }
1050
1097
  else {
1051
- throw error;
1098
+ const hint = values['inherit-build-proxy'] ? `\n${buildProxyFailureHint(engine)}` : '';
1099
+ throw new Error(`${redactBuildProxyValues(commandErrorMessage(error), buildProxy.redactionValues)}${hint}`);
1052
1100
  }
1053
1101
  }
1054
1102
  }
@@ -9,15 +9,17 @@ import { runEngine, runOkEngine, runSafeEngine, runVerboseEngine } from '../shel
9
9
  import { pruneSandboxDanglingImages } from '../image-prune.js';
10
10
  import { resolveTools } from '../tools.js';
11
11
  import { buildImageSignature, buildSandboxImageArgs, parseImageLabels, parseRefreshTimestamp } from '../image-build.js';
12
- const USAGE = `Usage: ai sandbox rebuild [--quiet] [--refresh]`;
13
- export function buildArgs(config, tools, dockerfilePath, imageSignature, { engine, runFn = runEngine, runSafeFn = runSafeEngine, env = process.env, refresh = false, lastRefresh } = {}) {
12
+ import { assertBuildProxyCompatibility, buildProxyFailureHint, prepareBuildProxy, redactBuildProxyValues } from '../build-proxy.js';
13
+ const USAGE = `Usage: ai sandbox rebuild [--quiet] [--refresh] [--inherit-build-proxy|-B]`;
14
+ export function buildArgs(config, tools, dockerfilePath, imageSignature, { engine, runFn = runEngine, runSafeFn = runSafeEngine, env = process.env, refresh = false, lastRefresh, buildProxyArgs = [] } = {}) {
14
15
  return buildSandboxImageArgs(config, tools, dockerfilePath, imageSignature, {
15
16
  engine: engine ?? detectEngine(config),
16
17
  runFn,
17
18
  runSafeFn,
18
19
  env,
19
20
  refresh,
20
- lastRefresh
21
+ lastRefresh,
22
+ buildProxyArgs
21
23
  });
22
24
  }
23
25
  function readExistingLastRefresh(config, engine) {
@@ -41,6 +43,7 @@ export async function rebuild(args) {
41
43
  options: {
42
44
  refresh: { type: 'boolean' },
43
45
  quiet: { type: 'boolean', short: 'q' },
46
+ 'inherit-build-proxy': { type: 'boolean', short: 'B' },
44
47
  help: { type: 'boolean', short: 'h' }
45
48
  }
46
49
  });
@@ -56,6 +59,12 @@ export async function rebuild(args) {
56
59
  const refresh = values.refresh ?? false;
57
60
  const engine = detectEngine(config);
58
61
  await ensureDocker(config, undefined);
62
+ if (values['inherit-build-proxy'] && config.dockerfile) {
63
+ throw new Error('Build proxy inheritance is unavailable with a custom sandbox Dockerfile.');
64
+ }
65
+ const buildProxy = prepareBuildProxy(values['inherit-build-proxy'] ?? false, process.env, engine);
66
+ if (values['inherit-build-proxy'])
67
+ assertBuildProxyCompatibility(engine);
59
68
  const lastRefresh = refresh ? Date.now() : readExistingLastRefresh(config, engine);
60
69
  p.intro(pc.cyan('Rebuilding sandbox image'));
61
70
  try {
@@ -65,9 +74,11 @@ export async function rebuild(args) {
65
74
  runEngine(engine, 'docker', buildArgs(config, tools, preparedDockerfile.path, imageSignature, {
66
75
  engine,
67
76
  refresh,
68
- lastRefresh
77
+ lastRefresh,
78
+ buildProxyArgs: buildProxy.args
69
79
  }), {
70
- cwd: config.repoRoot
80
+ cwd: config.repoRoot,
81
+ env: buildProxy.env
71
82
  });
72
83
  spinner.stop(pc.green('Sandbox image rebuilt'));
73
84
  }
@@ -76,12 +87,18 @@ export async function rebuild(args) {
76
87
  runVerboseEngine(engine, 'docker', buildArgs(config, tools, preparedDockerfile.path, imageSignature, {
77
88
  engine,
78
89
  refresh,
79
- lastRefresh
80
- }), { cwd: config.repoRoot });
90
+ lastRefresh,
91
+ buildProxyArgs: buildProxy.args
92
+ }), { cwd: config.repoRoot, env: buildProxy.env });
81
93
  p.log.success(pc.green('Sandbox image rebuilt'));
82
94
  }
83
95
  pruneSandboxDanglingImages(config, engine);
84
96
  }
97
+ catch (error) {
98
+ const message = redactBuildProxyValues(error instanceof Error ? error.message : String(error), buildProxy.redactionValues);
99
+ const hint = values['inherit-build-proxy'] ? `\n${buildProxyFailureHint(engine)}` : '';
100
+ throw new Error(`${message}${hint}`);
101
+ }
85
102
  finally {
86
103
  preparedDockerfile.cleanup();
87
104
  }
@@ -14,7 +14,7 @@ export function buildImageSignature(preparedDockerfile, tools) {
14
14
  .digest('hex')
15
15
  .slice(0, 12);
16
16
  }
17
- export function buildSandboxImageArgs(config, tools, dockerfilePath, imageSignature, { engine, runFn = runEngine, runSafeFn = runSafeEngine, env = process.env, refresh = false, lastRefresh } = {}) {
17
+ export function buildSandboxImageArgs(config, tools, dockerfilePath, imageSignature, { engine, runFn = runEngine, runSafeFn = runSafeEngine, env = process.env, refresh = false, lastRefresh, buildProxyArgs = [] } = {}) {
18
18
  const selectedEngine = engine ?? detectEngine({ engine: config.engine });
19
19
  const { uid: hostUid, gid: hostGid } = resolveBuildUid({
20
20
  engine: selectedEngine,
@@ -39,6 +39,7 @@ export function buildSandboxImageArgs(config, tools, dockerfilePath, imageSignat
39
39
  '--label',
40
40
  `${sandboxImageConfigLabel(config)}=${imageSignature}`
41
41
  ];
42
+ args.push(...buildProxyArgs);
42
43
  if (lastRefresh !== undefined) {
43
44
  args.push('--label', `${sandboxImageRefreshLabel(config)}=${lastRefresh}`);
44
45
  }
@@ -5,20 +5,41 @@ ENV NPM_CONFIG_PREFIX=/home/devuser/.npm-global
5
5
  ENV PATH="/home/devuser/.npm-global/bin:${PATH}"
6
6
 
7
7
  ARG AI_TOOL_PACKAGES=
8
- RUN set -e && \
8
+ RUN --mount=type=secret,id=HTTP_PROXY \
9
+ --mount=type=secret,id=HTTPS_PROXY \
10
+ --mount=type=secret,id=NO_PROXY \
11
+ export HTTP_PROXY="$(cat /run/secrets/HTTP_PROXY 2>/dev/null || true)" && \
12
+ export HTTPS_PROXY="$(cat /run/secrets/HTTPS_PROXY 2>/dev/null || true)" && \
13
+ export NO_PROXY="$(cat /run/secrets/NO_PROXY 2>/dev/null || true)" && \
14
+ export http_proxy="${HTTP_PROXY}" https_proxy="${HTTPS_PROXY}" no_proxy="${NO_PROXY}" && \
15
+ set -e && \
9
16
  for pkg in ${AI_TOOL_PACKAGES}; do \
10
17
  npm install -g "$pkg"; \
11
18
  done
12
19
 
13
20
  ARG AI_TOOLS_SHELL_INSTALL_B64=
14
- RUN if [ -n "${AI_TOOLS_SHELL_INSTALL_B64}" ]; then \
21
+ RUN --mount=type=secret,id=HTTP_PROXY \
22
+ --mount=type=secret,id=HTTPS_PROXY \
23
+ --mount=type=secret,id=NO_PROXY \
24
+ export HTTP_PROXY="$(cat /run/secrets/HTTP_PROXY 2>/dev/null || true)" && \
25
+ export HTTPS_PROXY="$(cat /run/secrets/HTTPS_PROXY 2>/dev/null || true)" && \
26
+ export NO_PROXY="$(cat /run/secrets/NO_PROXY 2>/dev/null || true)" && \
27
+ export http_proxy="${HTTP_PROXY}" https_proxy="${HTTPS_PROXY}" no_proxy="${NO_PROXY}" && \
28
+ if [ -n "${AI_TOOLS_SHELL_INSTALL_B64}" ]; then \
15
29
  set -e && \
16
30
  echo "${AI_TOOLS_SHELL_INSTALL_B64}" | base64 -d > /tmp/ai-tools-install.sh && \
17
31
  bash /tmp/ai-tools-install.sh && \
18
32
  rm /tmp/ai-tools-install.sh; \
19
33
  fi
20
34
 
21
- RUN npm install -g pyright
35
+ RUN --mount=type=secret,id=HTTP_PROXY \
36
+ --mount=type=secret,id=HTTPS_PROXY \
37
+ --mount=type=secret,id=NO_PROXY \
38
+ export HTTP_PROXY="$(cat /run/secrets/HTTP_PROXY 2>/dev/null || true)" && \
39
+ export HTTPS_PROXY="$(cat /run/secrets/HTTPS_PROXY 2>/dev/null || true)" && \
40
+ export NO_PROXY="$(cat /run/secrets/NO_PROXY 2>/dev/null || true)" && \
41
+ export http_proxy="${HTTP_PROXY}" https_proxy="${HTTPS_PROXY}" no_proxy="${NO_PROXY}" && \
42
+ npm install -g pyright
22
43
 
23
44
  RUN mkdir -p /home/devuser/.local/share /home/devuser/.local/state
24
45
 
@@ -19,7 +19,14 @@ RUN if [ "${HOST_UID}" = "0" ]; then \
19
19
  useradd -u ${HOST_UID} -g ${HOST_GID} -m -s /bin/bash devuser; \
20
20
  fi
21
21
 
22
- RUN apt-get update && apt-get install -y \
22
+ RUN --mount=type=secret,id=HTTP_PROXY \
23
+ --mount=type=secret,id=HTTPS_PROXY \
24
+ --mount=type=secret,id=NO_PROXY \
25
+ export HTTP_PROXY="$(cat /run/secrets/HTTP_PROXY 2>/dev/null || true)" && \
26
+ export HTTPS_PROXY="$(cat /run/secrets/HTTPS_PROXY 2>/dev/null || true)" && \
27
+ export NO_PROXY="$(cat /run/secrets/NO_PROXY 2>/dev/null || true)" && \
28
+ export http_proxy="${HTTP_PROXY}" https_proxy="${HTTPS_PROXY}" no_proxy="${NO_PROXY}" && \
29
+ apt-get update && apt-get install -y \
23
30
  curl wget git vim file jq \
24
31
  build-essential ca-certificates gnupg lsb-release \
25
32
  libevent-core-2.1-7 libncursesw6 libtinfo6 \
@@ -1,3 +1,10 @@
1
- RUN apt-get update && apt-get install -y \
1
+ RUN --mount=type=secret,id=HTTP_PROXY \
2
+ --mount=type=secret,id=HTTPS_PROXY \
3
+ --mount=type=secret,id=NO_PROXY \
4
+ export HTTP_PROXY="$(cat /run/secrets/HTTP_PROXY 2>/dev/null || true)" && \
5
+ export HTTPS_PROXY="$(cat /run/secrets/HTTPS_PROXY 2>/dev/null || true)" && \
6
+ export NO_PROXY="$(cat /run/secrets/NO_PROXY 2>/dev/null || true)" && \
7
+ export http_proxy="${HTTP_PROXY}" https_proxy="${HTTPS_PROXY}" no_proxy="${NO_PROXY}" && \
8
+ apt-get update && apt-get install -y \
2
9
  openjdk-17-jdk maven \
3
10
  && rm -rf /var/lib/apt/lists/*
@@ -1,3 +1,10 @@
1
- RUN apt-get update && apt-get install -y \
1
+ RUN --mount=type=secret,id=HTTP_PROXY \
2
+ --mount=type=secret,id=HTTPS_PROXY \
3
+ --mount=type=secret,id=NO_PROXY \
4
+ export HTTP_PROXY="$(cat /run/secrets/HTTP_PROXY 2>/dev/null || true)" && \
5
+ export HTTPS_PROXY="$(cat /run/secrets/HTTPS_PROXY 2>/dev/null || true)" && \
6
+ export NO_PROXY="$(cat /run/secrets/NO_PROXY 2>/dev/null || true)" && \
7
+ export http_proxy="${HTTP_PROXY}" https_proxy="${HTTPS_PROXY}" no_proxy="${NO_PROXY}" && \
8
+ apt-get update && apt-get install -y \
2
9
  openjdk-21-jdk maven \
3
10
  && rm -rf /var/lib/apt/lists/*
@@ -1,3 +1,10 @@
1
- RUN bash -o pipefail -c 'curl -fsSL --retry 5 --retry-delay 2 --retry-all-errors https://deb.nodesource.com/setup_20.x | bash -' && \
1
+ RUN --mount=type=secret,id=HTTP_PROXY \
2
+ --mount=type=secret,id=HTTPS_PROXY \
3
+ --mount=type=secret,id=NO_PROXY \
4
+ export HTTP_PROXY="$(cat /run/secrets/HTTP_PROXY 2>/dev/null || true)" && \
5
+ export HTTPS_PROXY="$(cat /run/secrets/HTTPS_PROXY 2>/dev/null || true)" && \
6
+ export NO_PROXY="$(cat /run/secrets/NO_PROXY 2>/dev/null || true)" && \
7
+ export http_proxy="${HTTP_PROXY}" https_proxy="${HTTPS_PROXY}" no_proxy="${NO_PROXY}" && \
8
+ bash -o pipefail -c 'curl -fsSL --retry 5 --retry-delay 2 --retry-all-errors https://deb.nodesource.com/setup_20.x | bash -' && \
2
9
  apt-get install -y nodejs && \
3
10
  rm -rf /var/lib/apt/lists/*
@@ -1,3 +1,10 @@
1
- RUN bash -o pipefail -c 'curl -fsSL --retry 5 --retry-delay 2 --retry-all-errors https://deb.nodesource.com/setup_22.x | bash -' && \
1
+ RUN --mount=type=secret,id=HTTP_PROXY \
2
+ --mount=type=secret,id=HTTPS_PROXY \
3
+ --mount=type=secret,id=NO_PROXY \
4
+ export HTTP_PROXY="$(cat /run/secrets/HTTP_PROXY 2>/dev/null || true)" && \
5
+ export HTTPS_PROXY="$(cat /run/secrets/HTTPS_PROXY 2>/dev/null || true)" && \
6
+ export NO_PROXY="$(cat /run/secrets/NO_PROXY 2>/dev/null || true)" && \
7
+ export http_proxy="${HTTP_PROXY}" https_proxy="${HTTPS_PROXY}" no_proxy="${NO_PROXY}" && \
8
+ bash -o pipefail -c 'curl -fsSL --retry 5 --retry-delay 2 --retry-all-errors https://deb.nodesource.com/setup_22.x | bash -' && \
2
9
  apt-get install -y nodejs && \
3
10
  rm -rf /var/lib/apt/lists/*
@@ -1,3 +1,10 @@
1
- RUN apt-get update && apt-get install -y \
1
+ RUN --mount=type=secret,id=HTTP_PROXY \
2
+ --mount=type=secret,id=HTTPS_PROXY \
3
+ --mount=type=secret,id=NO_PROXY \
4
+ export HTTP_PROXY="$(cat /run/secrets/HTTP_PROXY 2>/dev/null || true)" && \
5
+ export HTTPS_PROXY="$(cat /run/secrets/HTTPS_PROXY 2>/dev/null || true)" && \
6
+ export NO_PROXY="$(cat /run/secrets/NO_PROXY 2>/dev/null || true)" && \
7
+ export http_proxy="${HTTP_PROXY}" https_proxy="${HTTPS_PROXY}" no_proxy="${NO_PROXY}" && \
8
+ apt-get update && apt-get install -y \
2
9
  python3 python3-pip python3-venv \
3
10
  && rm -rf /var/lib/apt/lists/*
@@ -8,7 +8,8 @@ function normalizeOptions(opts = {}, stdio) {
8
8
  cwd: opts.cwd,
9
9
  encoding: opts.encoding,
10
10
  stdio,
11
- timeout: opts.timeout ?? DEFAULT_TIMEOUT_MS
11
+ timeout: opts.timeout ?? DEFAULT_TIMEOUT_MS,
12
+ env: opts.env
12
13
  };
13
14
  }
14
15
  function resolveCommand(cmd) {
@@ -10,11 +10,12 @@ Print a deterministic Issue body extracted from a task's task.md.
10
10
  Omit <ref> Resolve the unique active task for the current branch.
11
11
  <ref> Bare numeric short id, or a full TASK-YYYYMMDD-HHMMSS id.
12
12
  --template <path> Render the final body for the given GitHub Issue Form (scenario A);
13
- without it, print the default '描述 + 需求' body (scenario B).
13
+ without it, print the default '任务输入 + 描述 + 需求' body (scenario B).
14
14
 
15
- Only the task title, '## 描述' and '## 需求' sections are ever emitted; the rest of
16
- task.md (scaffolding sections, placeholders) is never written to the body.
15
+ Only the task title, '## 任务输入', '## 描述' and '## 需求' sections are ever
16
+ emitted; the rest of task.md is never written to the body.
17
17
  `;
18
+ const TASK_INPUT_ALIASES = ['任务输入', 'Task Input'];
18
19
  const DESCRIPTION_ALIASES = ['描述', 'Description'];
19
20
  const REQUIREMENTS_ALIASES = ['需求', 'Requirements'];
20
21
  /**
@@ -22,17 +23,21 @@ const REQUIREMENTS_ALIASES = ['需求', 'Requirements'];
22
23
  * task.md actually uses, with empty sections falling back to `N/A`.
23
24
  */
24
25
  function buildDefaultBody(content) {
26
+ const taskInputHeading = findSectionHeading(content, TASK_INPUT_ALIASES);
25
27
  const descHeading = findSectionHeading(content, DESCRIPTION_ALIASES);
26
28
  const reqHeading = findSectionHeading(content, REQUIREMENTS_ALIASES);
29
+ const taskInput = extractSection(content, TASK_INPUT_ALIASES) || PLACEHOLDER;
27
30
  const description = extractSection(content, DESCRIPTION_ALIASES) || PLACEHOLDER;
28
31
  const requirements = extractSection(content, REQUIREMENTS_ALIASES) || PLACEHOLDER;
29
- return `## ${descHeading}\n\n${description}\n\n## ${reqHeading}\n\n${requirements}\n`;
32
+ return `## ${taskInputHeading}\n\n${taskInput}\n\n## ${descHeading}\n\n${description}\n\n## ${reqHeading}\n\n${requirements}\n`;
30
33
  }
31
34
  function readTaskFields(content) {
32
35
  return {
33
36
  title: extractTitle(content),
34
37
  description: extractSection(content, DESCRIPTION_ALIASES),
35
- requirements: extractSection(content, REQUIREMENTS_ALIASES)
38
+ requirements: extractSection(content, REQUIREMENTS_ALIASES),
39
+ taskInput: extractSection(content, TASK_INPUT_ALIASES),
40
+ taskInputHeading: findSectionHeading(content, TASK_INPUT_ALIASES)
36
41
  };
37
42
  }
38
43
  function issueBody(args = []) {
@@ -79,7 +79,7 @@ const FAMILY = {
79
79
  'review-plan': { artifact: 'review-plan', started: ['technical-design', 'technical-design-review'], completed: ['technical-design', 'technical-design-review'], target: 'technical-design-review', label: 'Review Plan' },
80
80
  code: { artifact: 'code', started: ['technical-design-review', 'code-review'], completed: ['technical-design-review', 'code-review'], target: 'code', label: 'Code Task' },
81
81
  'review-code': { artifact: 'review-code', started: ['code', 'code-review', 'commit'], completed: ['code', 'code-review', 'commit'], target: 'code-review', label: 'Review Code' },
82
- 'manual-validation': { artifact: 'manual-validation', started: ['code-review'], completed: ['code-review'], target: null, label: 'Complete Manual Validation' }
82
+ 'manual-validation': { artifact: 'manual-validation', started: ['code-review', 'commit'], completed: ['code-review', 'commit'], target: null, label: 'Complete Manual Validation' }
83
83
  };
84
84
  function eventParts(event) {
85
85
  const [family, suffix] = event.split('.');
@@ -73,6 +73,8 @@ function renderTemplateBody(formText, fields) {
73
73
  const value = mapped.trim() === '' ? PLACEHOLDER : mapped;
74
74
  sections.push(`### ${label}\n\n${value}`);
75
75
  }
76
+ const taskInput = fields.taskInput.trim() === '' ? PLACEHOLDER : fields.taskInput;
77
+ sections.push(`### ${fields.taskInputHeading}\n\n${taskInput}`);
76
78
  return `${sections.join('\n\n')}\n`;
77
79
  }
78
80
  export { requirementFieldLabels, renderTemplateBody, mapFieldValue, PLACEHOLDER };
@@ -5,6 +5,15 @@ import { artifactName, maxRound } from "./review-artifacts.js";
5
5
  // covers ALL tracked changes by default. This default denylist is EMPTY —
6
6
  // projects opt into exclusions explicitly via review.post_review_exclude_globs.
7
7
  export const DEFAULT_POST_REVIEW_EXCLUDES = [];
8
+ export function loadPostReviewConfig(repoRoot) {
9
+ try {
10
+ const config = JSON.parse(fs.readFileSync(path.join(repoRoot, ".agents", ".airc.json"), "utf8"));
11
+ return config.review && typeof config.review === "object" ? config.review : {};
12
+ }
13
+ catch {
14
+ return {};
15
+ }
16
+ }
8
17
  export function resolvePostReviewGlobs(config = {}, reviewConfig = {}) {
9
18
  const projectExcludes = [
10
19
  ...(Array.isArray(reviewConfig.post_review_exclude_globs) ? reviewConfig.post_review_exclude_globs : []),