@fitlab-ai/agent-infra 0.9.14 → 0.9.15

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 (250) hide show
  1. package/README.md +10 -0
  2. package/README.zh-CN.md +10 -0
  3. package/bin/internal-cli.sh +45 -0
  4. package/dist/bin/internal-cli.js +93 -30
  5. package/dist/lib/agent-clients/adapters/codex-lifecycle/capability-store.js +165 -27
  6. package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +3 -2
  7. package/dist/lib/defaults.json +103 -103
  8. package/dist/lib/fs/durable-write.js +37 -0
  9. package/dist/lib/fs/file-lock.js +111 -0
  10. package/dist/lib/host-control/audit.js +19 -0
  11. package/dist/lib/host-control/client.js +142 -0
  12. package/dist/lib/host-control/command.js +49 -0
  13. package/dist/lib/host-control/path.js +160 -0
  14. package/dist/lib/host-control/secure-fs.js +140 -0
  15. package/dist/lib/host-control/server.js +159 -0
  16. package/dist/lib/host-control/service.js +112 -0
  17. package/dist/lib/init.js +11 -2
  18. package/dist/lib/internal/cli-route-inventory.js +3 -1
  19. package/dist/lib/internal/codex-lifecycle.js +6 -6
  20. package/dist/lib/internal/demo-promotion.js +322 -0
  21. package/dist/lib/internal/demo-transcript.js +454 -0
  22. package/dist/lib/internal/host-control.js +28 -0
  23. package/dist/lib/internal/manual-validation.js +322 -0
  24. package/dist/lib/internal/platform-pr.js +18 -4
  25. package/dist/lib/internal/release-workflow.js +63 -84
  26. package/dist/lib/internal/sandbox-control.js +149 -2
  27. package/dist/lib/internal/task-artifact.js +15 -262
  28. package/dist/lib/internal/task-create.js +6 -5
  29. package/dist/lib/internal/task-event.js +37 -37
  30. package/dist/lib/internal/task-operation-registry.js +101 -3
  31. package/dist/lib/internal/task-review.js +8 -53
  32. package/dist/lib/internal/task-validate.js +30 -10
  33. package/dist/lib/owned-path.js +33 -0
  34. package/dist/lib/platform/check-status.js +11 -0
  35. package/dist/lib/platform/comment-safety.js +4 -1
  36. package/dist/lib/platform/completion-backfill.js +1 -1
  37. package/dist/lib/platform/github-data.js +241 -0
  38. package/dist/lib/platform/github-provider.js +15 -20
  39. package/dist/lib/platform/issue-comments.js +2 -2
  40. package/dist/lib/platform/issues.js +1 -179
  41. package/dist/lib/platform/pr-checks.js +12 -65
  42. package/dist/lib/platform/pr-summary.js +55 -3
  43. package/dist/lib/platform/pull-requests.js +2 -105
  44. package/dist/lib/platform/snapshots.js +2 -0
  45. package/dist/lib/platform/verification-sync.js +127 -142
  46. package/dist/lib/release/workflow.js +1 -1
  47. package/dist/lib/sandbox/commands/create.js +7 -2
  48. package/dist/lib/sandbox/commands/rm.js +3 -1514
  49. package/dist/lib/sandbox/control/audit.js +203 -0
  50. package/dist/lib/sandbox/control/client.js +93 -54
  51. package/dist/lib/sandbox/control/executor.js +129 -65
  52. package/dist/lib/sandbox/control/finalization-response.js +27 -0
  53. package/dist/lib/sandbox/control/identity-sentinel.js +107 -0
  54. package/dist/lib/sandbox/control/lifecycle.js +36 -2
  55. package/dist/lib/sandbox/control/native-file-lock.js +6 -98
  56. package/dist/lib/sandbox/control/protocol.js +55 -4
  57. package/dist/lib/sandbox/control/server.js +491 -120
  58. package/dist/lib/sandbox/control/state.js +58 -0
  59. package/dist/lib/sandbox/control/task-workflow.js +74 -0
  60. package/dist/lib/sandbox/control/workflow-executor.js +99 -0
  61. package/dist/lib/sandbox/index.js +2 -2
  62. package/dist/lib/sandbox/recovery.js +28 -4
  63. package/dist/lib/sandbox/removal.js +1717 -0
  64. package/dist/lib/sandbox/task-cleanup.js +217 -0
  65. package/dist/lib/sandbox/workspace-view.js +13 -14
  66. package/dist/lib/task/activity-intent.js +2 -1
  67. package/dist/lib/task/activity-log.js +30 -24
  68. package/dist/lib/task/artifact-command.js +101 -0
  69. package/dist/lib/task/artifact-lifecycle.js +30 -28
  70. package/dist/lib/task/artifact-name.js +36 -0
  71. package/dist/lib/task/artifact-operations.js +23 -69
  72. package/dist/lib/task/artifact-receipts.js +3 -15
  73. package/dist/lib/task/artifact-repair-intent.js +87 -16
  74. package/dist/lib/task/artifact-schema.js +6 -14
  75. package/dist/lib/task/capabilities.js +12 -19
  76. package/dist/lib/task/codex-orchestration.js +71 -25
  77. package/dist/lib/task/commit-identity.js +28 -0
  78. package/dist/lib/task/commit-operation.js +2 -16
  79. package/dist/lib/task/control-authority.js +371 -5
  80. package/dist/lib/task/control-recovery.js +128 -0
  81. package/dist/lib/task/create.js +62 -9
  82. package/dist/lib/task/decision-details.js +2 -46
  83. package/dist/lib/task/events.js +120 -9
  84. package/dist/lib/task/finalization-state.js +9 -0
  85. package/dist/lib/task/human-override.js +3 -6
  86. package/dist/lib/task/implementation-inputs.js +3 -3
  87. package/dist/lib/task/intermediate-cleanup.js +383 -0
  88. package/dist/lib/task/invalidation.js +18 -65
  89. package/dist/lib/task/ledger-intents.js +7 -22
  90. package/dist/lib/task/ledger.js +1 -3
  91. package/dist/lib/task/lifecycle.js +19 -1
  92. package/dist/lib/task/local-artifact-finalization.js +132 -157
  93. package/dist/lib/task/manual-validation-completion.js +26 -0
  94. package/dist/lib/task/manual-validation-evidence.js +130 -0
  95. package/dist/lib/task/manual-validation-receipt.js +101 -0
  96. package/dist/lib/task/manual-validation-shared.js +40 -0
  97. package/dist/lib/task/manual-validation-transaction.js +243 -0
  98. package/dist/lib/task/markdown.js +47 -0
  99. package/dist/lib/task/orchestration.js +14 -17
  100. package/dist/lib/task/qualification-audit.js +20 -34
  101. package/dist/lib/task/review-artifacts.js +2 -18
  102. package/dist/lib/task/review-command.js +25 -0
  103. package/dist/lib/task/review-finalization.js +125 -193
  104. package/dist/lib/task/review-fingerprint.js +2 -2
  105. package/dist/lib/task/sections.js +34 -56
  106. package/dist/lib/task/verification-engine.js +116 -323
  107. package/dist/lib/task/verification.js +1 -1
  108. package/dist/lib/task/workflow-command.js +17 -0
  109. package/dist/lib/task/workflow-warning-intents.js +0 -16
  110. package/dist/lib/task/workflow-warnings.js +28 -2
  111. package/dist/lib/update.js +2 -0
  112. package/dist/lifecycle-build-manifest.json +3 -3
  113. package/lib/agent-clients/adapters/codex-lifecycle/capability-store.ts +223 -33
  114. package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +3 -2
  115. package/lib/fs/durable-write.ts +29 -0
  116. package/lib/fs/file-lock.ts +113 -0
  117. package/lib/host-control/audit.ts +22 -0
  118. package/lib/host-control/client.ts +172 -0
  119. package/lib/host-control/command.ts +60 -0
  120. package/lib/host-control/path.ts +167 -0
  121. package/lib/host-control/secure-fs.ts +135 -0
  122. package/lib/host-control/server.ts +180 -0
  123. package/lib/host-control/service.ts +133 -0
  124. package/lib/init.ts +15 -2
  125. package/lib/internal/cli-route-inventory.ts +3 -1
  126. package/lib/internal/codex-lifecycle.ts +6 -6
  127. package/lib/internal/demo-promotion.ts +354 -0
  128. package/lib/internal/demo-transcript.ts +419 -0
  129. package/lib/internal/host-control.ts +28 -0
  130. package/lib/internal/manual-validation.ts +306 -0
  131. package/lib/internal/platform-pr.ts +15 -4
  132. package/lib/internal/release-workflow.ts +74 -82
  133. package/lib/internal/sandbox-control.ts +162 -1
  134. package/lib/internal/task-artifact.ts +13 -216
  135. package/lib/internal/task-create.ts +5 -5
  136. package/lib/internal/task-event.ts +20 -11
  137. package/lib/internal/task-operation-registry.ts +111 -3
  138. package/lib/internal/task-review.ts +6 -51
  139. package/lib/internal/task-validate.ts +26 -10
  140. package/lib/owned-path.ts +26 -0
  141. package/lib/platform/check-status.ts +9 -0
  142. package/lib/platform/comment-safety.ts +5 -0
  143. package/lib/platform/completion-backfill.ts +1 -1
  144. package/lib/platform/github-data.ts +322 -0
  145. package/lib/platform/github-provider.ts +15 -20
  146. package/lib/platform/issue-comments.ts +3 -4
  147. package/lib/platform/issues.ts +4 -245
  148. package/lib/platform/pr-checks.ts +13 -77
  149. package/lib/platform/pr-summary.ts +61 -5
  150. package/lib/platform/pull-requests.ts +3 -170
  151. package/lib/platform/snapshots.ts +53 -0
  152. package/lib/platform/verification-sync.ts +145 -155
  153. package/lib/release/workflow.ts +3 -3
  154. package/lib/sandbox/commands/create.ts +7 -6
  155. package/lib/sandbox/commands/rm.ts +3 -1909
  156. package/lib/sandbox/control/audit.ts +253 -0
  157. package/lib/sandbox/control/client.ts +109 -66
  158. package/lib/sandbox/control/executor.ts +134 -74
  159. package/lib/sandbox/control/finalization-response.ts +29 -0
  160. package/lib/sandbox/control/identity-sentinel.ts +137 -0
  161. package/lib/sandbox/control/lifecycle.ts +45 -3
  162. package/lib/sandbox/control/native-file-lock.ts +6 -107
  163. package/lib/sandbox/control/protocol.ts +75 -6
  164. package/lib/sandbox/control/server.ts +538 -123
  165. package/lib/sandbox/control/state.ts +78 -0
  166. package/lib/sandbox/control/task-workflow.ts +95 -0
  167. package/lib/sandbox/control/workflow-executor.ts +102 -0
  168. package/lib/sandbox/index.ts +2 -2
  169. package/lib/sandbox/recovery.ts +28 -5
  170. package/lib/sandbox/removal.ts +2144 -0
  171. package/lib/sandbox/task-cleanup.ts +209 -0
  172. package/lib/sandbox/workspace-view.ts +17 -15
  173. package/lib/task/activity-intent.ts +2 -2
  174. package/lib/task/activity-log.ts +27 -19
  175. package/lib/task/artifact-command.ts +104 -0
  176. package/lib/task/artifact-lifecycle.ts +31 -26
  177. package/lib/task/artifact-name.ts +55 -0
  178. package/lib/task/artifact-operations.ts +22 -68
  179. package/lib/task/artifact-receipts.ts +3 -14
  180. package/lib/task/artifact-repair-intent.ts +99 -18
  181. package/lib/task/artifact-schema.ts +5 -34
  182. package/lib/task/capabilities.ts +13 -22
  183. package/lib/task/codex-orchestration.ts +85 -24
  184. package/lib/task/commit-identity.ts +31 -0
  185. package/lib/task/commit-operation.ts +2 -16
  186. package/lib/task/control-authority.ts +502 -5
  187. package/lib/task/control-recovery.ts +174 -0
  188. package/lib/task/create.ts +63 -8
  189. package/lib/task/decision-details.ts +3 -71
  190. package/lib/task/events.ts +131 -10
  191. package/lib/task/finalization-state.ts +11 -0
  192. package/lib/task/human-override.ts +3 -8
  193. package/lib/task/implementation-inputs.ts +3 -3
  194. package/lib/task/intermediate-cleanup.ts +455 -0
  195. package/lib/task/invalidation.ts +18 -62
  196. package/lib/task/ledger-intents.ts +7 -24
  197. package/lib/task/ledger.ts +1 -3
  198. package/lib/task/lifecycle.ts +17 -1
  199. package/lib/task/local-artifact-finalization.ts +148 -182
  200. package/lib/task/manual-validation-completion.ts +40 -0
  201. package/lib/task/manual-validation-evidence.ts +209 -0
  202. package/lib/task/manual-validation-receipt.ts +152 -0
  203. package/lib/task/manual-validation-shared.ts +50 -0
  204. package/lib/task/manual-validation-transaction.ts +304 -0
  205. package/lib/task/markdown.ts +70 -0
  206. package/lib/task/orchestration.ts +19 -19
  207. package/lib/task/qualification-audit.ts +22 -33
  208. package/lib/task/review-artifacts.ts +1 -20
  209. package/lib/task/review-command.ts +23 -0
  210. package/lib/task/review-finalization.ts +131 -259
  211. package/lib/task/review-fingerprint.ts +2 -2
  212. package/lib/task/sections.ts +33 -54
  213. package/lib/task/verification-engine.ts +110 -366
  214. package/lib/task/verification-types.ts +17 -0
  215. package/lib/task/verification.ts +1 -1
  216. package/lib/task/workflow-command.ts +18 -0
  217. package/lib/task/workflow-warning-intents.ts +0 -13
  218. package/lib/task/workflow-warnings.ts +21 -2
  219. package/lib/update.ts +6 -0
  220. package/package.json +6 -6
  221. package/templates/.agents/hooks/lifecycle-delegation.js +6 -6
  222. package/templates/.agents/rules/testing-discipline.en.md +2 -0
  223. package/templates/.agents/rules/testing-discipline.zh-CN.md +2 -0
  224. package/templates/.agents/scripts/package.json +3 -0
  225. package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +32 -20
  226. package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +32 -20
  227. package/templates/.agents/skills/complete-manual-validation/config/verify.en.json +1 -1
  228. package/templates/.agents/skills/complete-manual-validation/config/verify.zh-CN.json +1 -1
  229. package/templates/.agents/skills/complete-manual-validation/reference/report-template.en.md +2 -0
  230. package/templates/.agents/skills/complete-manual-validation/reference/report-template.zh-CN.md +2 -0
  231. package/templates/.agents/skills/complete-manual-validation/reference/summary-update.en.md +16 -7
  232. package/templates/.agents/skills/complete-manual-validation/reference/summary-update.zh-CN.md +16 -7
  233. package/templates/.agents/skills/complete-task/SKILL.en.md +1 -1
  234. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +1 -1
  235. package/templates/.agents/skills/release/SKILL.en.md +7 -3
  236. package/templates/.agents/skills/release/SKILL.zh-CN.md +7 -3
  237. package/templates/.agents/skills/run-manual-validation/SKILL.en.md +20 -3
  238. package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +20 -3
  239. package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.en.md +4 -1
  240. package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.zh-CN.md +4 -1
  241. package/templates/.agents/skills/run-manual-validation/reference/report-template.en.md +2 -0
  242. package/templates/.agents/skills/run-manual-validation/reference/report-template.zh-CN.md +2 -0
  243. package/templates/.agents/skills/run-task/SKILL.en.md +1 -1
  244. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +1 -1
  245. package/templates/.agents/skills/test/SKILL.en.md +17 -6
  246. package/templates/.agents/skills/test/SKILL.zh-CN.md +17 -6
  247. package/templates/.claude/commands/run-manual-validation.en.md +1 -1
  248. package/templates/.claude/commands/run-manual-validation.zh-CN.md +1 -1
  249. package/dist/lib/platform/adapters.js +0 -56
  250. package/lib/platform/adapters.ts +0 -197
@@ -0,0 +1,322 @@
1
+ import type { PlatformClient as GitHubClient } from './context.ts';
2
+ import type { PlatformIssueSnapshot as IssueSnapshot, PlatformChangeRequestSnapshot as PullRequestSnapshot } from './snapshots.ts';
3
+ import type { RequiredCheckSnapshot } from './provider-contract.ts';
4
+ import { checkStatusBucket } from './check-status.ts';
5
+
6
+ type RemoteIssue = {
7
+ number?: number;
8
+ id?: number;
9
+ node_id?: string;
10
+ html_url?: string;
11
+ state?: string;
12
+ title?: string;
13
+ body?: string | null;
14
+ labels?: Array<string | { name?: string }>;
15
+ assignees?: Array<{ login?: string }>;
16
+ milestone?: { title?: string } | null;
17
+ type?: { name?: string } | null;
18
+ pull_request?: unknown;
19
+ };
20
+
21
+ type IssueFieldSchema = {
22
+ id: string;
23
+ name: string;
24
+ kind: 'single-select' | 'date' | 'text' | 'number';
25
+ options: Array<{ id: string; name: string }>;
26
+ };
27
+
28
+ type IssueTypeSchema = { id: string; name: string; fields: IssueFieldSchema[] };
29
+
30
+ type CurrentField = { id: string; name: string; kind: IssueFieldSchema['kind']; value: string | number | null };
31
+
32
+ const ISSUE_FIELDS_QUERY = `query($owner:String!,$name:String!,$number:Int!){repository(owner:$owner,name:$name){issue(number:$number){id issueType{id name pinnedFields{__typename ... on IssueFieldSingleSelect{id name options{id name}} ... on IssueFieldDate{id name} ... on IssueFieldText{id name} ... on IssueFieldNumber{id name}}} issueFieldValues(first:50){nodes{__typename ... on IssueFieldSingleSelectValue{name optionId field{... on IssueFieldSingleSelect{id name}}} ... on IssueFieldDateValue{value field{... on IssueFieldDate{id name}}} ... on IssueFieldTextValue{value field{... on IssueFieldText{id name}}} ... on IssueFieldNumberValue{value field{... on IssueFieldNumber{id name}}}}}}}}`;
33
+
34
+ function normalizeIssue(remote: RemoteIssue, repository: string, fallbackNumber?: number): IssueSnapshot | null {
35
+ const number = Number.isInteger(remote.number) && Number(remote.number) > 0
36
+ ? Number(remote.number)
37
+ : fallbackNumber;
38
+ if (!number || !Number.isSafeInteger(number) || remote.pull_request) return null;
39
+ return {
40
+ repository,
41
+ number,
42
+ databaseId: Number.isInteger(remote.id) ? Number(remote.id) : null,
43
+ nodeId: remote.node_id || `issue-${number}`,
44
+ url: remote.html_url || `https://github.com/${repository}/issues/${number}`,
45
+ state: String(remote.state || '').toLowerCase() === 'closed' ? 'closed' : 'open',
46
+ title: remote.title || '',
47
+ body: remote.body || '',
48
+ labels: (remote.labels || []).map((label) => typeof label === 'string' ? label : label.name || '').filter(Boolean).sort(),
49
+ assignees: (remote.assignees || []).map((assignee) => assignee.login || '').filter(Boolean).sort(),
50
+ milestone: remote.milestone?.title || null,
51
+ issueType: remote.type?.name || null,
52
+ fields: {}
53
+ };
54
+ }
55
+
56
+ function fieldKind(value: { __typename?: string }): IssueFieldSchema['kind'] | null {
57
+ if (value.__typename === 'IssueFieldSingleSelect' || value.__typename === 'IssueFieldSingleSelectValue') return 'single-select';
58
+ if (value.__typename === 'IssueFieldDate' || value.__typename === 'IssueFieldDateValue') return 'date';
59
+ if (value.__typename === 'IssueFieldText' || value.__typename === 'IssueFieldTextValue') return 'text';
60
+ if (value.__typename === 'IssueFieldNumber' || value.__typename === 'IssueFieldNumberValue') return 'number';
61
+ return null;
62
+ }
63
+
64
+ function normalizeFieldSchemas(value: unknown): IssueFieldSchema[] {
65
+ if (!Array.isArray(value)) return [];
66
+ return value.flatMap((raw) => {
67
+ const item = raw as { id?: string; name?: string; __typename?: string; options?: Array<{ id?: string; name?: string }> };
68
+ const kind = fieldKind(item);
69
+ return item.id && item.name && kind ? [{
70
+ id: item.id,
71
+ name: item.name,
72
+ kind,
73
+ options: (item.options || []).flatMap((option) => option.id && option.name ? [{ id: option.id, name: option.name }] : [])
74
+ }] : [];
75
+ });
76
+ }
77
+
78
+ function normalizeCurrentFields(value: unknown): { issueId: string | null; type: IssueTypeSchema | null; values: CurrentField[] } {
79
+ const issue = (value as { data?: { repository?: { issue?: {
80
+ id?: string;
81
+ issueType?: { id?: string; name?: string; pinnedFields?: unknown[] } | null;
82
+ issueFieldValues?: { nodes?: unknown[] };
83
+ } } } })?.data?.repository?.issue;
84
+ const type = issue?.issueType?.name ? {
85
+ id: issue.issueType.id || issue.issueType.name,
86
+ name: issue.issueType.name,
87
+ fields: normalizeFieldSchemas(issue.issueType.pinnedFields)
88
+ } : null;
89
+ const values = (issue?.issueFieldValues?.nodes || []).flatMap((raw) => {
90
+ const item = raw as { __typename?: string; name?: string; value?: string | number; field?: { id?: string; name?: string } };
91
+ const kind = fieldKind(item);
92
+ const value = kind === 'single-select' ? item.name : item.value;
93
+ return item.field?.name && kind && value !== undefined
94
+ ? [{ id: item.field.id || item.field.name, name: item.field.name, kind, value: value ?? null }]
95
+ : [];
96
+ });
97
+ return { issueId: issue?.id || null, type, values };
98
+ }
99
+
100
+ function graphState(client: GitHubClient, repository: string, issue: number, cwd: string) {
101
+ const [owner, name] = repository.split('/');
102
+ if (!owner || !name) return null;
103
+ const current = client.json<unknown>([
104
+ 'api', 'graphql', '-f', `query=${ISSUE_FIELDS_QUERY}`, '-F', `owner=${owner}`, '-F', `name=${name}`, '-F', `number=${issue}`
105
+ ], { cwd });
106
+ if (!current.ok) return null;
107
+ return normalizeCurrentFields(current.value);
108
+ }
109
+
110
+ function fetchIssue(client: GitHubClient, repository: string, issue: number, cwd: string) {
111
+ return client.json<RemoteIssue>(['api', `repos/${repository}/issues/${issue}`], { cwd });
112
+ }
113
+
114
+ function inspectGitHubIssue(client: GitHubClient, repository: string, issue: number, cwd: string) {
115
+ const fetched = fetchIssue(client, repository, issue, cwd);
116
+ if (!fetched.ok) return fetched;
117
+ const snapshot = normalizeIssue(fetched.value, repository, issue);
118
+ return snapshot
119
+ ? { ok: true as const, value: snapshot }
120
+ : { ok: false as const, error: { code: 'ISSUE_IDENTITY_INVALID', message: 'Remote resource is not a valid Issue', retryable: false } };
121
+ }
122
+
123
+ type RemotePullRequest = {
124
+ number?: number;
125
+ node_id?: string;
126
+ html_url?: string;
127
+ state?: string;
128
+ title?: string;
129
+ body?: string | null;
130
+ draft?: boolean;
131
+ pull_request?: unknown;
132
+ head?: { ref?: string; sha?: string; repo?: { full_name?: string } | null };
133
+ base?: { ref?: string; sha?: string; repo?: { full_name?: string } | null };
134
+ merged_at?: string | null;
135
+ merge_commit_sha?: string | null;
136
+ labels?: Array<string | { name?: string }>;
137
+ assignees?: Array<{ login?: string }>;
138
+ milestone?: { title?: string } | null;
139
+ mergeable?: boolean | null;
140
+ mergeable_state?: string | null;
141
+ };
142
+
143
+ type ClosingPullRequestNode = {
144
+ number?: number;
145
+ id?: string;
146
+ url?: string;
147
+ state?: string;
148
+ title?: string;
149
+ body?: string | null;
150
+ isDraft?: boolean;
151
+ headRefName?: string;
152
+ headRefOid?: string;
153
+ headRepository?: { nameWithOwner?: string } | null;
154
+ baseRefName?: string;
155
+ baseRefOid?: string;
156
+ baseRepository?: { nameWithOwner?: string } | null;
157
+ mergedAt?: string | null;
158
+ mergeCommit?: { oid?: string } | null;
159
+ labels?: { nodes?: Array<{ name?: string }> };
160
+ assignees?: { nodes?: Array<{ login?: string }> };
161
+ milestone?: { title?: string } | null;
162
+ };
163
+
164
+ type ClosingPullRequestPage = {
165
+ data?: { repository?: { issue?: { closedByPullRequestsReferences?: {
166
+ nodes?: ClosingPullRequestNode[];
167
+ pageInfo?: { hasNextPage?: boolean; endCursor?: string | null };
168
+ } } } };
169
+ };
170
+
171
+ const CLOSING_PULL_REQUESTS_QUERY = `query($owner:String!,$name:String!,$issue:Int!,$cursor:String){repository(owner:$owner,name:$name){issue(number:$issue){closedByPullRequestsReferences(first:100,after:$cursor){nodes{number id url state title body isDraft headRefName headRefOid headRepository{nameWithOwner} baseRefName baseRefOid baseRepository{nameWithOwner} mergedAt mergeCommit{oid} labels(first:100){nodes{name}} assignees(first:100){nodes{login}} milestone{title}} pageInfo{hasNextPage endCursor}}}}}`;
172
+
173
+ function normalizePullRequest(remote: RemotePullRequest, repository: string): PullRequestSnapshot | null {
174
+ const number = Number(remote.number);
175
+ const headRepository = remote.head?.repo?.full_name;
176
+ const baseRepository = remote.base?.repo?.full_name;
177
+ if (!Number.isInteger(number) || number <= 0 || !remote.node_id || !remote.html_url ||
178
+ !remote.head?.ref || !remote.head.sha || !headRepository || !remote.base?.ref || !baseRepository) return null;
179
+ const mergeabilityDetail = remote.mergeable_state?.trim().toLowerCase() || null;
180
+ const mergeability = remote.mergeable === false
181
+ ? { state: 'conflicting' as const, detail: mergeabilityDetail }
182
+ : remote.mergeable === true && mergeabilityDetail !== 'dirty'
183
+ ? { state: 'mergeable' as const, detail: mergeabilityDetail }
184
+ : { state: 'unknown' as const, detail: mergeabilityDetail };
185
+ return {
186
+ repository,
187
+ number,
188
+ nodeId: remote.node_id,
189
+ url: remote.html_url,
190
+ state: remote.state === 'closed' ? 'closed' : 'open',
191
+ title: remote.title || '',
192
+ body: remote.body || '',
193
+ draft: Boolean(remote.draft),
194
+ head: { repository: headRepository, ref: remote.head.ref, sha: remote.head.sha },
195
+ base: { repository: baseRepository, ref: remote.base.ref, sha: remote.base.sha || '' },
196
+ mergedAt: remote.merged_at || null,
197
+ mergeCommitSha: remote.merge_commit_sha || null,
198
+ labels: (remote.labels || []).map((label) => typeof label === 'string' ? label : label.name || '').filter(Boolean).sort(),
199
+ assignees: (remote.assignees || []).map((assignee) => assignee.login || '').filter(Boolean).sort(),
200
+ milestone: remote.milestone?.title || null,
201
+ mergeability
202
+ };
203
+ }
204
+
205
+ function normalizeClosingPullRequest(node: ClosingPullRequestNode, repository: string): PullRequestSnapshot | null {
206
+ return normalizePullRequest({
207
+ number: node.number,
208
+ node_id: node.id,
209
+ html_url: node.url,
210
+ state: node.state === 'OPEN' ? 'open' : 'closed',
211
+ title: node.title,
212
+ body: node.body,
213
+ draft: node.isDraft,
214
+ head: { ref: node.headRefName, sha: node.headRefOid, repo: node.headRepository ? { full_name: node.headRepository.nameWithOwner } : null },
215
+ base: { ref: node.baseRefName, sha: node.baseRefOid, repo: node.baseRepository ? { full_name: node.baseRepository.nameWithOwner } : null },
216
+ merged_at: node.mergedAt,
217
+ merge_commit_sha: node.mergeCommit?.oid,
218
+ labels: node.labels?.nodes,
219
+ assignees: node.assignees?.nodes,
220
+ milestone: node.milestone
221
+ }, repository);
222
+ }
223
+
224
+ function inspectGitHubIssueClosingChangeRequests(
225
+ client: GitHubClient,
226
+ repository: string,
227
+ issueNumber: number,
228
+ cwd: string
229
+ ) {
230
+ const [owner, name] = repository.split('/');
231
+ if (!owner || !name || !Number.isInteger(issueNumber) || issueNumber <= 0) return {
232
+ ok: false as const,
233
+ error: { code: 'PR_IDENTITY_INVALID', message: 'Repository or Issue identity is invalid', retryable: false }
234
+ };
235
+ const candidates: PullRequestSnapshot[] = [];
236
+ let cursor: string | null = null;
237
+ for (;;) {
238
+ const args = [
239
+ 'api', 'graphql', '-f', `query=${CLOSING_PULL_REQUESTS_QUERY}`,
240
+ '-F', `owner=${owner}`, '-F', `name=${name}`, '-F', `issue=${issueNumber}`
241
+ ];
242
+ if (cursor) args.push('-F', `cursor=${cursor}`);
243
+ const response = client.json<ClosingPullRequestPage>(args, { cwd });
244
+ if (!response.ok) return response;
245
+ const connection = response.value?.data?.repository?.issue?.closedByPullRequestsReferences;
246
+ if (!connection || !Array.isArray(connection.nodes) || !connection.pageInfo) return {
247
+ ok: false as const,
248
+ error: { code: 'PR_IDENTITY_INVALID', message: 'Closing pull request response is incomplete', retryable: false }
249
+ };
250
+ for (const node of connection.nodes) {
251
+ const normalized = normalizeClosingPullRequest(node, repository);
252
+ if (!normalized) return {
253
+ ok: false as const,
254
+ error: { code: 'PR_IDENTITY_INVALID', message: 'Closing pull request identity is incomplete', retryable: false }
255
+ };
256
+ candidates.push(normalized);
257
+ }
258
+ if (!connection.pageInfo.hasNextPage) return { ok: true as const, value: candidates };
259
+ if (!connection.pageInfo.endCursor || connection.pageInfo.endCursor === cursor) return {
260
+ ok: false as const,
261
+ error: { code: 'PR_IDENTITY_INVALID', message: 'Closing pull request pagination cursor is invalid', retryable: false }
262
+ };
263
+ cursor = connection.pageInfo.endCursor;
264
+ }
265
+ }
266
+
267
+ function inspectGitHubPullRequest(client: GitHubClient, repository: string, number: number, cwd: string) {
268
+ const fetched = client.json<RemotePullRequest>(['api', `repos/${repository}/pulls/${number}`], { cwd });
269
+ if (!fetched.ok) return fetched;
270
+ const pullRequest = normalizePullRequest(fetched.value, repository);
271
+ return pullRequest
272
+ ? { ok: true as const, value: pullRequest }
273
+ : { ok: false as const, error: { code: 'PR_IDENTITY_INVALID', message: 'Remote resource is not a valid pull request', retryable: false } };
274
+ }
275
+
276
+ function normalizeGitHubChecks(value: unknown): RequiredCheckSnapshot[] {
277
+ if (!Array.isArray(value)) return [];
278
+ return value.flatMap((raw) => {
279
+ const item = raw as Record<string, unknown>;
280
+ const name = String(item.name || item.context || '');
281
+ return name ? [{
282
+ name,
283
+ status: checkStatusBucket(String(item.bucket || item.status || item.conclusion || item.state || '')),
284
+ conclusion: item.conclusion ? String(item.conclusion) : item.state ? String(item.state) : null,
285
+ detailsUrl: item.link ? String(item.link) : item.detailsUrl ? String(item.detailsUrl) : null
286
+ }] : [];
287
+ });
288
+ }
289
+
290
+ function inspectGitHubRequiredChecks(client: GitHubClient, repository: string, number: number, cwd: string) {
291
+ const inspected = client.json<unknown>([
292
+ 'pr', 'checks', String(number), '--repo', repository,
293
+ '--json', 'name,state,bucket,link,workflow,startedAt,completedAt'
294
+ ], { cwd });
295
+ return inspected.ok
296
+ ? { ok: true as const, value: normalizeGitHubChecks(inspected.value) }
297
+ : { ok: false as const, error: inspected.error };
298
+ }
299
+
300
+ function parseRunJobIdentity(detailsUrl: string): { runId: number; jobId: number | null } | null {
301
+ try {
302
+ const url = new URL(detailsUrl);
303
+ if (url.hostname !== 'github.com') return null;
304
+ const match = url.pathname.match(/\/actions\/runs\/(\d+)(?:\/job\/(\d+))?/);
305
+ if (!match) return null;
306
+ return { runId: Number(match[1]), jobId: match[2] ? Number(match[2]) : null };
307
+ } catch {
308
+ return null;
309
+ }
310
+ }
311
+
312
+ function fetchCheckLogText(client: GitHubClient, args: string[], cwd: string) {
313
+ if (!client.text) return { ok: false as const, error: { code: 'PLATFORM_CLIENT_TEXT_UNAVAILABLE', message: 'Platform client does not support text responses', retryable: false } };
314
+ const fetched = client.text(args, { cwd });
315
+ if (fetched.ok || args[0] !== 'api' || !/response contains terminal escape sequences/i.test(fetched.error.message)) {
316
+ return fetched;
317
+ }
318
+ return client.text([...args, '--allow-escape-sequences'], { cwd });
319
+ }
320
+
321
+ export { ISSUE_FIELDS_QUERY, graphState, inspectGitHubIssue, normalizePullRequest, inspectGitHubIssueClosingChangeRequests, inspectGitHubPullRequest, inspectGitHubRequiredChecks, parseRunJobIdentity, fetchCheckLogText };
322
+ export type { IssueFieldSchema, RemotePullRequest };
@@ -1,3 +1,4 @@
1
+ import { ISSUE_FIELDS_QUERY, graphState, inspectGitHubIssue, inspectGitHubPullRequest, inspectGitHubIssueClosingChangeRequests, inspectGitHubRequiredChecks, parseRunJobIdentity, fetchCheckLogText } from './github-data.ts';
1
2
  import { execFileSync } from 'node:child_process';
2
3
  import fs from 'node:fs';
3
4
  import os from 'node:os';
@@ -30,7 +31,6 @@ import type {
30
31
  RepositoryMetadataSnapshot,
31
32
  ReleaseSnapshot,
32
33
  RemoteCommentSnapshot,
33
- RequiredCheckSnapshot,
34
34
  ReviewSnapshot,
35
35
  SecurityAlertKind,
36
36
  SecurityAlertSnapshot,
@@ -41,7 +41,6 @@ import { extractPullRequestFileNames, syncLabelDelta } from './in-label-sync.ts'
41
41
 
42
42
  const CURRENT_USER_QUERY = 'query { viewer { login } }';
43
43
  const ISSUE_TYPES_QUERY = `query($owner:String!){organization(login:$owner){issueTypes(first:20){nodes{id name pinnedFields{__typename ... on IssueFieldSingleSelect{id name options{id name}} ... on IssueFieldDate{id name} ... on IssueFieldText{id name} ... on IssueFieldNumber{id name}}}}}}`;
44
- const ISSUE_FIELDS_QUERY = `query($owner:String!,$name:String!,$number:Int!){repository(owner:$owner,name:$name){issue(number:$number){id issueType{id name pinnedFields{__typename ... on IssueFieldSingleSelect{id name options{id name}} ... on IssueFieldDate{id name} ... on IssueFieldText{id name} ... on IssueFieldNumber{id name}}} issueFieldValues(first:50){nodes{__typename ... on IssueFieldSingleSelectValue{name optionId field{... on IssueFieldSingleSelect{id name}}} ... on IssueFieldDateValue{value field{... on IssueFieldDate{id name}}} ... on IssueFieldTextValue{value field{... on IssueFieldText{id name}}} ... on IssueFieldNumberValue{value field{... on IssueFieldNumber{id name}}}}}}}}`;
45
44
  const CLOSING_ISSUES_QUERY = 'query($owner:String!,$name:String!,$number:Int!,$cursor:String){repository(owner:$owner,name:$name){pullRequest(number:$number){closingIssuesReferences(first:100,after:$cursor){nodes{number} pageInfo{hasNextPage endCursor}}}}}';
46
45
 
47
46
  function parseGitHubRemote(remote: string): string | null {
@@ -465,10 +464,9 @@ function createGitHubOperations(client: GitHubClient): Pick<PlatformProvider, 'i
465
464
  async inspect({ context, target }) {
466
465
  const number = resourceIdentityNumber(target);
467
466
  if (!number) return invalid('ISSUE_NUMBER_INVALID', 'Issue number must be positive');
468
- const module = await import('./issues.ts');
469
- const fetched = module.inspectGitHubIssue(client, repository(context), number, context.workingDirectory);
467
+ const fetched = inspectGitHubIssue(client, repository(context), number, context.workingDirectory);
470
468
  if (!fetched.ok) return fetched;
471
- const graph = module.graphState(client, repository(context), number, context.workingDirectory);
469
+ const graph = graphState(client, repository(context), number, context.workingDirectory);
472
470
  return { ok: true, value: issueSnapshot(fetched.value, graph || undefined) };
473
471
  },
474
472
  async create({ context, desired }) {
@@ -648,8 +646,7 @@ function createGitHubOperations(client: GitHubClient): Pick<PlatformProvider, 'i
648
646
  async inspect({ context, target }) {
649
647
  const number = resourceIdentityNumber(target);
650
648
  if (!number) return invalid('PR_NUMBER_INVALID', 'Pull request number must be positive');
651
- const module = await import('./pull-requests.ts');
652
- const fetched = module.inspectGitHubPullRequest(client, repository(context), number, context.workingDirectory);
649
+ const fetched = inspectGitHubPullRequest(client, repository(context), number, context.workingDirectory);
653
650
  if (!fetched.ok) return fetched;
654
651
  return { ok: true, value: changeRequestSnapshot(fetched.value) };
655
652
  },
@@ -708,8 +705,7 @@ function createGitHubOperations(client: GitHubClient): Pick<PlatformProvider, 'i
708
705
  async listClosing({ context, issue }) {
709
706
  const number = resourceIdentityNumber(issue);
710
707
  if (!number) return invalid('ISSUE_NUMBER_INVALID', 'Issue number must be positive');
711
- const module = await import('./pull-requests.ts');
712
- const fetched = module.inspectGitHubIssueClosingChangeRequests(client, repository(context), number, context.workingDirectory);
708
+ const fetched = inspectGitHubIssueClosingChangeRequests(client, repository(context), number, context.workingDirectory);
713
709
  if (!fetched.ok) return fetched;
714
710
  return { ok: true, value: fetched.value.map(changeRequestSnapshot) };
715
711
  },
@@ -780,8 +776,7 @@ function createGitHubOperations(client: GitHubClient): Pick<PlatformProvider, 'i
780
776
  async resolveGitEvidence({ context, target, expected }) {
781
777
  const number = resourceIdentityNumber(target);
782
778
  if (!number) return invalid('PR_NUMBER_INVALID', 'Pull request number must be positive');
783
- const module = await import('./github-provider.ts');
784
- return module.resolveGitHubChangeRequestGitEvidence({
779
+ return resolveGitHubChangeRequestGitEvidence({
785
780
  cwd: context.workingDirectory,
786
781
  repository: repository(context),
787
782
  number,
@@ -795,16 +790,12 @@ function createGitHubOperations(client: GitHubClient): Pick<PlatformProvider, 'i
795
790
  async inspectRequired({ context, changeRequest }) {
796
791
  const number = resourceIdentityNumber(changeRequest);
797
792
  if (!number) return invalid('PR_NUMBER_INVALID', 'Pull request number must be positive');
798
- const module = await import('./pr-checks.ts');
799
- const fetched = module.inspectGitHubRequiredChecks(client, repository(context), number, context.workingDirectory);
800
- if (!fetched.ok) return fetched;
801
- return { ok: true, value: fetched.value.map((check: any): RequiredCheckSnapshot => ({ name: check.name, status: check.bucket, conclusion: check.conclusion, detailsUrl: check.detailsUrl })) };
793
+ return inspectGitHubRequiredChecks(client, repository(context), number, context.workingDirectory);
802
794
  },
803
795
  async resolveRun({ context, changeRequest, checkName, detailsUrl }) {
804
796
  const number = resourceIdentityNumber(changeRequest);
805
797
  if (!number) return invalid('PR_NUMBER_INVALID', 'Pull request number must be positive');
806
- const module = await import('./pr-checks.ts');
807
- const direct = detailsUrl ? module.parseRunJobIdentity(detailsUrl) : null;
798
+ const direct = detailsUrl ? parseRunJobIdentity(detailsUrl) : null;
808
799
  if (direct) {
809
800
  const run = client.json<any>(['api', `repos/${repository(context)}/actions/runs/${direct.runId}`], { cwd: context.workingDirectory });
810
801
  if (!run.ok) return run;
@@ -818,11 +809,10 @@ function createGitHubOperations(client: GitHubClient): Pick<PlatformProvider, 'i
818
809
  : invalid('CHECK_RUN_NOT_FOUND', 'Check run was not found');
819
810
  },
820
811
  async fetchLogs({ context, runId, jobId }) {
821
- const module = await import('./pr-checks.ts');
822
812
  const args = jobId
823
813
  ? ['api', `repos/${repository(context)}/actions/jobs/${jobId}/logs`]
824
814
  : ['run', 'view', runId, '--repo', repository(context), '--log-failed'];
825
- const fetched = module.fetchCheckLogText(client, args, context.workingDirectory);
815
+ const fetched = fetchCheckLogText(client, args, context.workingDirectory);
826
816
  if (!fetched.ok) return fetched;
827
817
  return { ok: true, value: { runId, jobId, text: fetched.value || '' } } as ProviderResult<CheckLogSnapshot>;
828
818
  }
@@ -891,7 +881,12 @@ function createGitHubOperations(client: GitHubClient): Pick<PlatformProvider, 'i
891
881
  }
892
882
  continue;
893
883
  }
894
- const response = client.json(['api', '--method', 'POST', `repos/${repository(context)}/milestones`, '-f', `title=${item.title}`, '-f', `description=${item.description}`], { cwd: context.workingDirectory });
884
+ const response = client.json([
885
+ 'api', '--method', 'POST', `repos/${repository(context)}/milestones`,
886
+ '-f', `title=${item.title}`,
887
+ '-f', `description=${item.description}`,
888
+ '-f', `state=${item.state}`
889
+ ], { cwd: context.workingDirectory });
895
890
  if (!response.ok) return response;
896
891
  created.push(item.title);
897
892
  }
@@ -17,11 +17,10 @@ import {
17
17
  import { resourceIdentityNumber } from './resource-identity.ts';
18
18
  import { taskIssueIdentity, taskIssueIdentityError } from './task-identities.ts';
19
19
  import {
20
- CONTROL_MARKER_PATTERN,
20
+ canonicalizeCommentBody,
21
21
  escapeHtmlText,
22
22
  fenceRanges,
23
- renderSafeCodeFence,
24
- sanitizeMarkdownDocument
23
+ renderSafeCodeFence
25
24
  } from './comment-safety.ts';
26
25
  import type { FenceRange } from './comment-safety.ts';
27
26
 
@@ -75,7 +74,7 @@ function splitFrontmatter(content: string): { frontmatter: string | null; body:
75
74
  }
76
75
 
77
76
  function sanitizeCommentBody(value: string, label: string): string {
78
- const result = sanitizeMarkdownDocument(value, { reservedMarkers: [CONTROL_MARKER_PATTERN] });
77
+ const result = canonicalizeCommentBody(value);
79
78
  if (!result.ok) throw new Error(`${result.error.code}: ${label}: ${result.error.message} at offset ${result.error.offset}`);
80
79
  return result.value;
81
80
  }