@fitlab-ai/agent-infra 0.7.1 → 0.7.3

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 (165) hide show
  1. package/README.md +7 -1
  2. package/README.zh-CN.md +9 -3
  3. package/bin/cli.ts +11 -0
  4. package/dist/bin/cli.js +12 -0
  5. package/dist/lib/defaults.json +0 -1
  6. package/dist/lib/init.js +0 -3
  7. package/dist/lib/sandbox/commands/create.js +10 -2
  8. package/dist/lib/sandbox/commands/enter.js +17 -18
  9. package/dist/lib/sandbox/commands/list-running.js +56 -32
  10. package/dist/lib/sandbox/commands/ls.js +27 -24
  11. package/dist/lib/sandbox/commands/start.js +36 -0
  12. package/dist/lib/sandbox/index.js +15 -3
  13. package/dist/lib/sandbox/task-resolver.js +1 -1
  14. package/dist/lib/sandbox/tools.js +1 -1
  15. package/dist/lib/table.js +38 -0
  16. package/dist/lib/task/commands/ls.js +122 -0
  17. package/dist/lib/task/commands/show.js +135 -0
  18. package/dist/lib/task/frontmatter.js +32 -0
  19. package/dist/lib/task/index.js +41 -0
  20. package/dist/lib/task/short-id.js +90 -0
  21. package/dist/lib/update.js +25 -8
  22. package/lib/defaults.json +0 -1
  23. package/lib/init.ts +0 -10
  24. package/lib/sandbox/commands/create.ts +11 -2
  25. package/lib/sandbox/commands/enter.ts +40 -20
  26. package/lib/sandbox/commands/list-running.ts +65 -37
  27. package/lib/sandbox/commands/ls.ts +35 -27
  28. package/lib/sandbox/commands/start.ts +61 -0
  29. package/lib/sandbox/index.ts +15 -3
  30. package/lib/sandbox/task-resolver.ts +1 -1
  31. package/lib/sandbox/tools.ts +1 -1
  32. package/lib/table.ts +44 -0
  33. package/lib/task/commands/ls.ts +138 -0
  34. package/lib/task/commands/show.ts +139 -0
  35. package/lib/task/frontmatter.ts +30 -0
  36. package/lib/task/index.ts +44 -0
  37. package/lib/task/short-id.ts +107 -0
  38. package/lib/update.ts +28 -10
  39. package/package.json +1 -1
  40. package/templates/.agents/hooks/auto-resume.sh +104 -0
  41. package/templates/.agents/rules/create-issue.github.en.md +1 -1
  42. package/templates/.agents/rules/create-issue.github.zh-CN.md +1 -1
  43. package/templates/.agents/rules/milestone-inference.github.en.md +4 -1
  44. package/templates/.agents/rules/milestone-inference.github.zh-CN.md +4 -1
  45. package/templates/.agents/rules/next-step-output.en.md +62 -0
  46. package/templates/.agents/rules/next-step-output.zh-CN.md +62 -0
  47. package/templates/.agents/rules/pr-checks-commands.en.md +5 -0
  48. package/templates/.agents/rules/pr-checks-commands.github.en.md +62 -0
  49. package/templates/.agents/rules/pr-checks-commands.github.zh-CN.md +62 -0
  50. package/templates/.agents/rules/pr-checks-commands.zh-CN.md +5 -0
  51. package/templates/.agents/rules/pr-sync.github.en.md +7 -0
  52. package/templates/.agents/rules/pr-sync.github.zh-CN.md +7 -0
  53. package/templates/.agents/rules/task-short-id.en.md +54 -62
  54. package/templates/.agents/rules/task-short-id.zh-CN.md +35 -54
  55. package/templates/.agents/scripts/platform-adapters/platform-sync.github.js +17 -0
  56. package/templates/.agents/scripts/task-short-id.js +32 -189
  57. package/templates/.agents/skills/analyze-task/SKILL.en.md +10 -12
  58. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +10 -12
  59. package/templates/.agents/skills/analyze-task/config/verify.en.json +1 -1
  60. package/templates/.agents/skills/analyze-task/config/verify.zh-CN.json +1 -1
  61. package/templates/.agents/skills/block-task/SKILL.en.md +13 -6
  62. package/templates/.agents/skills/block-task/SKILL.zh-CN.md +13 -6
  63. package/templates/.agents/skills/block-task/config/verify.json +1 -1
  64. package/templates/.agents/skills/cancel-task/SKILL.en.md +13 -6
  65. package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +13 -6
  66. package/templates/.agents/skills/cancel-task/config/verify.json +1 -1
  67. package/templates/.agents/skills/check-task/SKILL.en.md +12 -10
  68. package/templates/.agents/skills/check-task/SKILL.zh-CN.md +12 -10
  69. package/templates/.agents/skills/close-codescan/SKILL.en.md +13 -6
  70. package/templates/.agents/skills/close-codescan/SKILL.zh-CN.md +13 -6
  71. package/templates/.agents/skills/close-dependabot/SKILL.en.md +13 -6
  72. package/templates/.agents/skills/close-dependabot/SKILL.zh-CN.md +13 -6
  73. package/templates/.agents/skills/code-task/SKILL.en.md +10 -6
  74. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +11 -6
  75. package/templates/.agents/skills/code-task/config/verify.en.json +2 -1
  76. package/templates/.agents/skills/code-task/config/verify.zh-CN.json +2 -1
  77. package/templates/.agents/skills/code-task/reference/fix-mode.en.md +10 -5
  78. package/templates/.agents/skills/code-task/reference/fix-mode.zh-CN.md +10 -5
  79. package/templates/.agents/skills/code-task/reference/output-template.en.md +3 -3
  80. package/templates/.agents/skills/code-task/reference/output-template.zh-CN.md +3 -3
  81. package/templates/.agents/skills/code-task/reference/report-template.en.md +8 -0
  82. package/templates/.agents/skills/code-task/reference/report-template.zh-CN.md +8 -0
  83. package/templates/.agents/skills/commit/SKILL.en.md +3 -4
  84. package/templates/.agents/skills/commit/SKILL.zh-CN.md +3 -4
  85. package/templates/.agents/skills/commit/reference/task-status-update.en.md +37 -29
  86. package/templates/.agents/skills/commit/reference/task-status-update.zh-CN.md +37 -29
  87. package/templates/.agents/skills/complete-task/SKILL.en.md +41 -4
  88. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +41 -4
  89. package/templates/.agents/skills/complete-task/config/verify.en.json +1 -1
  90. package/templates/.agents/skills/complete-task/config/verify.zh-CN.json +1 -1
  91. package/templates/.agents/skills/create-pr/SKILL.en.md +20 -11
  92. package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +20 -11
  93. package/templates/.agents/skills/create-pr/config/verify.json +2 -1
  94. package/templates/.agents/skills/create-pr/reference/comment-publish.en.md +2 -1
  95. package/templates/.agents/skills/create-pr/reference/comment-publish.zh-CN.md +2 -1
  96. package/templates/.agents/skills/create-pr/reference/pr-body-template.en.md +3 -3
  97. package/templates/.agents/skills/create-pr/reference/pr-body-template.zh-CN.md +3 -3
  98. package/templates/.agents/skills/create-task/SKILL.en.md +17 -17
  99. package/templates/.agents/skills/create-task/SKILL.zh-CN.md +17 -17
  100. package/templates/.agents/skills/create-task/config/verify.json +1 -1
  101. package/templates/.agents/skills/import-codescan/SKILL.en.md +8 -8
  102. package/templates/.agents/skills/import-codescan/SKILL.zh-CN.md +8 -8
  103. package/templates/.agents/skills/import-codescan/config/verify.json +1 -1
  104. package/templates/.agents/skills/import-dependabot/SKILL.en.md +8 -8
  105. package/templates/.agents/skills/import-dependabot/SKILL.zh-CN.md +8 -8
  106. package/templates/.agents/skills/import-dependabot/config/verify.json +1 -1
  107. package/templates/.agents/skills/import-issue/SKILL.en.md +7 -7
  108. package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +7 -7
  109. package/templates/.agents/skills/plan-task/SKILL.en.md +10 -12
  110. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +10 -12
  111. package/templates/.agents/skills/plan-task/config/verify.en.json +1 -1
  112. package/templates/.agents/skills/plan-task/config/verify.zh-CN.json +1 -1
  113. package/templates/.agents/skills/restore-task/SKILL.en.md +1 -1
  114. package/templates/.agents/skills/restore-task/SKILL.zh-CN.md +1 -1
  115. package/templates/.agents/skills/review-analysis/SKILL.en.md +4 -2
  116. package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +4 -2
  117. package/templates/.agents/skills/review-analysis/config/verify.en.json +3 -2
  118. package/templates/.agents/skills/review-analysis/config/verify.zh-CN.json +3 -2
  119. package/templates/.agents/skills/review-analysis/reference/output-templates.en.md +15 -15
  120. package/templates/.agents/skills/review-analysis/reference/output-templates.zh-CN.md +15 -15
  121. package/templates/.agents/skills/review-analysis/reference/report-template.en.md +7 -1
  122. package/templates/.agents/skills/review-analysis/reference/report-template.zh-CN.md +7 -1
  123. package/templates/.agents/skills/review-analysis/reference/review-criteria.en.md +2 -0
  124. package/templates/.agents/skills/review-analysis/reference/review-criteria.zh-CN.md +2 -0
  125. package/templates/.agents/skills/review-code/SKILL.en.md +5 -2
  126. package/templates/.agents/skills/review-code/SKILL.zh-CN.md +5 -2
  127. package/templates/.agents/skills/review-code/config/verify.en.json +3 -2
  128. package/templates/.agents/skills/review-code/config/verify.zh-CN.json +3 -2
  129. package/templates/.agents/skills/review-code/reference/output-templates.en.md +9 -9
  130. package/templates/.agents/skills/review-code/reference/output-templates.zh-CN.md +9 -9
  131. package/templates/.agents/skills/review-code/reference/report-template.en.md +7 -1
  132. package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +7 -1
  133. package/templates/.agents/skills/review-code/reference/review-criteria.en.md +2 -0
  134. package/templates/.agents/skills/review-code/reference/review-criteria.zh-CN.md +2 -0
  135. package/templates/.agents/skills/review-plan/SKILL.en.md +4 -2
  136. package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +4 -2
  137. package/templates/.agents/skills/review-plan/config/verify.en.json +3 -2
  138. package/templates/.agents/skills/review-plan/config/verify.zh-CN.json +3 -2
  139. package/templates/.agents/skills/review-plan/reference/output-templates.en.md +15 -15
  140. package/templates/.agents/skills/review-plan/reference/output-templates.zh-CN.md +15 -15
  141. package/templates/.agents/skills/review-plan/reference/report-template.en.md +7 -1
  142. package/templates/.agents/skills/review-plan/reference/report-template.zh-CN.md +7 -1
  143. package/templates/.agents/skills/review-plan/reference/review-criteria.en.md +2 -0
  144. package/templates/.agents/skills/review-plan/reference/review-criteria.zh-CN.md +2 -0
  145. package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +0 -1
  146. package/templates/.agents/skills/watch-pr/SKILL.en.md +131 -0
  147. package/templates/.agents/skills/watch-pr/SKILL.zh-CN.md +131 -0
  148. package/templates/.agents/skills/watch-pr/config/verify.json +22 -0
  149. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.en.md +43 -0
  150. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.zh-CN.md +43 -0
  151. package/templates/.agents/templates/task.en.md +1 -1
  152. package/templates/.agents/templates/task.zh-CN.md +1 -1
  153. package/templates/.agents/workflows/bug-fix.en.yaml +7 -5
  154. package/templates/.agents/workflows/bug-fix.zh-CN.yaml +6 -5
  155. package/templates/.agents/workflows/feature-development.en.yaml +7 -5
  156. package/templates/.agents/workflows/feature-development.zh-CN.yaml +6 -5
  157. package/templates/.agents/workflows/refactoring.en.yaml +7 -5
  158. package/templates/.agents/workflows/refactoring.zh-CN.yaml +6 -5
  159. package/templates/.claude/commands/watch-pr.en.md +8 -0
  160. package/templates/.claude/commands/watch-pr.zh-CN.md +8 -0
  161. package/templates/.claude/settings.json +11 -0
  162. package/templates/.gemini/commands/_project_/watch-pr.en.toml +8 -0
  163. package/templates/.gemini/commands/_project_/watch-pr.zh-CN.toml +8 -0
  164. package/templates/.opencode/commands/watch-pr.en.md +11 -0
  165. package/templates/.opencode/commands/watch-pr.zh-CN.md +11 -0
@@ -0,0 +1,30 @@
1
+ type Frontmatter = Record<string, string>;
2
+
3
+ function parseTaskFrontmatter(content: string): Frontmatter {
4
+ const result: Frontmatter = {};
5
+ if (!content.startsWith('---')) return result;
6
+ const end = content.indexOf('\n---', 3);
7
+ if (end === -1) return result;
8
+ const body = content.slice(3, end);
9
+ for (const rawLine of body.split('\n')) {
10
+ const line = rawLine.replace(/\r$/, '');
11
+ if (!line.trim()) continue;
12
+ const colon = line.indexOf(':');
13
+ if (colon === -1) continue;
14
+ const key = line.slice(0, colon).trim();
15
+ const value = line.slice(colon + 1).trim();
16
+ if (key) result[key] = value;
17
+ }
18
+ return result;
19
+ }
20
+
21
+ function extractTitle(content: string): string {
22
+ for (const line of content.split('\n')) {
23
+ const m = /^#\s+(?:任务[::]?\s*)?(.+)$/.exec(line.trim());
24
+ if (m && m[1]) return m[1].trim();
25
+ }
26
+ return '';
27
+ }
28
+
29
+ export { parseTaskFrontmatter, extractTitle };
30
+ export type { Frontmatter };
@@ -0,0 +1,44 @@
1
+ const USAGE = `Usage: ai task <command> [options]
2
+
3
+ Commands:
4
+ ls [--all | --blocked | --completed] List tasks (default: active)
5
+ show <N | #N | TASK-id> Print a task.md
6
+
7
+ Examples:
8
+ ai task ls
9
+ ai task show 11
10
+ ai task show TASK-20260612-162737
11
+
12
+ Run 'ai task <command> --help' for details.`;
13
+
14
+ export async function runTask(args: string[]): Promise<void> {
15
+ const [subcommand, ...rest] = args;
16
+
17
+ if (!subcommand) {
18
+ process.stdout.write(`${USAGE}\n`);
19
+ process.exitCode = 1;
20
+ return;
21
+ }
22
+
23
+ if (subcommand === '--help' || subcommand === '-h' || subcommand === 'help') {
24
+ process.stdout.write(`${USAGE}\n`);
25
+ return;
26
+ }
27
+
28
+ switch (subcommand) {
29
+ case 'ls': {
30
+ const { ls } = await import('./commands/ls.ts');
31
+ ls(rest);
32
+ break;
33
+ }
34
+ case 'show': {
35
+ const { show } = await import('./commands/show.ts');
36
+ show(rest);
37
+ break;
38
+ }
39
+ default:
40
+ process.stderr.write(`Unknown task command: ${subcommand}\n\n`);
41
+ process.stdout.write(`${USAGE}\n`);
42
+ process.exitCode = 1;
43
+ }
44
+ }
@@ -0,0 +1,107 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+
4
+ const REGISTRY_NAME = '.short-ids.json';
5
+
6
+ type NormalizeResult =
7
+ | { kind: 'shortId'; value: string }
8
+ | { kind: 'pass'; value: string }
9
+ | { kind: 'error'; message: string };
10
+
11
+ type NormalizeOpts = { shortIdLength: number };
12
+
13
+ function normalizeShortIdInput(input: string, opts: NormalizeOpts): NormalizeResult {
14
+ const L = opts.shortIdLength;
15
+ const m = /^#?(\d+)$/.exec(input);
16
+ if (!m) {
17
+ return { kind: 'pass', value: input };
18
+ }
19
+ const n = Number(m[1]);
20
+ if (n === 0) {
21
+ return {
22
+ kind: 'error',
23
+ message: `short id '${input}' is invalid (#${'0'.repeat(L)} is reserved)`
24
+ };
25
+ }
26
+ const max = Math.pow(10, L) - 1;
27
+ if (n > max) {
28
+ return {
29
+ kind: 'error',
30
+ message: `short id ${n} exceeds shortIdLength=${L} capacity (max=${max}); archive tasks or raise task.shortIdLength in .agents/.airc.json`
31
+ };
32
+ }
33
+ return { kind: 'shortId', value: `#${String(n).padStart(L, '0')}` };
34
+ }
35
+
36
+ type RegistrySchema = {
37
+ version: number;
38
+ ids: Record<string, string>;
39
+ };
40
+
41
+ function readRegistry(repoRoot: string): RegistrySchema | null {
42
+ const registryPath = path.join(repoRoot, '.agents', 'workspace', 'active', REGISTRY_NAME);
43
+ if (!fs.existsSync(registryPath)) return null;
44
+ try {
45
+ const raw = fs.readFileSync(registryPath, 'utf8');
46
+ const data = JSON.parse(raw) as RegistrySchema;
47
+ if (!data || typeof data !== 'object' || !data.ids) return null;
48
+ return data;
49
+ } catch {
50
+ return null;
51
+ }
52
+ }
53
+
54
+ function readBranchFromTaskMd(repoRoot: string, taskId: string): string | null {
55
+ const taskMdPath = path.join(repoRoot, '.agents', 'workspace', 'active', taskId, 'task.md');
56
+ if (!fs.existsSync(taskMdPath)) return null;
57
+ const content = fs.readFileSync(taskMdPath, 'utf8');
58
+ const m = content.match(/^branch:\s*(.+)$/m);
59
+ if (!m || !m[1]) return null;
60
+ return m[1].trim().replace(/^(["'])(.*)\1$/, '$2');
61
+ }
62
+
63
+ function loadShortIdByTaskId(repoRoot: string): Map<string, string> {
64
+ const registry = readRegistry(repoRoot);
65
+ const map = new Map<string, string>();
66
+ if (!registry) return map;
67
+ for (const [key, taskId] of Object.entries(registry.ids)) {
68
+ map.set(taskId, `#${key}`);
69
+ }
70
+ return map;
71
+ }
72
+
73
+ /**
74
+ * Resolve a branch to its active-task short id (`#NN`), or `null` when no
75
+ * active task is bound to that branch.
76
+ *
77
+ * Two-state semantics: this only consults the active registry
78
+ * (`active/.short-ids.json`) plus each `active/{taskId}/task.md`. Tasks moved
79
+ * to completed/blocked/cancelled/archive have already released their short id,
80
+ * so their branches return `null` — in `ai sandbox ls` that surfaces as `-`,
81
+ * meaning the sandbox is free to remove.
82
+ */
83
+ function lookupShortIdByBranch(
84
+ branch: string,
85
+ repoRoot: string,
86
+ _opts?: { shortIdLength?: number }
87
+ ): string | null {
88
+ const registry = readRegistry(repoRoot);
89
+ if (!registry) return null;
90
+ const matches: string[] = [];
91
+ for (const [key, taskId] of Object.entries(registry.ids)) {
92
+ const taskBranch = readBranchFromTaskMd(repoRoot, taskId);
93
+ if (taskBranch && taskBranch === branch) {
94
+ matches.push(`#${key}`);
95
+ }
96
+ }
97
+ if (matches.length === 0) return null;
98
+ if (matches.length > 1) {
99
+ process.stderr.write(
100
+ `Warning: branch '${branch}' is bound to multiple active tasks: ${matches.join(', ')}; using ${matches[0]}\n`
101
+ );
102
+ }
103
+ return matches[0]!;
104
+ }
105
+
106
+ export { normalizeShortIdInput, lookupShortIdByBranch, loadShortIdByTaskId };
107
+ export type { NormalizeResult, NormalizeOpts };
package/lib/update.ts CHANGED
@@ -17,7 +17,8 @@ type UpdateConfig = {
17
17
  org: string;
18
18
  language: string;
19
19
  platform?: { type?: string };
20
- requiresPullRequest?: boolean;
20
+ requiresPullRequest?: boolean; // legacy field; read-only, migrated to prFlow then removed
21
+ prFlow?: 'required' | 'disabled';
21
22
  sandbox?: Record<string, unknown>;
22
23
  task?: { shortIdLength: number };
23
24
  labels?: Record<string, unknown>;
@@ -27,7 +28,6 @@ type UpdateConfig = {
27
28
 
28
29
  type Defaults = {
29
30
  platform: { type: string };
30
- requiresPullRequest: boolean;
31
31
  sandbox: Record<string, unknown>;
32
32
  task: { shortIdLength: number };
33
33
  labels: Record<string, unknown>;
@@ -41,6 +41,25 @@ const defaults = JSON.parse(
41
41
  const CONFIG_DIR = '.agents';
42
42
  const CONFIG_PATH = path.join(CONFIG_DIR, '.airc.json');
43
43
 
44
+ // One-time migration of the legacy project-level PR switch to the three-state
45
+ // `prFlow` preference. `true` (the old default / "PR flow on") maps to the
46
+ // strong constraint `required`; `false` maps to `disabled`. A missing or
47
+ // already-migrated config is left untouched (idempotent). Returns the new
48
+ // prFlow value when a migration happened, otherwise null.
49
+ function migratePrFlow(config: UpdateConfig): 'required' | 'disabled' | null {
50
+ if (config.requiresPullRequest === true) {
51
+ delete config.requiresPullRequest;
52
+ config.prFlow = 'required';
53
+ return 'required';
54
+ }
55
+ if (config.requiresPullRequest === false) {
56
+ delete config.requiresPullRequest;
57
+ config.prFlow = 'disabled';
58
+ return 'disabled';
59
+ }
60
+ return null;
61
+ }
62
+
44
63
  function isPathOwnedByOtherPlatform(relativePath: string, platformType: string): boolean {
45
64
  const top = String(relativePath || '').replace(/\\/g, '/').replace(/^\.\//, '').split('/')[0] ?? '';
46
65
  if (!top.startsWith('.')) return false;
@@ -195,7 +214,7 @@ async function cmdUpdate(): Promise<void> {
195
214
  const sandboxAdded = !config.sandbox;
196
215
  const taskAdded = !config.task;
197
216
  const labelsAdded = !config.labels;
198
- const requiresPullRequestAdded = config.requiresPullRequest === undefined;
217
+ const prFlowMigrated = migratePrFlow(config);
199
218
  let configChanged = changed;
200
219
 
201
220
  if (platformAdded) {
@@ -218,8 +237,7 @@ async function cmdUpdate(): Promise<void> {
218
237
  configChanged = true;
219
238
  }
220
239
 
221
- if (requiresPullRequestAdded) {
222
- config.requiresPullRequest = defaults.requiresPullRequest;
240
+ if (prFlowMigrated) {
223
241
  configChanged = true;
224
242
  }
225
243
 
@@ -233,7 +251,7 @@ async function cmdUpdate(): Promise<void> {
233
251
  for (const entry of added.merged) {
234
252
  ok(` merged: ${entry}`);
235
253
  }
236
- } else if (platformAdded || sandboxAdded || taskAdded || labelsAdded || requiresPullRequestAdded) {
254
+ } else if (platformAdded || sandboxAdded || taskAdded || labelsAdded || prFlowMigrated) {
237
255
  if (platformAdded) {
238
256
  info(`Default platform config added to ${CONFIG_PATH}.`);
239
257
  }
@@ -246,8 +264,8 @@ async function cmdUpdate(): Promise<void> {
246
264
  if (labelsAdded) {
247
265
  info(`Default labels.in config added to ${CONFIG_PATH}.`);
248
266
  }
249
- if (requiresPullRequestAdded) {
250
- info(`Default requiresPullRequest=${defaults.requiresPullRequest} added to ${CONFIG_PATH}.`);
267
+ if (prFlowMigrated) {
268
+ info(`Migrated legacy requiresPullRequest to prFlow="${prFlowMigrated}" in ${CONFIG_PATH}.`);
251
269
  }
252
270
  } else {
253
271
  info(`File registry changed in ${CONFIG_PATH}.`);
@@ -264,8 +282,8 @@ async function cmdUpdate(): Promise<void> {
264
282
  if (hasNewEntries && platformAdded) {
265
283
  info(`Default platform config added to ${CONFIG_PATH}.`);
266
284
  }
267
- if (hasNewEntries && requiresPullRequestAdded) {
268
- info(`Default requiresPullRequest=${defaults.requiresPullRequest} added to ${CONFIG_PATH}.`);
285
+ if (hasNewEntries && prFlowMigrated) {
286
+ info(`Migrated legacy requiresPullRequest to prFlow="${prFlowMigrated}" in ${CONFIG_PATH}.`);
269
287
  }
270
288
  fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2) + '\n', 'utf8');
271
289
  ok(`Updated ${CONFIG_PATH}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fitlab-ai/agent-infra",
3
- "version": "0.7.1",
3
+ "version": "0.7.3",
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",
@@ -0,0 +1,104 @@
1
+ #!/bin/sh
2
+ # StopFailure hook: auto-resume Claude Code after a recoverable API error.
3
+ #
4
+ # Fires when a turn ends due to an API error. Runs four gates and, if all pass,
5
+ # injects a "please continue" message into the current tmux pane via send-keys.
6
+ # StopFailure output and exit code are ignored by Claude Code, so recovery is
7
+ # delivered out-of-band through tmux; every exit path here returns 0 and the
8
+ # only observable trace is the log file.
9
+ #
10
+ # Intentionally NOT using `set -e`: the network probe, tmux and state-file
11
+ # writes may fail locally without warranting an abort of the whole script.
12
+
13
+ LOG="$HOME/.claude/auto-resume.log"
14
+ STATE_DIR="$HOME/.claude/auto-resume.state"
15
+ WHITELIST="unknown server_error overloaded"
16
+ WINDOW=1800
17
+ MAX=10
18
+ PROBE_URL="https://api.anthropic.com/"
19
+ PROBE_DEADLINE=60
20
+ RESUME_TEXT="Unexpected interruption. Please continue the unfinished operation."
21
+
22
+ log() {
23
+ mkdir -p "$HOME/.claude" 2>/dev/null
24
+ printf '%s %s\n' "$(date '+%Y-%m-%d %H:%M:%S%z')" "$1" >> "$LOG"
25
+ lines=$(wc -l < "$LOG" 2>/dev/null | tr -cd '0-9')
26
+ [ -z "$lines" ] && lines=0
27
+ if [ "$lines" -gt 5000 ]; then
28
+ tail -n 2500 "$LOG" > "$LOG.tmp" 2>/dev/null && mv "$LOG.tmp" "$LOG"
29
+ fi
30
+ }
31
+
32
+ # Read the StopFailure payload once, then extract session_id and error. The
33
+ # `error` field identifies the API error type and drives the whitelist gate.
34
+ payload=$(cat)
35
+ session_id=$(printf '%s' "$payload" | node -e 'let c=[];process.stdin.on("data",d=>c.push(d));process.stdin.on("end",()=>{try{const p=JSON.parse(Buffer.concat(c).toString());process.stdout.write(String(p.session_id||""))}catch{process.stdout.write("")}})' 2>/dev/null)
36
+ error=$(printf '%s' "$payload" | node -e 'let c=[];process.stdin.on("data",d=>c.push(d));process.stdin.on("end",()=>{try{const p=JSON.parse(Buffer.concat(c).toString());process.stdout.write(String(p.error||""))}catch{process.stdout.write("")}})' 2>/dev/null)
37
+
38
+ # Gate 1: only act inside a tmux pane; stay silent everywhere else.
39
+ if [ -z "$TMUX_PANE" ]; then
40
+ log "not in tmux, skip (error=$error)"
41
+ exit 0
42
+ fi
43
+
44
+ # Gate 2: only recover from the whitelisted, retriable error types.
45
+ case " $WHITELIST " in
46
+ *" $error "*) : ;;
47
+ *) log "blocked: non-recoverable error=$error"; exit 0 ;;
48
+ esac
49
+
50
+ # Gate 3: back off after MAX fires within a WINDOW-second sliding window per session.
51
+ mkdir -p "$STATE_DIR" 2>/dev/null
52
+ # Treat the payload session_id as untrusted: sanitize to a safe filename so a
53
+ # value like "../outside" cannot write the state file outside STATE_DIR.
54
+ safe_session=$(printf '%s' "${session_id:-nosession}" | tr -c 'A-Za-z0-9._-' '_')
55
+ f="$STATE_DIR/$safe_session.count"
56
+ now=$(date +%s)
57
+ if [ -f "$f" ]; then
58
+ awk -v n="$now" -v w="$WINDOW" '$1 > n - w' "$f" > "$f.tmp" 2>/dev/null && mv "$f.tmp" "$f"
59
+ fi
60
+ # BSD `wc -l` (macOS) pads the count with leading spaces; strip to bare digits
61
+ # so the integer compare and the log line stay portable across GNU/BSD.
62
+ count=$( [ -f "$f" ] && wc -l < "$f" 2>/dev/null | tr -cd '0-9' || echo 0 )
63
+ [ -z "$count" ] && count=0
64
+ if [ "$count" -ge "$MAX" ]; then
65
+ log "backoff: $count fires in 30m, skip (error=$error)"
66
+ exit 0
67
+ fi
68
+ echo "$now" >> "$f"
69
+
70
+ # Gate 4: wait until the API is reachable again, up to PROBE_DEADLINE seconds.
71
+ # No --fail: any HTTP response (incl. 401/404) proves TLS/network connectivity.
72
+ waited=0
73
+ until curl -s -o /dev/null --max-time 3 "$PROBE_URL"; do
74
+ waited=$((waited + 3))
75
+ if [ "$waited" -ge "$PROBE_DEADLINE" ]; then
76
+ log "probe timeout after ${waited}s, skip (error=$error)"
77
+ exit 0
78
+ fi
79
+ sleep 3
80
+ done
81
+ log "probe ok after ${waited}s (error=$error)"
82
+
83
+ # Inject the resume message with a deliberately timing-insensitive sequence:
84
+ # 1. Escape leaves any non-input TUI state.
85
+ # 2. A 1s settle covers every known TUI escape timeout (vim 1000ms,
86
+ # xterm/readline 50ms) so the next bytes are delivered as fresh input
87
+ # instead of being folded into the escape sequence (the dropped-`U` race).
88
+ # 3. The text travels through a NAMED paste buffer pasted with bracketed
89
+ # paste (-p): the TUI ingests it as a single paste rather than per-character
90
+ # keypresses, so no leading char is eaten and the body is not read as a
91
+ # submit. The named buffer (-b) guarantees we paste exactly this text, and
92
+ # -d deletes it afterward so the user's anonymous paste stack is untouched.
93
+ # 4. Enter is a separate send-keys after the paste, so the submit signal is
94
+ # never merged into the pasted content (the must-press-Enter race).
95
+ # Every step stays non-blocking (2>/dev/null, exit 0 below) and logs a WARN on
96
+ # failure so the log can localize which tmux step broke.
97
+ log "tmux inject start (error=$error)"
98
+ tmux send-keys -t "$TMUX_PANE" Escape 2>/dev/null || log "WARN: tmux Escape failed (error=$error)"
99
+ sleep 1
100
+ tmux set-buffer -b auto-resume -- "$RESUME_TEXT" 2>/dev/null || log "WARN: tmux set-buffer failed (error=$error)"
101
+ tmux paste-buffer -t "$TMUX_PANE" -b auto-resume -p -d 2>/dev/null || log "WARN: tmux paste-buffer failed (error=$error)"
102
+ tmux send-keys -t "$TMUX_PANE" Enter 2>/dev/null || log "WARN: tmux Enter failed (error=$error)"
103
+ log "tmux inject done (error=$error)"
104
+ exit 0
@@ -180,7 +180,7 @@ Update task.md:
180
180
  - Write `issue_number: {n}` into the frontmatter (replace if it exists; append at the end of the frontmatter otherwise)
181
181
  - Update `updated_at` to the current time (command: `date "+%Y-%m-%d %H:%M:%S%:z"`)
182
182
 
183
- > 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.
183
+ > 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 (`Create Task`), written by the caller SKILL step 3.
184
184
 
185
185
  ### 9. Return the Result
186
186
 
@@ -180,7 +180,7 @@ gh api "repos/$upstream_repo/issues/{issue-number}" -X PATCH \
180
180
  - 把 `issue_number: {n}` 写入 frontmatter(已存在则替换;不存在则在 frontmatter 末尾追加)
181
181
  - 更新 `updated_at` 为当前时间(命令:`date "+%Y-%m-%d %H:%M:%S%:z"`)
182
182
 
183
- > 不要在此追加 Activity Log 条目。Issue 创建事件已由 GitHub Issue 自身和 frontmatter `issue_number` 承载;Activity Log 仅记录 `create-task` skill 一次执行的整体锚点(`Task Created`),由调用方 SKILL 步骤 3 写入。
183
+ > 不要在此追加 Activity Log 条目。Issue 创建事件已由 GitHub Issue 自身和 frontmatter `issue_number` 承载;Activity Log 仅记录 `create-task` skill 一次执行的整体锚点(`Create Task`),由调用方 SKILL 步骤 3 写入。
184
184
 
185
185
  ### 9. 返回结果
186
186
 
@@ -81,7 +81,10 @@ if [ "$has_triage" = "true" ]; then
81
81
  fi
82
82
  ```
83
83
 
84
- 6. If `has_triage=false`, the target milestone does not exist, or the branch ancestry cannot be determined reliably, keep the original milestone unchanged
84
+ 6. Keep the original milestone unchanged only in the following cases (otherwise narrow per step 5):
85
+ - Trunk mode with no open concrete version under the release line — the `code-task` / `create-pr` `verify_milestone_specific` gate will fail and prompt maintainers to create the missing concrete version
86
+ - Multi-release-line mode when both `git merge-base --is-ancestor` checks are unreliable or the remote refs are missing
87
+ - In any mode, `has_triage=false` (the bot will reconcile later)
85
88
 
86
89
  Suggested concrete-version query:
87
90
 
@@ -81,7 +81,10 @@ if [ "$has_triage" = "true" ]; then
81
81
  fi
82
82
  ```
83
83
 
84
- 6. 如果 `has_triage=false`、目标 milestone 不存在,或无法可靠判断 -> 保持原 milestone 不变
84
+ 6. 仅在以下情况保持原 milestone 不变(其余情形必须按步骤 5 收窄):
85
+ - 主干模式下版本线下没有 open 具体版本 —— `code-task` / `create-pr` 的 `verify_milestone_specific` gate 会 FAIL,提醒维护者补建具体版本
86
+ - 多版本分支模式下 `git merge-base --is-ancestor` 两条判断都不可靠或远程引用缺失
87
+ - 任意模式下 `has_triage=false`(由 bot 后补)
85
88
 
86
89
  具体版本查询建议:
87
90
 
@@ -0,0 +1,62 @@
1
+ # Next-Step Output Rule
2
+
3
+ This file defines two **independent** rules for a skill's "notify-user / Next steps" output; read this file before rendering the final output and apply both:
4
+
5
+ 1. **Next-step output structure**: how "Next steps" commands and the "Task info" block present the task ID (placeholders / short-id lookup / fallback).
6
+ 2. **Agent output trailing line (Completed at)**: the **very last line** of user-facing output, **independent of the "Next steps" block**, applying to normal / error / early-return paths alike.
7
+
8
+ ## Placeholder semantics
9
+
10
+ | Placeholder | Meaning | Rendered form |
11
+ |-------------|---------|---------------|
12
+ | `{task-ref}` | Current task **short id** | `#`-prefixed, e.g. `#15`; falls back to the full `TASK-id` when unavailable |
13
+ | `{task-id}` | Current task **full id** | `TASK-YYYYMMDD-HHMMSS` |
14
+
15
+ ## Scope
16
+
17
+ - **Next-step TUI commands** (`/analyze-task`, `/{{project}}:review-code`, `$create-pr`, etc., including commands inside Markdown table cells) → always use `{task-ref}` (short id).
18
+ - **"Task info" / "Task status" structured field lines** → show full id and short id together: `- Task ID: {task-id} (short id {task-ref})`.
19
+ - **Report titles** (`Task {task-id} ... completed`) and **artifact paths** (`.agents/workspace/active/{task-id}/...`) → keep the full `{task-id}` (physical path and archive key, must not change).
20
+
21
+ ## Obtaining the short id (`{task-ref}`)
22
+
23
+ The single source of truth for short ids is the registry `.agents/workspace/active/.short-ids.json` (via `task-short-id.js`). **Never** read the `short_id` field from task.md frontmatter (that field is not authoritative).
24
+
25
+ Once the full `$task_id` is resolved, use the snippet below to look up the short id; it returns `#NN` on hit and falls back to the full `TASK-id` on miss:
26
+
27
+ ```bash
28
+ task_ref=$(node -e '
29
+ const cp=require("child_process");
30
+ const out=cp.execSync("node .agents/scripts/task-short-id.js list",{encoding:"utf8"});
31
+ const ids=(JSON.parse(out).ids)||{};
32
+ const full=process.argv[1];
33
+ const hit=Object.entries(ids).find(([,v])=>v===full);
34
+ process.stdout.write(hit?("#"+hit[0]):full);
35
+ ' "$task_id")
36
+ # Example: $task_id=TASK-20260613-225809 -> task_ref=#15
37
+ ```
38
+
39
+ ## Fallback conditions
40
+
41
+ `{task-ref}` falls back to the full `TASK-id` in these cases (i.e. the registry has no matching short id):
42
+
43
+ - **Unallocated**: very early paths before `create-task` / `import-*` / `restore-task` has allocated a short id.
44
+ - **Released**: after a task is archived by `complete-task` / `cancel-task` / `block-task` / `close-codescan` / `close-dependabot`, its short id is immediately removed from the registry. The terminal/summary lines of these archival skills therefore fall back to the full `TASK-id` naturally, with no special-casing.
45
+
46
+ `restore-task` re-allocates a short id when restoring a task (possibly different from before); the snippet picks up the new short id.
47
+
48
+ ## `#` prefix and shell quoting
49
+
50
+ Short ids are always rendered with a `#` prefix as `#NN`, matching how task.md frontmatter renders `short_id`. `#` starts a comment in bash, so pasting example commands depends on the TUI (both the bare numeric `NN` and `#NN` are accepted by `task-short-id.js resolve`).
51
+
52
+ ## Agent output trailing line (Completed at)
53
+
54
+ This section is a standalone rule, **co-equal with the next-step output structure** and **not part of the "Next steps" block**. Every skill that renders user-facing output must append the completion-time line as the **very last line** of that output — including **complete-task, which renders no next-step commands**, and **error / early-return paths** where a precondition is unmet. This lets users scanning across tmux windows tell at a glance which agent finished most recently:
55
+
56
+ ```text
57
+ Completed at: YYYY-MM-DD HH:mm:ss
58
+ ```
59
+
60
+ - Value command (local timezone, no offset): `date "+%Y-%m-%d %H:%M:%S"`
61
+ - Position: it must be the last line of the entire user-facing output, after all "Next steps" commands. If a scenario has a conditional reminder line after the commands (e.g. the env-blocked reminder), the completion line goes after that reminder.
62
+ - This line is for terminal scanning only; it is never written to any artifact file or Issue/PR comment. The single source of truth for completion time remains the Activity Log in task.md.
@@ -0,0 +1,62 @@
1
+ # 下一步输出规则
2
+
3
+ 本文件定义 skill「告知用户 / 下一步」输出的两类**相互独立**的规则;渲染最终输出前先读取本文件并同时落实两者:
4
+
5
+ 1. **下一步输出结构**:「下一步」命令与「任务信息」段如何呈现任务 ID 形态(占位符 / 取短号 / 回退)。
6
+ 2. **Agent 输出收尾行(Completed at)**:面向用户输出的**绝对最后一行**,**独立于「下一步」块**,正常 / 错误 / 早退路径都适用。
7
+
8
+ ## 占位符语义
9
+
10
+ | 占位符 | 含义 | 渲染形态 |
11
+ |--------|------|----------|
12
+ | `{task-ref}` | 当前任务**短号** | 带 `#` 前缀,如 `#15`;取不到时回退完整 `TASK-id` |
13
+ | `{task-id}` | 当前任务**完整 ID** | `TASK-YYYYMMDD-HHMMSS` |
14
+
15
+ ## 适用范围
16
+
17
+ - **下一步 TUI 命令**(`/analyze-task`、`/{{project}}:review-code`、`$create-pr` 等,含 Markdown 表格单元格内的命令)→ 一律用 `{task-ref}`(短号)。
18
+ - **「任务信息」/「任务状态」结构化字段行** → 完整 ID 与短号同显:`- 任务 ID:{task-id}(短号 {task-ref})`。
19
+ - **报告标题**(`任务 {task-id} ... 完成`)与**产出文件路径**(`.agents/workspace/active/{task-id}/...`)→ 保持完整 `{task-id}`(物理路径与归档键,不可改)。
20
+
21
+ ## 取短号(`{task-ref}`)
22
+
23
+ 短号唯一真源是注册表 `.agents/workspace/active/.short-ids.json`(经 `task-short-id.js`)。**禁止**读取 task.md frontmatter 的 `short_id` 字段(该字段不可信)。
24
+
25
+ 在已解析出完整 `$task_id` 后,用以下片段反查短号;命中返回 `#NN`,未命中自动回退完整 `TASK-id`:
26
+
27
+ ```bash
28
+ task_ref=$(node -e '
29
+ const cp=require("child_process");
30
+ const out=cp.execSync("node .agents/scripts/task-short-id.js list",{encoding:"utf8"});
31
+ const ids=(JSON.parse(out).ids)||{};
32
+ const full=process.argv[1];
33
+ const hit=Object.entries(ids).find(([,v])=>v===full);
34
+ process.stdout.write(hit?("#"+hit[0]):full);
35
+ ' "$task_id")
36
+ # 示例:$task_id=TASK-20260613-225809 -> task_ref=#15
37
+ ```
38
+
39
+ ## 回退条件
40
+
41
+ `{task-ref}` 在以下情况回退为完整 `TASK-id`(即注册表查不到对应短号):
42
+
43
+ - **未分配**:任务尚未经 `create-task` / `import-*` / `restore-task` 分配短号的极早期路径。
44
+ - **已释放**:任务经 `complete-task` / `cancel-task` / `block-task` / `close-codescan` / `close-dependabot` 归档后,短号立即从注册表移除。这些归档类 skill 的终态/摘要行因此自然回退完整 `TASK-id`,无需特判。
45
+
46
+ `restore-task` 恢复任务时会重新分配短号(可能与历史不同),片段会取到新短号。
47
+
48
+ ## `#` 前缀与 shell 引用
49
+
50
+ 短号统一渲染为带 `#` 前缀的 `#NN`,与 task.md frontmatter 的 `short_id` 渲染一致。`#` 在 bash 中是注释起始符,示例命令若直接粘贴需视 TUI 而定(裸数字 `NN` 与 `#NN` 都被 `task-short-id.js resolve` 接受)。
51
+
52
+ ## Agent 输出收尾行(Completed at)
53
+
54
+ 本节是与「下一步输出结构」**并列的独立规则**,不隶属于「下一步」块。任何向用户渲染输出的 skill 都必须在面向用户输出的**绝对最后一行**追加完成时间收尾行——包括**声明「不渲染下一步命令」的 complete-task**,以及前置条件未满足而提前 return 的**错误 / 早退路径**。便于用户在 tmux 多窗口扫视时一眼判断各 Agent 的完成先后:
55
+
56
+ ```text
57
+ Completed at: YYYY-MM-DD HH:mm:ss
58
+ ```
59
+
60
+ - 取值命令(本地时区、不带偏移):`date "+%Y-%m-%d %H:%M:%S"`
61
+ - 位置:必须是整段面向用户输出的最后一行,排在所有「下一步」命令之后。若某场景在命令之后还有条件性提醒行(如 env-blocked 提醒),收尾行排在该提醒行之后。
62
+ - 该行只用于终端扫视,不写入任何产物文件或 Issue/PR 评论;完成时刻的单一事实源仍是 task.md 的 Activity Log。
@@ -0,0 +1,5 @@
1
+ # PR Checks Platform Commands
2
+
3
+ This code platform does not provide built-in pull request check commands.
4
+
5
+ Platform-specific check monitoring is skipped for custom platforms unless you provide matching `.{platform}.en.md` rule templates. Keep local task artifacts as the source of truth, or install a platform-specific template pack before running the `watch-pr` skill.
@@ -0,0 +1,62 @@
1
+ # PR Checks Platform Commands (GitHub)
2
+
3
+ Read this file before watching a PR's required checks, resolving a failing run, pulling failure logs, or reading the current branch's PR. The `watch-pr` skill's platform-specific commands live here; the skill body and `reference/` stay platform-agnostic.
4
+
5
+ ## Current Branch PR / Repository Info
6
+
7
+ ```bash
8
+ gh pr view --json number -q .number # PR number for the current branch
9
+ gh pr view {pr#} --json headRefOid -q .headRefOid # PR head SHA
10
+ gh repo view --json nameWithOwner -q .nameWithOwner # {owner}/{repo}
11
+ ```
12
+
13
+ If `gh` is not authenticated or a command fails, stop or degrade per the calling skill's error handling.
14
+
15
+ ## Watch Required Checks
16
+
17
+ ```bash
18
+ gh pr checks {pr#} --required --watch --fail-fast -i 30 \
19
+ --json name,bucket,link,workflow
20
+ ```
21
+
22
+ - `--required`: include only checks the repository's branch protection marks as required.
23
+ - `--watch`: block until those checks finish; `--fail-fast`: exit watch on the first failure.
24
+ - `-i 30`: poll every 30 seconds (backoff). **Overall time cap default 30 minutes (1800 seconds)**: use the timeout mechanism that matches the execution environment; on timeout, treat as "pending" (exit code 8).
25
+ - POSIX shell: `timeout 1800 gh pr checks {pr#} --required --watch --fail-fast -i 30 …`
26
+ - PowerShell (Windows): use a job timeout —
27
+ ```powershell
28
+ $job = Start-Job { gh pr checks {pr#} --required --watch --fail-fast -i 30 }
29
+ if (Wait-Job $job -Timeout 1800) { Receive-Job $job } else { Stop-Job $job; <treat as "pending"> }
30
+ ```
31
+ - Platform-neutral fallback (no external timeout tool): record the start time, loop `gh pr checks {pr#} --required --json name,bucket,link,workflow` **without** `--watch`, sleeping `-i` seconds each round and checking whether any `bucket` is still `pending`; if the elapsed time reaches 1800 seconds without finishing, exit the loop and treat as "pending".
32
+ - The `bucket` field of `--json` classifies each check as `pass` / `fail` / `pending` / `skipping` / `cancel`.
33
+
34
+ Exit code semantics:
35
+
36
+ | Exit code | Meaning | Outcome class |
37
+ |-----------|---------|---------------|
38
+ | 0 | all required checks passed | all green |
39
+ | 1 | at least one failed / errored | failure |
40
+ | 8 | still pending (watch timed out or was cut off by `timeout`) | pending |
41
+
42
+ Old `gh` (< 2.93) without `--required`: fall back to `gh pr checks {pr#} --watch --fail-fast` (i.e. "all checks must succeed"), and note this degradation in the help/report and suggest upgrading `gh`.
43
+
44
+ ## Resolve a Failing Run id and Pull Logs
45
+
46
+ `gh pr checks --json` does not return a run id directly, but it returns each failing check's `link` (a URL to the run/job). Resolve in this deterministic order:
47
+
48
+ 1. Extract from the failing check's `link` via regex: `https://github.com/{owner}/{repo}/actions/runs/(\d+)(?:/job/(\d+))?` → group 1 is the run id (optional group 2 is the job id).
49
+ 2. When `link` is not a run URL or cannot be parsed, query check-runs by head SHA:
50
+ ```bash
51
+ sha=$(gh pr view {pr#} --json headRefOid -q .headRefOid)
52
+ gh api "repos/{owner}/{repo}/commits/$sha/check-runs" \
53
+ --jq '.check_runs[] | select(.name=="{failed-check-name}") | .details_url'
54
+ ```
55
+ then extract the run id from `details_url` the same way.
56
+ 3. If neither path yields a run id → treat as "unlocatable" and use the skill's help exit; do not self-heal blindly.
57
+
58
+ Once the run id is known, pull the failure logs:
59
+
60
+ ```bash
61
+ gh run view {run-id} --log-failed
62
+ ```