@fitlab-ai/agent-infra 0.6.2-alpha.1 → 0.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/dist/lib/sandbox/commands/create.js +27 -19
- package/dist/lib/sandbox/credentials.js +43 -24
- package/dist/package.json +1 -1
- package/lib/sandbox/commands/create.ts +29 -19
- package/lib/sandbox/credentials.ts +49 -26
- package/package.json +1 -1
- package/templates/.agents/rules/create-issue.github.en.md +3 -3
- package/templates/.agents/rules/create-issue.github.zh-CN.md +3 -3
- package/templates/.agents/skills/analyze-task/SKILL.en.md +3 -0
- package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +3 -0
- package/templates/.agents/skills/complete-task/SKILL.en.md +1 -0
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +1 -0
- package/templates/.agents/skills/create-pr/config/verify.json +1 -0
- package/templates/.agents/skills/create-task/SKILL.en.md +3 -3
- package/templates/.agents/skills/create-task/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/plan-task/SKILL.en.md +2 -0
- package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +2 -0
package/README.md
CHANGED
|
@@ -777,7 +777,7 @@ The generated `.agents/.airc.json` file is the central contract between the boot
|
|
|
777
777
|
"project": "my-project",
|
|
778
778
|
"org": "my-org",
|
|
779
779
|
"language": "en",
|
|
780
|
-
"templateVersion": "v0.6.
|
|
780
|
+
"templateVersion": "v0.6.2",
|
|
781
781
|
"templates": {
|
|
782
782
|
"sources": [
|
|
783
783
|
{ "type": "local", "path": "~/private-templates" }
|
package/README.zh-CN.md
CHANGED
|
@@ -753,7 +753,7 @@ import-issue #42 从 GitHub Issue 导入任务
|
|
|
753
753
|
"project": "my-project",
|
|
754
754
|
"org": "my-org",
|
|
755
755
|
"language": "en",
|
|
756
|
-
"templateVersion": "v0.6.
|
|
756
|
+
"templateVersion": "v0.6.2",
|
|
757
757
|
"templates": {
|
|
758
758
|
"sources": [
|
|
759
759
|
{ "type": "local", "path": "~/private-templates" }
|
|
@@ -18,7 +18,7 @@ import { hostJoin, toEnginePath, volumeArg } from "../engines/wsl2-paths.js";
|
|
|
18
18
|
import { validateSelinuxDisableEnv } from "../engines/selinux.js";
|
|
19
19
|
import { resolveBuildUid } from "../engines/native.js";
|
|
20
20
|
import { dotfilesCacheDir, materializeDotfiles } from "../dotfiles.js";
|
|
21
|
-
import {
|
|
21
|
+
import { prepareClaudeCredentials, redactCommandError, validateClaudeCredentialsEnvOverride } from "../credentials.js";
|
|
22
22
|
const OPENCODE_YOLO_PERMISSION = '{"*":"allow","read":"allow","bash":"allow","edit":"allow","webfetch":"allow","external_directory":"allow","doom_loop":"allow"}';
|
|
23
23
|
const SANDBOX_ALIAS_BLOCK_BEGIN = '# >>> agent-infra managed aliases >>>';
|
|
24
24
|
const SANDBOX_ALIAS_BLOCK_END = '# <<< agent-infra managed aliases <<<';
|
|
@@ -825,11 +825,13 @@ export async function create(args) {
|
|
|
825
825
|
assertBranchAvailable(config.repoRoot, branch, { allowedWorktrees: worktreeCandidates });
|
|
826
826
|
const tools = resolveTools(effectiveConfig);
|
|
827
827
|
const resolvedTools = resolveToolDirs(effectiveConfig, tools, branch);
|
|
828
|
-
//
|
|
829
|
-
// Claude Code credential
|
|
830
|
-
//
|
|
831
|
-
|
|
832
|
-
|
|
828
|
+
// Fatal credential states still fail before filesystem/docker side effects.
|
|
829
|
+
// A genuinely missing Claude Code credential only removes Claude Code's
|
|
830
|
+
// sandbox config and credential mounts for this create run.
|
|
831
|
+
const credentialOutcome = prepareClaudeCredentials(effectiveConfig.home, effectiveConfig.project, resolvedTools);
|
|
832
|
+
const effectiveResolvedTools = credentialOutcome.status === 'SKIPPED'
|
|
833
|
+
? resolvedTools.filter(({ tool }) => tool.id !== 'claude-code')
|
|
834
|
+
: resolvedTools;
|
|
833
835
|
const container = containerName(effectiveConfig, branch);
|
|
834
836
|
const worktree = worktreeCandidates.find((candidate) => fs.existsSync(candidate)) ?? worktreeCandidates[0] ?? '';
|
|
835
837
|
const shareCommon = shareCommonDir(effectiveConfig);
|
|
@@ -840,6 +842,11 @@ export async function create(args) {
|
|
|
840
842
|
const engine = detectEngine(effectiveConfig);
|
|
841
843
|
p.intro(pc.cyan('AI Sandbox'));
|
|
842
844
|
p.log.info(`Project: ${pc.bold(effectiveConfig.project)} | Branch: ${pc.bold(branch)} | Base: ${pc.bold(baseBranch || 'HEAD')}`);
|
|
845
|
+
if (credentialOutcome.status === 'SKIPPED') {
|
|
846
|
+
p.log.warn('Claude Code credentials not found on host - creating this sandbox WITHOUT Claude Code credentials.\n'
|
|
847
|
+
+ ' Claude Code is still installed in the image but will not be authenticated.\n'
|
|
848
|
+
+ ' To enable it: run "claude" once on the host to complete login, then re-run "ai sandbox create".');
|
|
849
|
+
}
|
|
843
850
|
try {
|
|
844
851
|
p.log.step('Checking container engine...');
|
|
845
852
|
await ensureDocker(effectiveConfig, (detail) => {
|
|
@@ -913,7 +920,7 @@ export async function create(args) {
|
|
|
913
920
|
{
|
|
914
921
|
title: 'Preparing tool state',
|
|
915
922
|
task: async () => {
|
|
916
|
-
for (const { tool, dir } of
|
|
923
|
+
for (const { tool, dir } of effectiveResolvedTools) {
|
|
917
924
|
fs.mkdirSync(dir, { recursive: true });
|
|
918
925
|
for (const { hostPath, sandboxName } of tool.hostPreSeedFiles ?? []) {
|
|
919
926
|
const destination = path.join(dir, sandboxName);
|
|
@@ -946,7 +953,7 @@ export async function create(args) {
|
|
|
946
953
|
fs.writeFileSync(filePath, content, 'utf8');
|
|
947
954
|
}
|
|
948
955
|
}
|
|
949
|
-
return `${
|
|
956
|
+
return `${effectiveResolvedTools.length} tool config directories ready`;
|
|
950
957
|
}
|
|
951
958
|
},
|
|
952
959
|
{
|
|
@@ -978,31 +985,32 @@ export async function create(args) {
|
|
|
978
985
|
const cachedGpg = needsGpg
|
|
979
986
|
? readGpgCache(effectiveConfig.home, effectiveConfig.project, undefined, signingKey)
|
|
980
987
|
: null;
|
|
981
|
-
const envFile = buildContainerEnvFile(
|
|
988
|
+
const envFile = buildContainerEnvFile(effectiveResolvedTools, engine);
|
|
982
989
|
let hostShellConfig;
|
|
983
990
|
try {
|
|
984
|
-
const claudeCodeEntry =
|
|
991
|
+
const claudeCodeEntry = effectiveResolvedTools.find(({ tool }) => tool.id === 'claude-code');
|
|
985
992
|
if (claudeCodeEntry) {
|
|
986
993
|
ensureClaudeOnboarding(claudeCodeEntry.dir, effectiveConfig.home);
|
|
987
994
|
ensureClaudeSettings(claudeCodeEntry.dir, effectiveConfig.home);
|
|
988
|
-
//
|
|
989
|
-
//
|
|
995
|
+
// prepareClaudeCredentials wrote the shared credentials file
|
|
996
|
+
// before this point. If credentials were missing, the
|
|
997
|
+
// claude-code entry was removed from effectiveResolvedTools.
|
|
990
998
|
}
|
|
991
|
-
const codexEntry =
|
|
999
|
+
const codexEntry = effectiveResolvedTools.find(({ tool }) => tool.id === 'codex');
|
|
992
1000
|
if (codexEntry) {
|
|
993
1001
|
ensureCodexModelInheritance(codexEntry.dir, effectiveConfig.home);
|
|
994
1002
|
ensureCodexWorkspaceTrust(codexEntry.dir);
|
|
995
1003
|
}
|
|
996
|
-
const geminiEntry =
|
|
1004
|
+
const geminiEntry = effectiveResolvedTools.find(({ tool }) => tool.id === 'gemini-cli');
|
|
997
1005
|
if (geminiEntry) {
|
|
998
1006
|
ensureGeminiWorkspaceTrust(geminiEntry.dir);
|
|
999
1007
|
}
|
|
1000
|
-
const opencodeEntry =
|
|
1008
|
+
const opencodeEntry = effectiveResolvedTools.find(({ tool }) => tool.id === 'opencode');
|
|
1001
1009
|
if (opencodeEntry) {
|
|
1002
1010
|
// The TUI reads <toolDir>/opencode.json via OPENCODE_CONFIG pinned in tools.js.
|
|
1003
1011
|
ensureOpenCodeModelInheritance(opencodeEntry.dir, effectiveConfig.home);
|
|
1004
1012
|
}
|
|
1005
|
-
const toolVolumes =
|
|
1013
|
+
const toolVolumes = effectiveResolvedTools.flatMap(({ tool, dir }) => [
|
|
1006
1014
|
'-v',
|
|
1007
1015
|
volumeArg(engine, dir, tool.containerMount)
|
|
1008
1016
|
]);
|
|
@@ -1017,7 +1025,7 @@ export async function create(args) {
|
|
|
1017
1025
|
'-v',
|
|
1018
1026
|
volumeArg(engine, hostPath, containerPath, ':ro')
|
|
1019
1027
|
]);
|
|
1020
|
-
const liveMountVolumes =
|
|
1028
|
+
const liveMountVolumes = effectiveResolvedTools.flatMap(({ tool }) => (tool.hostLiveMounts ?? [])
|
|
1021
1029
|
.filter(({ hostPath }) => fs.existsSync(hostPath))
|
|
1022
1030
|
.flatMap(({ hostPath, containerSubpath }) => [
|
|
1023
1031
|
'-v',
|
|
@@ -1105,7 +1113,7 @@ export async function create(args) {
|
|
|
1105
1113
|
: 'Host GPG keys unavailable; using stripped git config fallback...');
|
|
1106
1114
|
}
|
|
1107
1115
|
}
|
|
1108
|
-
for (const { tool } of
|
|
1116
|
+
for (const { tool } of effectiveResolvedTools) {
|
|
1109
1117
|
for (const command of tool.postSetupCmds ?? []) {
|
|
1110
1118
|
runSafeEngine(engine, 'docker', ['exec', container, 'bash', '-lc', command]);
|
|
1111
1119
|
}
|
|
@@ -1143,7 +1151,7 @@ export async function create(args) {
|
|
|
1143
1151
|
}
|
|
1144
1152
|
}
|
|
1145
1153
|
p.outro(pc.green('Sandbox ready'));
|
|
1146
|
-
const toolHints =
|
|
1154
|
+
const toolHints = effectiveResolvedTools.map(({ tool, dir }) => {
|
|
1147
1155
|
const hasLiveMount = (tool.hostLiveMounts ?? []).some(({ hostPath }) => fs.existsSync(hostPath));
|
|
1148
1156
|
const hint = hasLiveMount
|
|
1149
1157
|
? 'Live-mounted auth/config files stay in sync with the host.'
|
|
@@ -395,43 +395,62 @@ export function formatRemaining(expiresAt) {
|
|
|
395
395
|
const minutes = totalMinutes % 60;
|
|
396
396
|
return hours > 0 ? `${hours}h ${minutes}m` : `${minutes}m`;
|
|
397
397
|
}
|
|
398
|
-
export function
|
|
398
|
+
export function prepareClaudeCredentials(home, project, resolvedTools, extractFn = extractClaudeCredentialsBlob, writeFn = writeClaudeCredentialsFile, inspectFn = inspectClaudeKeychainStatus) {
|
|
399
399
|
const claudeCodeEntry = resolvedTools.find(({ tool }) => tool.id === 'claude-code');
|
|
400
400
|
if (!claudeCodeEntry) {
|
|
401
|
-
return;
|
|
401
|
+
return { status: 'NOT_APPLICABLE' };
|
|
402
402
|
}
|
|
403
403
|
let blob = null;
|
|
404
404
|
const hasCustomInspectFn = inspectFn !== inspectClaudeKeychainStatus;
|
|
405
405
|
const hasCustomExtractFn = extractFn !== extractClaudeCredentialsBlob;
|
|
406
406
|
if (hasCustomInspectFn || !hasCustomExtractFn) {
|
|
407
407
|
const inspection = inspectFn(home);
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
408
|
+
switch (inspection.status) {
|
|
409
|
+
case 'OK':
|
|
410
|
+
blob = inspection.blob;
|
|
411
|
+
break;
|
|
412
|
+
case 'MISSING':
|
|
413
|
+
return { status: 'SKIPPED' };
|
|
414
|
+
case 'KEYCHAIN_LOCKED':
|
|
415
|
+
throw new Error([
|
|
416
|
+
'Claude Code credentials are stored in the macOS keychain, but the keychain is locked.',
|
|
417
|
+
'',
|
|
418
|
+
buildLockedGuidance()
|
|
419
|
+
].join('\n'));
|
|
420
|
+
case 'STALE_ACCESS':
|
|
421
|
+
throw new Error([
|
|
422
|
+
'Claude Code credentials on host are invalid or expired.',
|
|
423
|
+
'',
|
|
424
|
+
'The sandbox needs valid Claude Code OAuth credentials so the container can use Claude Code.',
|
|
425
|
+
'',
|
|
426
|
+
'To fix:',
|
|
427
|
+
' 1. On the host, run "claude" once and complete the OAuth login flow.',
|
|
428
|
+
' 2. Verify with "claude /status" that you see your subscription.',
|
|
429
|
+
' 3. Re-run "ai sandbox create".'
|
|
430
|
+
].join('\n'));
|
|
431
|
+
case 'KEYCHAIN_ERROR':
|
|
432
|
+
throw new Error([
|
|
433
|
+
'Claude Code credentials could not be read from the host keychain.',
|
|
434
|
+
'',
|
|
435
|
+
inspection.detail ? `Detail: ${inspection.detail}` : 'Detail: unknown keychain error',
|
|
436
|
+
'',
|
|
437
|
+
'To fix:',
|
|
438
|
+
' 1. Unlock or repair the host keychain, then re-run "ai sandbox create".',
|
|
439
|
+
' 2. For SSH / CI, set AGENT_INFRA_CLAUDE_CREDENTIALS_FILE to an absolute path containing a valid credentials blob.'
|
|
440
|
+
].join('\n'));
|
|
441
|
+
default: {
|
|
442
|
+
const _exhaustive = inspection;
|
|
443
|
+
throw new Error(`Unhandled Claude Code credential inspection status: ${_exhaustive.status}`);
|
|
444
|
+
}
|
|
414
445
|
}
|
|
415
|
-
blob = inspection.status === 'OK' ? inspection.blob : null;
|
|
416
446
|
}
|
|
417
447
|
else {
|
|
418
448
|
blob = extractFn(home);
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
'Claude Code credentials not found on host.',
|
|
423
|
-
'',
|
|
424
|
-
'The sandbox needs your Claude Code OAuth credentials so the container can use Claude Code.',
|
|
425
|
-
'',
|
|
426
|
-
'To fix:',
|
|
427
|
-
' 1. On the host, run "claude" once and complete the OAuth login flow.',
|
|
428
|
-
' 2. Verify with "claude /status" that you see your subscription.',
|
|
429
|
-
' 3. Re-run "ai sandbox create".',
|
|
430
|
-
'',
|
|
431
|
-
'Alternatively, if you do not need Claude Code in this sandbox,',
|
|
432
|
-
'remove "claude-code" from the "sandbox.tools" array in .agents/.airc.json.'
|
|
433
|
-
].join('\n'));
|
|
449
|
+
if (!blob) {
|
|
450
|
+
return { status: 'SKIPPED' };
|
|
451
|
+
}
|
|
434
452
|
}
|
|
435
453
|
writeFn(home, project, blob);
|
|
454
|
+
return { status: 'OK' };
|
|
436
455
|
}
|
|
437
456
|
//# sourceMappingURL=credentials.js.map
|
package/dist/package.json
CHANGED
|
@@ -43,7 +43,7 @@ import { validateSelinuxDisableEnv } from '../engines/selinux.ts';
|
|
|
43
43
|
import { resolveBuildUid } from '../engines/native.ts';
|
|
44
44
|
import { dotfilesCacheDir, materializeDotfiles } from '../dotfiles.ts';
|
|
45
45
|
import {
|
|
46
|
-
|
|
46
|
+
prepareClaudeCredentials,
|
|
47
47
|
redactCommandError,
|
|
48
48
|
validateClaudeCredentialsEnvOverride
|
|
49
49
|
} from '../credentials.ts';
|
|
@@ -1100,15 +1100,17 @@ export async function create(args: string[]): Promise<void> {
|
|
|
1100
1100
|
assertBranchAvailable(config.repoRoot, branch, { allowedWorktrees: worktreeCandidates });
|
|
1101
1101
|
const tools = resolveTools(effectiveConfig);
|
|
1102
1102
|
const resolvedTools = resolveToolDirs(effectiveConfig, tools, branch);
|
|
1103
|
-
//
|
|
1104
|
-
// Claude Code credential
|
|
1105
|
-
//
|
|
1106
|
-
|
|
1107
|
-
assertClaudeCredentialsAvailable(
|
|
1103
|
+
// Fatal credential states still fail before filesystem/docker side effects.
|
|
1104
|
+
// A genuinely missing Claude Code credential only removes Claude Code's
|
|
1105
|
+
// sandbox config and credential mounts for this create run.
|
|
1106
|
+
const credentialOutcome = prepareClaudeCredentials(
|
|
1108
1107
|
effectiveConfig.home,
|
|
1109
1108
|
effectiveConfig.project,
|
|
1110
1109
|
resolvedTools
|
|
1111
1110
|
);
|
|
1111
|
+
const effectiveResolvedTools = credentialOutcome.status === 'SKIPPED'
|
|
1112
|
+
? resolvedTools.filter(({ tool }) => tool.id !== 'claude-code')
|
|
1113
|
+
: resolvedTools;
|
|
1112
1114
|
const container = containerName(effectiveConfig, branch);
|
|
1113
1115
|
const worktree = worktreeCandidates.find((candidate) => fs.existsSync(candidate)) ?? worktreeCandidates[0] ?? '';
|
|
1114
1116
|
const shareCommon = shareCommonDir(effectiveConfig);
|
|
@@ -1122,6 +1124,13 @@ export async function create(args: string[]): Promise<void> {
|
|
|
1122
1124
|
p.log.info(
|
|
1123
1125
|
`Project: ${pc.bold(effectiveConfig.project)} | Branch: ${pc.bold(branch)} | Base: ${pc.bold(baseBranch || 'HEAD')}`
|
|
1124
1126
|
);
|
|
1127
|
+
if (credentialOutcome.status === 'SKIPPED') {
|
|
1128
|
+
p.log.warn(
|
|
1129
|
+
'Claude Code credentials not found on host - creating this sandbox WITHOUT Claude Code credentials.\n'
|
|
1130
|
+
+ ' Claude Code is still installed in the image but will not be authenticated.\n'
|
|
1131
|
+
+ ' To enable it: run "claude" once on the host to complete login, then re-run "ai sandbox create".'
|
|
1132
|
+
);
|
|
1133
|
+
}
|
|
1125
1134
|
|
|
1126
1135
|
try {
|
|
1127
1136
|
p.log.step('Checking container engine...');
|
|
@@ -1206,7 +1215,7 @@ export async function create(args: string[]): Promise<void> {
|
|
|
1206
1215
|
{
|
|
1207
1216
|
title: 'Preparing tool state',
|
|
1208
1217
|
task: async () => {
|
|
1209
|
-
for (const { tool, dir } of
|
|
1218
|
+
for (const { tool, dir } of effectiveResolvedTools) {
|
|
1210
1219
|
fs.mkdirSync(dir, { recursive: true });
|
|
1211
1220
|
|
|
1212
1221
|
for (const { hostPath, sandboxName } of tool.hostPreSeedFiles ?? []) {
|
|
@@ -1243,7 +1252,7 @@ export async function create(args: string[]): Promise<void> {
|
|
|
1243
1252
|
}
|
|
1244
1253
|
}
|
|
1245
1254
|
|
|
1246
|
-
return `${
|
|
1255
|
+
return `${effectiveResolvedTools.length} tool config directories ready`;
|
|
1247
1256
|
}
|
|
1248
1257
|
},
|
|
1249
1258
|
{
|
|
@@ -1283,31 +1292,32 @@ export async function create(args: string[]): Promise<void> {
|
|
|
1283
1292
|
signingKey
|
|
1284
1293
|
)
|
|
1285
1294
|
: null;
|
|
1286
|
-
const envFile = buildContainerEnvFile(
|
|
1295
|
+
const envFile = buildContainerEnvFile(effectiveResolvedTools, engine);
|
|
1287
1296
|
let hostShellConfig: HostShellConfig;
|
|
1288
1297
|
try {
|
|
1289
|
-
const claudeCodeEntry =
|
|
1298
|
+
const claudeCodeEntry = effectiveResolvedTools.find(({ tool }) => tool.id === 'claude-code');
|
|
1290
1299
|
if (claudeCodeEntry) {
|
|
1291
1300
|
ensureClaudeOnboarding(claudeCodeEntry.dir, effectiveConfig.home);
|
|
1292
1301
|
ensureClaudeSettings(claudeCodeEntry.dir, effectiveConfig.home);
|
|
1293
|
-
//
|
|
1294
|
-
//
|
|
1302
|
+
// prepareClaudeCredentials wrote the shared credentials file
|
|
1303
|
+
// before this point. If credentials were missing, the
|
|
1304
|
+
// claude-code entry was removed from effectiveResolvedTools.
|
|
1295
1305
|
}
|
|
1296
|
-
const codexEntry =
|
|
1306
|
+
const codexEntry = effectiveResolvedTools.find(({ tool }) => tool.id === 'codex');
|
|
1297
1307
|
if (codexEntry) {
|
|
1298
1308
|
ensureCodexModelInheritance(codexEntry.dir, effectiveConfig.home);
|
|
1299
1309
|
ensureCodexWorkspaceTrust(codexEntry.dir);
|
|
1300
1310
|
}
|
|
1301
|
-
const geminiEntry =
|
|
1311
|
+
const geminiEntry = effectiveResolvedTools.find(({ tool }) => tool.id === 'gemini-cli');
|
|
1302
1312
|
if (geminiEntry) {
|
|
1303
1313
|
ensureGeminiWorkspaceTrust(geminiEntry.dir);
|
|
1304
1314
|
}
|
|
1305
|
-
const opencodeEntry =
|
|
1315
|
+
const opencodeEntry = effectiveResolvedTools.find(({ tool }) => tool.id === 'opencode');
|
|
1306
1316
|
if (opencodeEntry) {
|
|
1307
1317
|
// The TUI reads <toolDir>/opencode.json via OPENCODE_CONFIG pinned in tools.js.
|
|
1308
1318
|
ensureOpenCodeModelInheritance(opencodeEntry.dir, effectiveConfig.home);
|
|
1309
1319
|
}
|
|
1310
|
-
const toolVolumes =
|
|
1320
|
+
const toolVolumes = effectiveResolvedTools.flatMap(({ tool, dir }) => [
|
|
1311
1321
|
'-v',
|
|
1312
1322
|
volumeArg(engine, dir, tool.containerMount)
|
|
1313
1323
|
]);
|
|
@@ -1322,7 +1332,7 @@ export async function create(args: string[]): Promise<void> {
|
|
|
1322
1332
|
'-v',
|
|
1323
1333
|
volumeArg(engine, hostPath, containerPath, ':ro')
|
|
1324
1334
|
]);
|
|
1325
|
-
const liveMountVolumes =
|
|
1335
|
+
const liveMountVolumes = effectiveResolvedTools.flatMap(({ tool }) =>
|
|
1326
1336
|
(tool.hostLiveMounts ?? [])
|
|
1327
1337
|
.filter(({ hostPath }) => fs.existsSync(hostPath))
|
|
1328
1338
|
.flatMap(({ hostPath, containerSubpath }) => [
|
|
@@ -1435,7 +1445,7 @@ export async function create(args: string[]): Promise<void> {
|
|
|
1435
1445
|
}
|
|
1436
1446
|
}
|
|
1437
1447
|
|
|
1438
|
-
for (const { tool } of
|
|
1448
|
+
for (const { tool } of effectiveResolvedTools) {
|
|
1439
1449
|
for (const command of tool.postSetupCmds ?? []) {
|
|
1440
1450
|
runSafeEngine(engine, 'docker', ['exec', container, 'bash', '-lc', command]);
|
|
1441
1451
|
}
|
|
@@ -1477,7 +1487,7 @@ export async function create(args: string[]): Promise<void> {
|
|
|
1477
1487
|
|
|
1478
1488
|
p.outro(pc.green('Sandbox ready'));
|
|
1479
1489
|
|
|
1480
|
-
const toolHints =
|
|
1490
|
+
const toolHints = effectiveResolvedTools.map(({ tool, dir }) => {
|
|
1481
1491
|
const hasLiveMount = (tool.hostLiveMounts ?? []).some(({ hostPath }) => fs.existsSync(hostPath));
|
|
1482
1492
|
const hint = hasLiveMount
|
|
1483
1493
|
? 'Live-mounted auth/config files stay in sync with the host.'
|
|
@@ -92,6 +92,11 @@ type ResolvedTool = {
|
|
|
92
92
|
};
|
|
93
93
|
};
|
|
94
94
|
|
|
95
|
+
export type ClaudeCredentialOutcome =
|
|
96
|
+
| { status: 'OK' }
|
|
97
|
+
| { status: 'SKIPPED' }
|
|
98
|
+
| { status: 'NOT_APPLICABLE' };
|
|
99
|
+
|
|
95
100
|
type CredentialPayload = {
|
|
96
101
|
claudeAiOauth?: CredentialPayload;
|
|
97
102
|
scopes?: unknown;
|
|
@@ -584,17 +589,17 @@ export function formatRemaining(expiresAt: unknown): string {
|
|
|
584
589
|
return hours > 0 ? `${hours}h ${minutes}m` : `${minutes}m`;
|
|
585
590
|
}
|
|
586
591
|
|
|
587
|
-
export function
|
|
592
|
+
export function prepareClaudeCredentials(
|
|
588
593
|
home: string,
|
|
589
594
|
project: string,
|
|
590
595
|
resolvedTools: ResolvedTool[],
|
|
591
596
|
extractFn: (home: string) => string | null = extractClaudeCredentialsBlob,
|
|
592
597
|
writeFn: (home: string, project: string, blob: string) => void = writeClaudeCredentialsFile,
|
|
593
598
|
inspectFn: (home: string) => CredentialInspection = inspectClaudeKeychainStatus
|
|
594
|
-
):
|
|
599
|
+
): ClaudeCredentialOutcome {
|
|
595
600
|
const claudeCodeEntry = resolvedTools.find(({ tool }) => tool.id === 'claude-code');
|
|
596
601
|
if (!claudeCodeEntry) {
|
|
597
|
-
return;
|
|
602
|
+
return { status: 'NOT_APPLICABLE' };
|
|
598
603
|
}
|
|
599
604
|
|
|
600
605
|
let blob: string | null = null;
|
|
@@ -602,33 +607,51 @@ export function assertClaudeCredentialsAvailable(
|
|
|
602
607
|
const hasCustomExtractFn = extractFn !== extractClaudeCredentialsBlob;
|
|
603
608
|
if (hasCustomInspectFn || !hasCustomExtractFn) {
|
|
604
609
|
const inspection = inspectFn(home);
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
610
|
+
switch (inspection.status) {
|
|
611
|
+
case 'OK':
|
|
612
|
+
blob = inspection.blob;
|
|
613
|
+
break;
|
|
614
|
+
case 'MISSING':
|
|
615
|
+
return { status: 'SKIPPED' };
|
|
616
|
+
case 'KEYCHAIN_LOCKED':
|
|
617
|
+
throw new Error([
|
|
618
|
+
'Claude Code credentials are stored in the macOS keychain, but the keychain is locked.',
|
|
619
|
+
'',
|
|
620
|
+
buildLockedGuidance()
|
|
621
|
+
].join('\n'));
|
|
622
|
+
case 'STALE_ACCESS':
|
|
623
|
+
throw new Error([
|
|
624
|
+
'Claude Code credentials on host are invalid or expired.',
|
|
625
|
+
'',
|
|
626
|
+
'The sandbox needs valid Claude Code OAuth credentials so the container can use Claude Code.',
|
|
627
|
+
'',
|
|
628
|
+
'To fix:',
|
|
629
|
+
' 1. On the host, run "claude" once and complete the OAuth login flow.',
|
|
630
|
+
' 2. Verify with "claude /status" that you see your subscription.',
|
|
631
|
+
' 3. Re-run "ai sandbox create".'
|
|
632
|
+
].join('\n'));
|
|
633
|
+
case 'KEYCHAIN_ERROR':
|
|
634
|
+
throw new Error([
|
|
635
|
+
'Claude Code credentials could not be read from the host keychain.',
|
|
636
|
+
'',
|
|
637
|
+
inspection.detail ? `Detail: ${inspection.detail}` : 'Detail: unknown keychain error',
|
|
638
|
+
'',
|
|
639
|
+
'To fix:',
|
|
640
|
+
' 1. Unlock or repair the host keychain, then re-run "ai sandbox create".',
|
|
641
|
+
' 2. For SSH / CI, set AGENT_INFRA_CLAUDE_CREDENTIALS_FILE to an absolute path containing a valid credentials blob.'
|
|
642
|
+
].join('\n'));
|
|
643
|
+
default: {
|
|
644
|
+
const _exhaustive: never = inspection;
|
|
645
|
+
throw new Error(`Unhandled Claude Code credential inspection status: ${(_exhaustive as { status: string }).status}`);
|
|
646
|
+
}
|
|
611
647
|
}
|
|
612
|
-
blob = inspection.status === 'OK' ? inspection.blob : null;
|
|
613
648
|
} else {
|
|
614
649
|
blob = extractFn(home);
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
throw new Error([
|
|
619
|
-
'Claude Code credentials not found on host.',
|
|
620
|
-
'',
|
|
621
|
-
'The sandbox needs your Claude Code OAuth credentials so the container can use Claude Code.',
|
|
622
|
-
'',
|
|
623
|
-
'To fix:',
|
|
624
|
-
' 1. On the host, run "claude" once and complete the OAuth login flow.',
|
|
625
|
-
' 2. Verify with "claude /status" that you see your subscription.',
|
|
626
|
-
' 3. Re-run "ai sandbox create".',
|
|
627
|
-
'',
|
|
628
|
-
'Alternatively, if you do not need Claude Code in this sandbox,',
|
|
629
|
-
'remove "claude-code" from the "sandbox.tools" array in .agents/.airc.json.'
|
|
630
|
-
].join('\n'));
|
|
650
|
+
if (!blob) {
|
|
651
|
+
return { status: 'SKIPPED' };
|
|
652
|
+
}
|
|
631
653
|
}
|
|
632
654
|
|
|
633
655
|
writeFn(home, project, blob);
|
|
656
|
+
return { status: 'OK' };
|
|
634
657
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fitlab-ai/agent-infra",
|
|
3
|
-
"version": "0.6.2
|
|
3
|
+
"version": "0.6.2",
|
|
4
4
|
"description": "Bootstrap tool for AI multi-tool collaboration infrastructure — works with Claude Code, Codex, Gemini CLI, and OpenCode",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -149,13 +149,13 @@ gh api "repos/$upstream_repo/issues/{issue-number}" -X PATCH \
|
|
|
149
149
|
|
|
150
150
|
Failure is non-blocking.
|
|
151
151
|
|
|
152
|
-
###
|
|
152
|
+
### 7. Set Issue Fields (Optional)
|
|
153
153
|
|
|
154
154
|
If `has_push=true`, read `.agents/rules/issue-fields.md` and follow Flow A to write any applicable non-empty `priority`, `effort`, `start_date`, and `target_date` values from `task.md`.
|
|
155
155
|
|
|
156
156
|
Field write failures are non-blocking.
|
|
157
157
|
|
|
158
|
-
###
|
|
158
|
+
### 8. Write Back task.md
|
|
159
159
|
|
|
160
160
|
Update task.md:
|
|
161
161
|
|
|
@@ -164,7 +164,7 @@ Update task.md:
|
|
|
164
164
|
|
|
165
165
|
> Do NOT append an Activity Log entry here. The Issue creation event is already captured by the GitHub Issue itself and by the frontmatter `issue_number` field; the Activity Log only records the single `create-task` skill execution anchor (`Task Created`), written by the caller SKILL step 3.
|
|
166
166
|
|
|
167
|
-
###
|
|
167
|
+
### 9. Return the Result
|
|
168
168
|
|
|
169
169
|
Hand the following back to the caller `create-task`:
|
|
170
170
|
|
|
@@ -149,13 +149,13 @@ gh api "repos/$upstream_repo/issues/{issue-number}" -X PATCH \
|
|
|
149
149
|
|
|
150
150
|
设置失败不阻断流程。
|
|
151
151
|
|
|
152
|
-
###
|
|
152
|
+
### 7. 设置 Issue 字段(可选)
|
|
153
153
|
|
|
154
154
|
如果 `has_push=true`,读取 `.agents/rules/issue-fields.md`,按流程 A 写入 `task.md` 中适用且非空的 `priority`、`effort`、`start_date` 和 `target_date`。
|
|
155
155
|
|
|
156
156
|
字段写入失败不阻断流程。
|
|
157
157
|
|
|
158
|
-
###
|
|
158
|
+
### 8. 回写 task.md
|
|
159
159
|
|
|
160
160
|
更新 task.md:
|
|
161
161
|
|
|
@@ -164,7 +164,7 @@ gh api "repos/$upstream_repo/issues/{issue-number}" -X PATCH \
|
|
|
164
164
|
|
|
165
165
|
> 不要在此追加 Activity Log 条目。Issue 创建事件已由 GitHub Issue 自身和 frontmatter `issue_number` 承载;Activity Log 仅记录 `create-task` skill 一次执行的整体锚点(`Task Created`),由调用方 SKILL 步骤 3 写入。
|
|
166
166
|
|
|
167
|
-
###
|
|
167
|
+
### 9. 返回结果
|
|
168
168
|
|
|
169
169
|
把以下信息回传给调用方 `create-task`:
|
|
170
170
|
|
|
@@ -49,6 +49,8 @@ If `task.md` contains these source fields, also read the corresponding source in
|
|
|
49
49
|
|
|
50
50
|
### 4. Perform Requirements Analysis
|
|
51
51
|
|
|
52
|
+
Before analysis begins: if `start_date` in the frontmatter is empty, write today's date immediately (command: `date +%F`, format `YYYY-MM-DD`); keep any existing value. Before writing, read `.agents/rules/version-stamp.md` and refresh `updated_at` / `agent_infra_version` at the same time.
|
|
53
|
+
|
|
52
54
|
Follow the `analysis` step in `.agents/workflows/feature-development.yaml`:
|
|
53
55
|
|
|
54
56
|
**Required tasks** (analysis only, no business code changes):
|
|
@@ -126,6 +128,7 @@ If task.md contains a valid `issue_number`, perform these sync actions (skip and
|
|
|
126
128
|
- Set `status: pending-design-work` by following issue-sync.md
|
|
127
129
|
- Create or update the task comment marker defined in `.agents/rules/issue-sync.md` (follow the task.md comment sync rule in issue-sync.md)
|
|
128
130
|
- Publish the `{analysis-artifact}` comment
|
|
131
|
+
- Read `.agents/rules/issue-fields.md` and follow Flow A to sync every non-empty Issue field (`priority`/`effort`/`start_date`/`target_date`) from `task.md` to the Issue (idempotent; skip without blocking when `has_push=false` or the fetch/write fails)
|
|
129
132
|
|
|
130
133
|
### 7. Verification Gate
|
|
131
134
|
|
|
@@ -49,6 +49,8 @@ description: "分析任务并输出需求分析文档"
|
|
|
49
49
|
|
|
50
50
|
### 4. 执行需求分析
|
|
51
51
|
|
|
52
|
+
开始分析前:若 frontmatter 的 `start_date` 为空,立即写入当日日期(命令 `date +%F`,格式 `YYYY-MM-DD`);已有值则保留。写入前先读取 `.agents/rules/version-stamp.md`,并同步刷新 `updated_at` / `agent_infra_version`。
|
|
53
|
+
|
|
52
54
|
遵循 `.agents/workflows/feature-development.yaml` 中的 `analysis` 步骤:
|
|
53
55
|
|
|
54
56
|
**必要任务**(仅分析,不编写业务代码):
|
|
@@ -126,6 +128,7 @@ date "+%Y-%m-%d %H:%M:%S%:z"
|
|
|
126
128
|
- 按 issue-sync.md 设置 `status: pending-design-work`
|
|
127
129
|
- 创建或更新 `.agents/rules/issue-sync.md` 中定义的 task 评论标记(按 issue-sync.md 的 task.md 评论同步规则)
|
|
128
130
|
- 发布 `{analysis-artifact}` 评论
|
|
131
|
+
- 读取 `.agents/rules/issue-fields.md`,按流程 A 把 `task.md` 中所有非空的 Issue 字段(`priority`/`effort`/`start_date`/`target_date`)同步到 Issue(幂等;`has_push=false` 或取数/写入失败时跳过,不阻断)
|
|
129
132
|
|
|
130
133
|
### 7. 完成校验
|
|
131
134
|
|
|
@@ -98,6 +98,7 @@ If a valid `issue_number` exists:
|
|
|
98
98
|
- Backfill checked `## Requirements` items to the Issue body by following the requirements-checkbox sync steps in issue-sync.md
|
|
99
99
|
- Do not set any `status:` label — status labels are automatically cleared when the Issue is closed
|
|
100
100
|
- Finally create or update the summary comment marked with the summary marker defined in `.agents/rules/issue-sync.md`
|
|
101
|
+
- Read `.agents/rules/issue-fields.md` and follow Flow A to sync every non-empty Issue field (`priority`/`effort`/`start_date`/`target_date`) from `task.md` to the Issue (idempotent; skip without blocking when `has_push=false` or the fetch/write fails)
|
|
101
102
|
|
|
102
103
|
### 7. Verification Gate
|
|
103
104
|
|
|
@@ -98,6 +98,7 @@ ls .agents/workspace/completed/{task-id}/task.md
|
|
|
98
98
|
- 按 issue-sync.md 的需求复选框同步步骤,兜底同步 `## 需求` 中已勾选的条目到 Issue body
|
|
99
99
|
- 不要设置 `status:` label — Issue 关闭后 status label 会被自动清除
|
|
100
100
|
- 最后创建或更新 `.agents/rules/issue-sync.md` 中定义的 summary 评论标记对应的 summary 评论
|
|
101
|
+
- 读取 `.agents/rules/issue-fields.md`,按流程 A 把 `task.md` 中所有非空的 Issue 字段(`priority`/`effort`/`start_date`/`target_date`)同步到 Issue(幂等;`has_push=false` 或取数/写入失败时跳过,不阻断)
|
|
101
102
|
|
|
102
103
|
### 7. 完成校验
|
|
103
104
|
|
|
@@ -78,8 +78,8 @@ created_at: {YYYY-MM-DD HH:mm:ss±HH:MM}
|
|
|
78
78
|
updated_at: {YYYY-MM-DD HH:mm:ss±HH:MM}
|
|
79
79
|
agent_infra_version: {agent_infra_version}
|
|
80
80
|
created_by: human
|
|
81
|
-
priority: #
|
|
82
|
-
effort: #
|
|
81
|
+
priority: # required; inferred by the AI from the title/description; Urgent | High | Medium | Low
|
|
82
|
+
effort: # required; inferred by the AI from the title/description; High | Medium | Low
|
|
83
83
|
start_date: # optional; YYYY-MM-DD
|
|
84
84
|
target_date: # optional; YYYY-MM-DD
|
|
85
85
|
current_step: requirement-analysis
|
|
@@ -87,7 +87,7 @@ assigned_to: {current AI agent}
|
|
|
87
87
|
```
|
|
88
88
|
|
|
89
89
|
Note: `created_by` is `human` because the task comes from the user's description.
|
|
90
|
-
|
|
90
|
+
priority / effort are required: the AI infers them from the task title and description (candidates in `.agents/rules/issue-fields.md`; normalize localized input). Leave start_date / target_date empty at creation - analyze-task / plan-task fill them later; do not invent dates.
|
|
91
91
|
|
|
92
92
|
### 3. Update Task Status
|
|
93
93
|
|
|
@@ -78,8 +78,8 @@ created_at: {YYYY-MM-DD HH:mm:ss±HH:MM}
|
|
|
78
78
|
updated_at: {YYYY-MM-DD HH:mm:ss±HH:MM}
|
|
79
79
|
agent_infra_version: {agent_infra_version}
|
|
80
80
|
created_by: human
|
|
81
|
-
priority: #
|
|
82
|
-
effort: #
|
|
81
|
+
priority: # 必填;由 AI 从标题/描述推断;Urgent | High | Medium | Low
|
|
82
|
+
effort: # 必填;由 AI 从标题/描述推断;High | Medium | Low
|
|
83
83
|
start_date: # 可选;YYYY-MM-DD
|
|
84
84
|
target_date: # 可选;YYYY-MM-DD
|
|
85
85
|
current_step: requirement-analysis
|
|
@@ -87,7 +87,7 @@ assigned_to: {当前 AI 代理}
|
|
|
87
87
|
```
|
|
88
88
|
|
|
89
89
|
注意:`created_by` 为 `human`,因为任务来源于用户的描述。
|
|
90
|
-
|
|
90
|
+
priority / effort 必填:由 AI 从任务标题与描述推断后填入(候选值见 `.agents/rules/issue-fields.md`;中文输入按本地化映射规范化)。start_date / target_date 创建时保持留空,由 analyze-task / plan-task 阶段填入;不要臆测日期。
|
|
91
91
|
|
|
92
92
|
### 3. 更新任务状态
|
|
93
93
|
|
|
@@ -91,6 +91,7 @@ Update `.agents/workspace/active/{task-id}/task.md`:
|
|
|
91
91
|
- `assigned_to`: {current AI agent}
|
|
92
92
|
- `updated_at`: {current time}
|
|
93
93
|
- `agent_infra_version`: value from `.agents/rules/version-stamp.md`
|
|
94
|
+
- If `target_date` is empty, write an estimated completion date based on the effort estimate (`YYYY-MM-DD`); leave it empty without blocking when no reasonable estimate exists; keep any existing value
|
|
94
95
|
- Record the plan artifact for this round: `{plan-artifact}` (Round `{plan-round}`)
|
|
95
96
|
- If the task template contains a `## Design` section, update it to link to `{plan-artifact}`
|
|
96
97
|
- Mark technical-design as complete in workflow progress and include the actual round when the task template supports it
|
|
@@ -104,6 +105,7 @@ If task.md contains a valid `issue_number`, perform these sync actions (skip and
|
|
|
104
105
|
- Set `status: pending-design-work` by following issue-sync.md
|
|
105
106
|
- Create or update the task comment marker defined in `.agents/rules/issue-sync.md` (follow the task.md comment sync rule in issue-sync.md)
|
|
106
107
|
- Publish the `{plan-artifact}` comment
|
|
108
|
+
- Read `.agents/rules/issue-fields.md` and follow Flow A to sync every non-empty Issue field (`priority`/`effort`/`start_date`/`target_date`) from `task.md` to the Issue (idempotent; skip without blocking when `has_push=false` or the fetch/write fails)
|
|
107
109
|
|
|
108
110
|
### 8. Verification Gate
|
|
109
111
|
|
|
@@ -91,6 +91,7 @@ date "+%Y-%m-%d %H:%M:%S%:z"
|
|
|
91
91
|
- `assigned_to`:{当前 AI 代理}
|
|
92
92
|
- `updated_at`:{当前时间}
|
|
93
93
|
- `agent_infra_version`:按 `.agents/rules/version-stamp.md` 取值
|
|
94
|
+
- 若 `target_date` 为空,基于工作量评估写入预估完成日(`YYYY-MM-DD`);无法合理预估时保持留空、不阻塞;已有值则保留
|
|
94
95
|
- 记录本轮方案产物:`{plan-artifact}`(Round `{plan-round}`)
|
|
95
96
|
- 如任务模板包含 `## 设计` 段落,更新为指向 `{plan-artifact}` 的链接
|
|
96
97
|
- 在工作流进度中标记 technical-design 为已完成,并注明实际轮次(如果任务模板支持)
|
|
@@ -104,6 +105,7 @@ date "+%Y-%m-%d %H:%M:%S%:z"
|
|
|
104
105
|
- 按 issue-sync.md 设置 `status: pending-design-work`
|
|
105
106
|
- 创建或更新 `.agents/rules/issue-sync.md` 中定义的 task 评论标记(按 issue-sync.md 的 task.md 评论同步规则)
|
|
106
107
|
- 发布 `{plan-artifact}` 评论
|
|
108
|
+
- 读取 `.agents/rules/issue-fields.md`,按流程 A 把 `task.md` 中所有非空的 Issue 字段(`priority`/`effort`/`start_date`/`target_date`)同步到 Issue(幂等;`has_push=false` 或取数/写入失败时跳过,不阻断)
|
|
107
109
|
|
|
108
110
|
### 8. 完成校验
|
|
109
111
|
|