@fitlab-ai/agent-infra 0.9.12 → 0.9.13
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 +11 -11
- package/README.zh-CN.md +11 -11
- package/dist/bin/internal-cli.js +24 -19
- package/dist/lib/agent-clients/next-steps.js +6 -1
- package/dist/lib/decide.js +3 -3
- package/dist/lib/internal/platform-checks.js +3 -3
- package/dist/lib/internal/platform-comment.js +9 -9
- package/dist/lib/internal/platform-context.js +2 -2
- package/dist/lib/internal/platform-issue.js +8 -9
- package/dist/lib/internal/platform-pr-review.js +10 -10
- package/dist/lib/internal/platform-pr.js +63 -24
- package/dist/lib/internal/platform-release-notes.js +3 -3
- package/dist/lib/internal/pr-review-grade.js +11 -9
- package/dist/lib/internal/release-workflow.js +4 -4
- package/dist/lib/internal/sandbox-control.js +30 -4
- package/dist/lib/internal/task-activity.js +3 -3
- package/dist/lib/internal/task-artifact.js +76 -24
- package/dist/lib/internal/task-create.js +2 -2
- package/dist/lib/internal/task-event.js +6 -3
- package/dist/lib/internal/task-finalization.js +2 -2
- package/dist/lib/internal/task-invalidation.js +60 -0
- package/dist/lib/internal/task-ledger.js +9 -6
- package/dist/lib/internal/task-lifecycle.js +2 -2
- package/dist/lib/internal/task-override.js +4 -4
- package/dist/lib/internal/task-review.js +3 -3
- package/dist/lib/internal/task-verify.js +6 -6
- package/dist/lib/internal/task-warning.js +3 -3
- package/dist/lib/platform/adapters.js +1 -3
- package/dist/lib/platform/change-request-git-evidence.js +27 -7
- package/dist/lib/platform/completion-backfill.js +6 -5
- package/dist/lib/platform/context.js +70 -130
- package/dist/lib/platform/github-client.js +1 -1
- package/dist/lib/platform/github-provider.js +903 -0
- package/dist/lib/platform/in-label-sync.js +143 -0
- package/dist/lib/platform/issue-comments.js +94 -41
- package/dist/lib/platform/issues.js +335 -189
- package/dist/lib/platform/metadata-labels.js +6 -2
- package/dist/lib/platform/none-provider.js +28 -0
- package/dist/lib/platform/pr-change-report.js +551 -0
- package/dist/lib/platform/pr-checks.js +119 -104
- package/dist/lib/platform/pr-review.js +88 -72
- package/dist/lib/platform/pr-summary.js +520 -50
- package/dist/lib/platform/provider-bridge.js +45 -0
- package/dist/lib/platform/provider-contract.d.ts +487 -0
- package/dist/lib/platform/provider-contract.js +95 -0
- package/dist/lib/platform/provider-loader.js +251 -0
- package/dist/lib/platform/provider-validation.js +504 -0
- package/dist/lib/platform/pull-request-metadata.js +8 -6
- package/dist/lib/platform/pull-requests.js +783 -537
- package/dist/lib/platform/release-notes.js +78 -55
- package/dist/lib/platform/releases.js +79 -50
- package/dist/lib/platform/resource-identity.d.ts +26 -0
- package/dist/lib/platform/resource-identity.js +92 -0
- package/dist/lib/platform/task-identities.js +31 -0
- package/dist/lib/platform/verification-required.js +11 -12
- package/dist/lib/platform/verification-sync.js +112 -238
- package/dist/lib/pr-review/evidence-grading.js +19 -6
- package/dist/lib/run/index.js +63 -23
- package/dist/lib/sandbox/control/client.js +96 -15
- package/dist/lib/sandbox/control/executor.js +3 -3
- package/dist/lib/sandbox/control/protocol.js +77 -0
- package/dist/lib/sandbox/control/server.js +398 -25
- package/dist/lib/sandbox/control/state.js +204 -1
- package/dist/lib/server/protocol.js +38 -1
- package/dist/lib/task/activity-log.js +8 -1
- package/dist/lib/task/artifact-lifecycle.js +33 -5
- package/dist/lib/task/capabilities.js +239 -0
- package/dist/lib/task/commands/status.js +32 -0
- package/dist/lib/task/commit-operation.js +12 -1
- package/dist/lib/task/control-authority.js +8 -5
- package/dist/lib/task/create-service.js +8 -8
- package/dist/lib/task/current-contract.js +5 -1
- package/dist/lib/task/decision-details.js +1 -1
- package/dist/lib/task/events.js +297 -17
- package/dist/lib/task/finalization.js +37 -13
- package/dist/lib/task/human-override.js +58 -36
- package/dist/lib/task/invalidation-command.js +86 -0
- package/dist/lib/task/invalidation.js +260 -0
- package/dist/lib/task/ledger-intents.js +37 -0
- package/dist/lib/task/local-artifact-finalization.js +374 -0
- package/dist/lib/task/orchestration.js +49 -93
- package/dist/lib/task/pr-delivery-fact.js +102 -101
- package/dist/lib/task/review-fingerprint.js +24 -2
- package/dist/lib/task/rework-intent.js +138 -0
- package/dist/lib/task/skill-scope.js +23 -0
- package/dist/lib/task/task-execution-lock.js +27 -17
- package/dist/lib/task/verification-engine.js +43 -16
- package/dist/lib/task/verification.js +3 -3
- package/dist/lib/task/workflow-warning-intents.js +1 -1
- package/dist/lib/task/write.js +11 -0
- package/dist/lib/version.js +11 -1
- package/dist/lifecycle-build-manifest.json +3 -3
- package/lib/agent-clients/next-steps.ts +6 -1
- package/lib/decide.ts +3 -3
- package/lib/init.ts +1 -1
- package/lib/internal/platform-checks.ts +3 -3
- package/lib/internal/platform-comment.ts +8 -8
- package/lib/internal/platform-context.ts +2 -2
- package/lib/internal/platform-issue.ts +7 -8
- package/lib/internal/platform-pr-review.ts +9 -9
- package/lib/internal/platform-pr.ts +47 -22
- package/lib/internal/platform-release-notes.ts +3 -3
- package/lib/internal/pr-review-grade.ts +10 -8
- package/lib/internal/release-workflow.ts +4 -4
- package/lib/internal/sandbox-control.ts +27 -3
- package/lib/internal/task-activity.ts +3 -3
- package/lib/internal/task-artifact.ts +65 -22
- package/lib/internal/task-create.ts +2 -2
- package/lib/internal/task-event.ts +6 -3
- package/lib/internal/task-finalization.ts +3 -3
- package/lib/internal/task-invalidation.ts +39 -0
- package/lib/internal/task-ledger.ts +11 -8
- package/lib/internal/task-lifecycle.ts +2 -2
- package/lib/internal/task-override.ts +6 -6
- package/lib/internal/task-review.ts +3 -3
- package/lib/internal/task-verify.ts +6 -6
- package/lib/internal/task-warning.ts +3 -3
- package/lib/platform/adapters.ts +3 -2
- package/lib/platform/change-request-git-evidence.ts +36 -9
- package/lib/platform/completion-backfill.ts +9 -8
- package/lib/platform/context.ts +97 -134
- package/lib/platform/github-client.ts +2 -2
- package/lib/platform/github-provider.ts +910 -0
- package/lib/platform/in-label-sync.ts +208 -0
- package/lib/platform/issue-comments.ts +97 -47
- package/lib/platform/issues.ts +344 -174
- package/lib/platform/metadata-labels.ts +5 -2
- package/lib/platform/none-provider.ts +35 -0
- package/lib/platform/pr-change-report.ts +645 -0
- package/lib/platform/pr-checks.ts +126 -106
- package/lib/platform/pr-review.ts +88 -85
- package/lib/platform/pr-summary.ts +560 -53
- package/lib/platform/provider-bridge.ts +82 -0
- package/lib/platform/provider-contract.ts +536 -0
- package/lib/platform/provider-loader.ts +307 -0
- package/lib/platform/provider-validation.ts +529 -0
- package/lib/platform/pull-request-metadata.ts +10 -7
- package/lib/platform/pull-requests.ts +826 -543
- package/lib/platform/release-notes.ts +76 -59
- package/lib/platform/releases.ts +77 -43
- package/lib/platform/resource-identity.ts +121 -0
- package/lib/platform/task-identities.ts +40 -0
- package/lib/platform/types.ts +4 -2
- package/lib/platform/verification-required.ts +11 -13
- package/lib/platform/verification-sync.ts +112 -292
- package/lib/pr-review/evidence-grading.ts +25 -8
- package/lib/run/index.ts +57 -22
- package/lib/sandbox/control/client.ts +105 -15
- package/lib/sandbox/control/executor.ts +4 -4
- package/lib/sandbox/control/protocol.ts +113 -0
- package/lib/sandbox/control/server.ts +431 -23
- package/lib/sandbox/control/state.ts +251 -1
- package/lib/server/protocol.ts +36 -1
- package/lib/task/activity-log.ts +8 -0
- package/lib/task/artifact-lifecycle.ts +32 -6
- package/lib/task/capabilities.ts +262 -0
- package/lib/task/commands/status.ts +38 -0
- package/lib/task/commit-operation.ts +12 -1
- package/lib/task/control-authority.ts +11 -8
- package/lib/task/create-service.ts +8 -8
- package/lib/task/current-contract.ts +6 -1
- package/lib/task/decision-details.ts +6 -2
- package/lib/task/events.ts +296 -16
- package/lib/task/finalization.ts +50 -14
- package/lib/task/human-override.ts +70 -46
- package/lib/task/invalidation-command.ts +86 -0
- package/lib/task/invalidation.ts +332 -0
- package/lib/task/ledger-intents.ts +35 -1
- package/lib/task/local-artifact-finalization.ts +563 -0
- package/lib/task/orchestration.ts +52 -85
- package/lib/task/pr-delivery-fact.ts +131 -148
- package/lib/task/review-fingerprint.ts +23 -2
- package/lib/task/rework-intent.ts +165 -0
- package/lib/task/skill-scope.ts +24 -0
- package/lib/task/task-execution-lock.ts +23 -14
- package/lib/task/verification-engine.ts +52 -17
- package/lib/task/verification.ts +4 -4
- package/lib/task/workflow-warning-intents.ts +1 -1
- package/lib/task/write.ts +19 -0
- package/lib/update.ts +1 -1
- package/lib/version.ts +15 -1
- package/package.json +7 -1
- package/templates/.agents/rules/README.en.md +1 -1
- package/templates/.agents/rules/README.zh-CN.md +1 -1
- package/templates/.agents/rules/issue-pr-commands.en.md +1 -1
- package/templates/.agents/rules/issue-pr-commands.zh-CN.md +1 -1
- package/templates/.agents/rules/issue-sync.en.md +2 -0
- package/templates/.agents/rules/issue-sync.zh-CN.md +2 -0
- package/templates/.agents/rules/local-artifact-repair.en.md +9 -2
- package/templates/.agents/rules/local-artifact-repair.zh-CN.md +9 -2
- package/templates/.agents/rules/pr-sync.en.md +10 -4
- package/templates/.agents/rules/pr-sync.zh-CN.md +17 -4
- package/templates/.agents/rules/task-short-id.en.md +1 -4
- package/templates/.agents/rules/task-short-id.zh-CN.md +1 -3
- package/templates/.agents/skills/analyze-task/SKILL.en.md +13 -3
- package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +10 -3
- package/templates/.agents/skills/check-task/SKILL.en.md +16 -37
- package/templates/.agents/skills/check-task/SKILL.zh-CN.md +16 -37
- package/templates/.agents/skills/code-task/SKILL.en.md +27 -8
- package/templates/.agents/skills/code-task/SKILL.zh-CN.md +29 -10
- package/templates/.agents/skills/code-task/config/verify.en.json +1 -0
- package/templates/.agents/skills/code-task/config/verify.zh-CN.json +1 -0
- package/templates/.agents/skills/commit/reference/issue-metadata-sync.en.md +2 -0
- package/templates/.agents/skills/commit/reference/issue-metadata-sync.zh-CN.md +2 -0
- package/templates/.agents/skills/commit/reference/pr-summary-sync.en.md +3 -3
- package/templates/.agents/skills/commit/reference/pr-summary-sync.zh-CN.md +3 -3
- package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +5 -3
- package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +5 -3
- package/templates/.agents/skills/complete-manual-validation/reference/summary-update.en.md +4 -3
- package/templates/.agents/skills/complete-manual-validation/reference/summary-update.zh-CN.md +4 -3
- package/templates/.agents/skills/complete-task/SKILL.en.md +21 -3
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +20 -3
- package/templates/.agents/skills/create-pr/SKILL.en.md +7 -6
- package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +7 -7
- package/templates/.agents/skills/create-pr/config/verify.json +1 -0
- package/templates/.agents/skills/create-pr/reference/change-report.en.md +16 -4
- package/templates/.agents/skills/create-pr/reference/change-report.zh-CN.md +16 -4
- package/templates/.agents/skills/create-pr/reference/comment-publish.en.md +5 -3
- package/templates/.agents/skills/create-pr/reference/comment-publish.zh-CN.md +5 -3
- package/templates/.agents/skills/create-pr/reference/pr-body-template.en.md +1 -1
- package/templates/.agents/skills/create-pr/reference/pr-body-template.zh-CN.md +1 -1
- package/templates/.agents/skills/create-pr/scripts/change-report.mjs +3 -0
- package/templates/.agents/skills/plan-task/SKILL.en.md +13 -3
- package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +10 -3
- package/templates/.agents/skills/review-analysis/SKILL.en.md +3 -3
- package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/review-code/SKILL.en.md +4 -4
- package/templates/.agents/skills/review-code/SKILL.zh-CN.md +4 -4
- package/templates/.agents/skills/review-plan/SKILL.en.md +3 -3
- package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/run-manual-validation/SKILL.en.md +4 -2
- package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +4 -2
- package/templates/.agents/skills/watch-pr/SKILL.en.md +2 -2
- package/templates/.agents/skills/watch-pr/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.en.md +6 -0
- package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.zh-CN.md +6 -0
- package/templates/.agents/templates/task.en.md +18 -1
- package/templates/.agents/templates/task.zh-CN.md +18 -1
- package/templates/.github/workflows/pr-label.yml +7 -26
package/lib/platform/issues.ts
CHANGED
|
@@ -8,13 +8,11 @@ import { requirementFieldLabels, renderTemplateBody } from '../task/issue-form.t
|
|
|
8
8
|
import { resolveTaskRef } from '../task/resolve-ref.ts';
|
|
9
9
|
import { extractSection, findSectionHeading } from '../task/sections.ts';
|
|
10
10
|
import { writeTask } from '../task/write.ts';
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import type { GitHubClient } from './github-client.ts';
|
|
11
|
+
import { resolvePlatformProviderContext } from './context.ts';
|
|
12
|
+
import type { PlatformClient } from './context.ts';
|
|
14
13
|
import {
|
|
15
14
|
DEFAULT_REQUIREMENT_SECTION_ANCHORS,
|
|
16
15
|
chooseMilestone,
|
|
17
|
-
computeInLabels,
|
|
18
16
|
desiredIssueType,
|
|
19
17
|
normalizeOption,
|
|
20
18
|
planIssueMetadata
|
|
@@ -22,10 +20,32 @@ import {
|
|
|
22
20
|
import type { IssueDesiredState, PlannedOperation, Requirement, RequirementSectionAnchor } from './issue-metadata.ts';
|
|
23
21
|
import { platformResult } from './types.ts';
|
|
24
22
|
import type { PlatformResult } from './types.ts';
|
|
23
|
+
import {
|
|
24
|
+
labelDelta,
|
|
25
|
+
planInLabelUpdate,
|
|
26
|
+
syncLabelDelta,
|
|
27
|
+
validateInLabelMapping,
|
|
28
|
+
validateRepositoryLabelPayload
|
|
29
|
+
} from './in-label-sync.ts';
|
|
30
|
+
import {
|
|
31
|
+
providerError,
|
|
32
|
+
providerOperationContext,
|
|
33
|
+
providerStatus,
|
|
34
|
+
providerResourceIdentity,
|
|
35
|
+
providerResourceToken,
|
|
36
|
+
unsupportedProviderOperation
|
|
37
|
+
} from './provider-bridge.ts';
|
|
38
|
+
import { resourceIdentityEquals, resourceIdentityNumber, resourceIdentityString, serializeResourceIdentity } from './resource-identity.ts';
|
|
39
|
+
import { taskIssueIdentity, taskIssueIdentityError } from './task-identities.ts';
|
|
40
|
+
import type { ResourceIdentity } from './resource-identity.ts';
|
|
41
|
+
|
|
42
|
+
type GitHubClient = PlatformClient;
|
|
43
|
+
import type { IssueSnapshot as ProviderIssueSnapshot, RepositoryMetadataSnapshot } from './provider-contract.ts';
|
|
25
44
|
|
|
26
45
|
type IssueSnapshot = {
|
|
27
46
|
repository: string;
|
|
28
47
|
number: number;
|
|
48
|
+
identity?: ResourceIdentity;
|
|
29
49
|
databaseId: number | null;
|
|
30
50
|
nodeId: string;
|
|
31
51
|
url: string;
|
|
@@ -42,9 +62,9 @@ type IssueResult = PlatformResult & {
|
|
|
42
62
|
task: { id: string | null; issueNumber: number | null };
|
|
43
63
|
issue: IssueSnapshot | null;
|
|
44
64
|
};
|
|
45
|
-
type SharedOptions = { cwd?: string; client?:
|
|
65
|
+
type SharedOptions = { cwd?: string; client?: PlatformClient; runtimeVersion?: string };
|
|
46
66
|
type CreateOptions = SharedOptions & { agent: string; dryRun?: boolean };
|
|
47
|
-
type BindOptions = SharedOptions & { issue: number; agent: string; dryRun?: boolean };
|
|
67
|
+
type BindOptions = SharedOptions & { issue: string | number; agent: string; dryRun?: boolean };
|
|
48
68
|
type SyncOptions = SharedOptions & {
|
|
49
69
|
agent: string;
|
|
50
70
|
status?: string | 'none';
|
|
@@ -71,6 +91,7 @@ type RemoteIssue = {
|
|
|
71
91
|
labels?: Array<string | { name?: string }>;
|
|
72
92
|
assignees?: Array<{ login?: string }>;
|
|
73
93
|
milestone?: { title?: string } | null;
|
|
94
|
+
type?: { name?: string } | null;
|
|
74
95
|
pull_request?: unknown;
|
|
75
96
|
};
|
|
76
97
|
|
|
@@ -100,39 +121,138 @@ function result(
|
|
|
100
121
|
};
|
|
101
122
|
}
|
|
102
123
|
|
|
103
|
-
function resolvedContext(taskRef: string, options: SharedOptions) {
|
|
124
|
+
async function resolvedContext(taskRef: string, options: SharedOptions) {
|
|
104
125
|
const resolved = resolveTaskRef(taskRef, options.cwd ? { repoRoot: options.cwd } : {});
|
|
105
126
|
if (!resolved.ok) return { ok: false as const, output: result('failed', resolved.taskId, null, {
|
|
106
127
|
error: { code: resolved.code, message: resolved.message, retryable: false }
|
|
107
128
|
}) };
|
|
108
129
|
const content = fs.readFileSync(resolved.taskMdPath, 'utf8');
|
|
109
130
|
const frontmatter = parseTaskFrontmatter(content);
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
131
|
+
let issueIdentity: ResourceIdentity | null;
|
|
132
|
+
try { issueIdentity = taskIssueIdentity(frontmatter, undefined, options.runtimeVersion); }
|
|
133
|
+
catch (error) {
|
|
134
|
+
return { ok: false as const, output: result('failed', resolved.taskId, null, { error: { ...taskIssueIdentityError(error), retryable: false } }) };
|
|
135
|
+
}
|
|
136
|
+
const issueNumber = resourceIdentityNumber(issueIdentity);
|
|
137
|
+
const loaded = await resolvePlatformProviderContext({ cwd: resolved.repoRoot, client: options.client });
|
|
138
|
+
const context = loaded.ok ? loaded.value.context : loaded.context;
|
|
114
139
|
const usable = (context.status === 'no-op' || context.status === 'degraded') && context.platform.repository;
|
|
115
|
-
if (!usable) return { ok: false as const, output: result(context.status, resolved.taskId, issueNumber, {
|
|
140
|
+
if (!usable || !loaded.ok) return { ok: false as const, output: result(context.status, resolved.taskId, issueNumber, {
|
|
116
141
|
platform: context.platform, capabilities: context.capabilities, operations: context.operations, error: context.error
|
|
117
142
|
}) };
|
|
118
|
-
return { ok: true as const, resolved, content, frontmatter, issueNumber, client, context };
|
|
143
|
+
return { ok: true as const, resolved, content, frontmatter, issueIdentity, issueNumber, client: options.client, context, provider: loaded.value.provider, providerType: loaded.value.providerType, loadedContext: loaded.value };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function normalizeProviderIssue(remote: ProviderIssueSnapshot, repository: string, fallbackNumber: number | null): IssueSnapshot {
|
|
147
|
+
const identity = remote.identity || (remote.number ? { kind: 'number' as const, value: remote.number } : { kind: 'id' as const, value: remote.id });
|
|
148
|
+
const snapshot: IssueSnapshot = {
|
|
149
|
+
repository,
|
|
150
|
+
number: remote.number ?? resourceIdentityNumber(identity) ?? fallbackNumber ?? 0,
|
|
151
|
+
identity,
|
|
152
|
+
databaseId: null,
|
|
153
|
+
nodeId: remote.id,
|
|
154
|
+
url: remote.displayUrl || '',
|
|
155
|
+
state: remote.state,
|
|
156
|
+
title: remote.title,
|
|
157
|
+
body: remote.body,
|
|
158
|
+
labels: [...remote.labels].sort(),
|
|
159
|
+
assignees: [...remote.assignees].sort(),
|
|
160
|
+
milestone: remote.milestone,
|
|
161
|
+
issueType: remote.issueType?.name || null,
|
|
162
|
+
fields: { ...remote.fields }
|
|
163
|
+
};
|
|
164
|
+
Object.defineProperty(snapshot, 'identity', { value: identity, enumerable: false, configurable: true });
|
|
165
|
+
if (remote.issueType) Object.defineProperty(snapshot, 'issueTypeSnapshot', { value: remote.issueType, enumerable: false, configurable: true });
|
|
166
|
+
return snapshot;
|
|
119
167
|
}
|
|
120
168
|
|
|
121
|
-
function
|
|
122
|
-
|
|
169
|
+
function planProviderMetadata(
|
|
170
|
+
remote: ProviderIssueSnapshot,
|
|
171
|
+
metadata: RepositoryMetadataSnapshot,
|
|
172
|
+
frontmatter: Record<string, string>,
|
|
173
|
+
options: Pick<SyncOptions, 'issueType' | 'fields'>,
|
|
174
|
+
capabilities: PlatformResult['capabilities']
|
|
175
|
+
): PlannedOperation[] {
|
|
176
|
+
if (!options.issueType && !options.fields) return [];
|
|
177
|
+
if (!capabilities.push) return [
|
|
178
|
+
...(options.issueType ? [{ name: 'issue-type', status: 'skipped' as const, reasonCode: 'PUSH_REQUIRED' }] : []),
|
|
179
|
+
...(options.fields ? [{ name: 'fields', status: 'skipped' as const, reasonCode: 'PUSH_REQUIRED' }] : [])
|
|
180
|
+
];
|
|
181
|
+
const target = metadata.issueTypes.find((candidate) => candidate.name === desiredIssueType(frontmatter.type || ''));
|
|
182
|
+
if (!target) return [
|
|
183
|
+
...(options.issueType ? [{ name: 'issue-type', status: 'skipped' as const, reasonCode: 'ISSUE_TYPES_UNSUPPORTED' }] : []),
|
|
184
|
+
...(options.fields ? [{ name: 'fields', status: 'skipped' as const, reasonCode: 'ISSUE_TYPES_UNSUPPORTED' }] : [])
|
|
185
|
+
];
|
|
186
|
+
const operations: PlannedOperation[] = [];
|
|
187
|
+
if (options.issueType) {
|
|
188
|
+
operations.push(remote.issueType && resourceIdentityEquals(remote.issueType.identity, target.identity)
|
|
189
|
+
? { name: 'issue-type', status: 'no-op', reasonCode: null }
|
|
190
|
+
: { name: 'issue-type', status: 'planned', reasonCode: null, value: target.name });
|
|
191
|
+
}
|
|
192
|
+
if (options.fields) {
|
|
193
|
+
const fields = target.fields.map((field) => ({
|
|
194
|
+
id: resourceIdentityString(field.identity) || '',
|
|
195
|
+
name: field.name,
|
|
196
|
+
kind: field.kind,
|
|
197
|
+
options: field.options.map((option) => ({ id: resourceIdentityString(option.identity) || '', name: option.name }))
|
|
198
|
+
}));
|
|
199
|
+
const desired = desiredFieldValues(frontmatter, fields);
|
|
200
|
+
const values = Object.fromEntries(desired.map((field) => [field.name, field.value]));
|
|
201
|
+
const changed = Object.entries(values).some(([name, value]) => remote.fields[name] !== value);
|
|
202
|
+
operations.push(changed
|
|
203
|
+
? { name: 'fields', status: 'planned', reasonCode: null, value: values }
|
|
204
|
+
: { name: 'fields', status: 'no-op', reasonCode: null });
|
|
205
|
+
}
|
|
206
|
+
return operations;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function providerIssueError(base: Awaited<ReturnType<typeof resolvedContext>> & { ok: true }, error: { code: string; message: string; retryable: boolean }, issueIdentity: ResourceIdentity | null): IssueResult {
|
|
210
|
+
const issueNumber = resourceIdentityNumber(issueIdentity);
|
|
211
|
+
return result(error.code.startsWith('IN_LABEL_SYNC') ? 'blocked' : providerStatus(error), base.resolved.taskId, issueNumber, {
|
|
212
|
+
changed: error.code === 'IN_LABEL_SYNC_PARTIAL',
|
|
213
|
+
platform: base.context.platform,
|
|
214
|
+
capabilities: base.context.capabilities,
|
|
215
|
+
resource: { kind: 'issue', number: issueNumber, ...(issueIdentity ? { identity: issueIdentity } : {}) },
|
|
216
|
+
error: providerError(error, 'PLATFORM_PROVIDER_OPERATION_FAILED')
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async function inspectExternalIssue(
|
|
221
|
+
base: Awaited<ReturnType<typeof resolvedContext>> & { ok: true },
|
|
222
|
+
issueIdentity: ResourceIdentity
|
|
223
|
+
): Promise<IssueResult> {
|
|
224
|
+
const operation = base.provider.issues?.inspect
|
|
225
|
+
? await base.provider.issues.inspect({ context: providerOperationContext(base.loadedContext), target: issueIdentity })
|
|
226
|
+
: unsupportedProviderOperation(base.provider, 'issues.inspect');
|
|
227
|
+
if (!operation.ok) return providerIssueError(base, operation.error, issueIdentity);
|
|
228
|
+
const issue = normalizeProviderIssue(operation.value, base.context.platform.repository!, resourceIdentityNumber(issueIdentity));
|
|
229
|
+
return result('no-op', base.resolved.taskId, resourceIdentityNumber(issueIdentity), {
|
|
230
|
+
platform: base.context.platform,
|
|
231
|
+
capabilities: base.context.capabilities,
|
|
232
|
+
resource: { kind: 'issue', number: resourceIdentityNumber(issueIdentity), identity: issueIdentity },
|
|
233
|
+
issue,
|
|
234
|
+
error: null
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function normalizeIssue(remote: RemoteIssue, repository: string, fallbackNumber?: number): IssueSnapshot | null {
|
|
239
|
+
const number = Number.isInteger(remote.number) && Number(remote.number) > 0
|
|
240
|
+
? Number(remote.number)
|
|
241
|
+
: fallbackNumber;
|
|
242
|
+
if (!number || !Number.isSafeInteger(number) || remote.pull_request) return null;
|
|
123
243
|
return {
|
|
124
244
|
repository,
|
|
125
|
-
number
|
|
245
|
+
number,
|
|
126
246
|
databaseId: Number.isInteger(remote.id) ? Number(remote.id) : null,
|
|
127
|
-
nodeId: remote.node_id
|
|
128
|
-
url: remote.html_url || `https://github.com/${repository}/issues/${
|
|
129
|
-
state: remote.state === 'closed' ? 'closed' : 'open',
|
|
247
|
+
nodeId: remote.node_id || `issue-${number}`,
|
|
248
|
+
url: remote.html_url || `https://github.com/${repository}/issues/${number}`,
|
|
249
|
+
state: String(remote.state || '').toLowerCase() === 'closed' ? 'closed' : 'open',
|
|
130
250
|
title: remote.title || '',
|
|
131
251
|
body: remote.body || '',
|
|
132
252
|
labels: (remote.labels || []).map((label) => typeof label === 'string' ? label : label.name || '').filter(Boolean).sort(),
|
|
133
253
|
assignees: (remote.assignees || []).map((assignee) => assignee.login || '').filter(Boolean).sort(),
|
|
134
254
|
milestone: remote.milestone?.title || null,
|
|
135
|
-
issueType: null,
|
|
255
|
+
issueType: remote.type?.name || null,
|
|
136
256
|
fields: {}
|
|
137
257
|
};
|
|
138
258
|
}
|
|
@@ -173,15 +293,17 @@ function normalizeCurrentFields(value: unknown): { issueId: string | null; type:
|
|
|
173
293
|
issueType?: { id?: string; name?: string; pinnedFields?: unknown[] } | null;
|
|
174
294
|
issueFieldValues?: { nodes?: unknown[] };
|
|
175
295
|
} } } })?.data?.repository?.issue;
|
|
176
|
-
const type = issue?.issueType?.
|
|
177
|
-
id: issue.issueType.id
|
|
296
|
+
const type = issue?.issueType?.name ? {
|
|
297
|
+
id: issue.issueType.id || issue.issueType.name,
|
|
298
|
+
name: issue.issueType.name,
|
|
299
|
+
fields: normalizeFieldSchemas(issue.issueType.pinnedFields)
|
|
178
300
|
} : null;
|
|
179
301
|
const values = (issue?.issueFieldValues?.nodes || []).flatMap((raw) => {
|
|
180
302
|
const item = raw as { __typename?: string; name?: string; value?: string | number; field?: { id?: string; name?: string } };
|
|
181
303
|
const kind = fieldKind(item);
|
|
182
304
|
const value = kind === 'single-select' ? item.name : item.value;
|
|
183
|
-
return item.field?.
|
|
184
|
-
? [{ id: item.field.id, name: item.field.name, kind, value: value ?? null }]
|
|
305
|
+
return item.field?.name && kind && value !== undefined
|
|
306
|
+
? [{ id: item.field.id || item.field.name, name: item.field.name, kind, value: value ?? null }]
|
|
185
307
|
: [];
|
|
186
308
|
});
|
|
187
309
|
return { issueId: issue?.id || null, type, values };
|
|
@@ -204,7 +326,7 @@ function fetchIssue(client: GitHubClient, repository: string, issue: number, cwd
|
|
|
204
326
|
function inspectGitHubIssue(client: GitHubClient, repository: string, issue: number, cwd: string) {
|
|
205
327
|
const fetched = fetchIssue(client, repository, issue, cwd);
|
|
206
328
|
if (!fetched.ok) return fetched;
|
|
207
|
-
const snapshot = normalizeIssue(fetched.value, repository);
|
|
329
|
+
const snapshot = normalizeIssue(fetched.value, repository, issue);
|
|
208
330
|
return snapshot
|
|
209
331
|
? { ok: true as const, value: snapshot }
|
|
210
332
|
: { ok: false as const, error: { code: 'ISSUE_IDENTITY_INVALID', message: 'Remote resource is not a valid Issue', retryable: false } };
|
|
@@ -218,37 +340,21 @@ function inspectGitHubIssueMetadata(client: GitHubClient, repository: string, is
|
|
|
218
340
|
ok: true as const,
|
|
219
341
|
value: {
|
|
220
342
|
state: String(remote.state || '').toLowerCase() === 'closed' ? 'closed' as const : 'open' as const,
|
|
221
|
-
labels: (remote.labels || []).map((label) => typeof label === 'string' ? label : label.name || '').filter(Boolean).sort(),
|
|
343
|
+
labels: (remote.labels || []).map((label: string | { name?: string }) => typeof label === 'string' ? label : label.name || '').filter(Boolean).sort(),
|
|
222
344
|
body: remote.body || '',
|
|
223
345
|
milestone: remote.milestone?.title || null
|
|
224
346
|
}
|
|
225
347
|
};
|
|
226
348
|
}
|
|
227
349
|
|
|
228
|
-
function inspectPlatformIssue(taskRef: string, options: SharedOptions = {}): IssueResult {
|
|
229
|
-
const base = resolvedContext(taskRef, options);
|
|
350
|
+
async function inspectPlatformIssue(taskRef: string, options: SharedOptions = {}): Promise<IssueResult> {
|
|
351
|
+
const base = await resolvedContext(taskRef, options);
|
|
230
352
|
if (!base.ok) return base.output;
|
|
231
|
-
if (!base.
|
|
232
|
-
platform: base.context.platform, capabilities: base.context.capabilities,
|
|
233
|
-
error: { code: 'ISSUE_NOT_LINKED', message: 'Task has no valid issue_number', retryable: false }
|
|
234
|
-
});
|
|
235
|
-
const fetched = inspectGitHubIssue(base.client, base.context.platform.repository!, base.issueNumber, base.resolved.repoRoot);
|
|
236
|
-
if (!fetched.ok) return result(fetched.error.retryable ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, {
|
|
237
|
-
platform: base.context.platform, capabilities: base.context.capabilities,
|
|
238
|
-
resource: { kind: 'issue', number: base.issueNumber }, error: fetched.error
|
|
239
|
-
});
|
|
240
|
-
const issue = fetched.value;
|
|
241
|
-
if (base.context.capabilities.push) {
|
|
242
|
-
const graph = graphState(base.client, base.context.platform.repository!, base.issueNumber, base.resolved.repoRoot);
|
|
243
|
-
if (graph) {
|
|
244
|
-
issue.issueType = graph.type?.name || null;
|
|
245
|
-
issue.fields = Object.fromEntries(graph.values.map((field) => [field.name, field.value]));
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
return result('no-op', base.resolved.taskId, base.issueNumber, {
|
|
353
|
+
if (!base.issueIdentity) return result('no-op', base.resolved.taskId, null, {
|
|
249
354
|
platform: base.context.platform, capabilities: base.context.capabilities,
|
|
250
|
-
|
|
355
|
+
error: { code: 'ISSUE_NOT_LINKED', message: 'Task has no valid platform issue identity', retryable: false }
|
|
251
356
|
});
|
|
357
|
+
return inspectExternalIssue(base, base.issueIdentity);
|
|
252
358
|
}
|
|
253
359
|
|
|
254
360
|
function taskTitle(content: string): string {
|
|
@@ -321,101 +427,112 @@ function deterministicIssueBody(repoRoot: string, content: string, taskType: str
|
|
|
321
427
|
return buildDefaultBody(content);
|
|
322
428
|
}
|
|
323
429
|
|
|
324
|
-
function createPlatformIssue(taskRef: string, options: CreateOptions): IssueResult {
|
|
325
|
-
const base = resolvedContext(taskRef, options);
|
|
430
|
+
async function createPlatformIssue(taskRef: string, options: CreateOptions): Promise<IssueResult> {
|
|
431
|
+
const base = await resolvedContext(taskRef, options);
|
|
326
432
|
if (!base.ok) return base.output;
|
|
327
|
-
if (base.
|
|
433
|
+
if (base.issueIdentity) return inspectPlatformIssue(taskRef, options);
|
|
328
434
|
const repository = base.context.platform.repository!;
|
|
329
435
|
let repositoryLabels: string[] = [];
|
|
330
|
-
let milestones:
|
|
436
|
+
let milestones: string[] = [];
|
|
331
437
|
if (base.context.capabilities.triage) {
|
|
332
|
-
const
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
if (
|
|
336
|
-
|
|
337
|
-
|
|
438
|
+
const metadata = base.provider.issues?.describeRepository
|
|
439
|
+
? await base.provider.issues.describeRepository({ context: providerOperationContext(base.loadedContext) })
|
|
440
|
+
: unsupportedProviderOperation(base.provider, 'issues.describeRepository');
|
|
441
|
+
if (!metadata.ok) return providerIssueError(base, metadata.error, null);
|
|
442
|
+
repositoryLabels = metadata.value.labels.map((label) => label.name);
|
|
443
|
+
milestones = metadata.value.milestones.map((milestone) => milestone.title);
|
|
338
444
|
}
|
|
339
445
|
const typeLabel = ({
|
|
340
446
|
bug: 'type: bug', bugfix: 'type: bug', feature: 'type: feature', enhancement: 'type: enhancement',
|
|
341
447
|
refactor: 'type: enhancement', refactoring: 'type: enhancement', documentation: 'type: documentation',
|
|
342
448
|
docs: 'type: documentation', 'dependency-upgrade': 'type: dependency-upgrade', task: 'type: task', chore: 'type: task'
|
|
343
449
|
} as Record<string, string>)[base.frontmatter.type || ''];
|
|
344
|
-
const explicitMilestone = base.frontmatter.milestone && milestones.some((item) => item
|
|
450
|
+
const explicitMilestone = base.frontmatter.milestone && milestones.some((item) => item === base.frontmatter.milestone)
|
|
345
451
|
? base.frontmatter.milestone
|
|
346
|
-
: chooseMilestone('initial', milestones
|
|
452
|
+
: chooseMilestone('initial', milestones);
|
|
347
453
|
const payload = {
|
|
348
454
|
title: conventionalTitle(base.content, base.frontmatter.type || 'task', base.resolved.repoRoot),
|
|
349
455
|
body: deterministicIssueBody(base.resolved.repoRoot, base.content, base.frontmatter.type || 'task'),
|
|
350
456
|
assignees: base.context.platform.currentUser ? [base.context.platform.currentUser] : [],
|
|
351
457
|
...(typeLabel && repositoryLabels.includes(typeLabel) ? { labels: [typeLabel] } : {}),
|
|
352
|
-
...(explicitMilestone ? { milestone:
|
|
458
|
+
...(explicitMilestone ? { milestone: explicitMilestone } : {})
|
|
353
459
|
};
|
|
354
460
|
if (options.dryRun) return result('planned', base.resolved.taskId, null, {
|
|
355
461
|
platform: base.context.platform, capabilities: base.context.capabilities,
|
|
356
462
|
operations: [{ name: 'issue:create', status: 'planned', reasonCode: null }], error: null
|
|
357
463
|
});
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
464
|
+
{
|
|
465
|
+
const created = base.provider.issues?.create
|
|
466
|
+
? await base.provider.issues.create({
|
|
467
|
+
context: providerOperationContext(base.loadedContext),
|
|
468
|
+
desired: {
|
|
469
|
+
title: payload.title,
|
|
470
|
+
body: payload.body,
|
|
471
|
+
labels: payload.labels || [],
|
|
472
|
+
assignees: payload.assignees,
|
|
473
|
+
milestone: typeof payload.milestone === 'string' ? payload.milestone : null,
|
|
474
|
+
fields: {}
|
|
475
|
+
},
|
|
476
|
+
mutation: { idempotencyKey: `issue:create:${base.resolved.taskId}` }
|
|
477
|
+
})
|
|
478
|
+
: unsupportedProviderOperation(base.provider, 'issues.create');
|
|
479
|
+
if (!created.ok) return providerIssueError(base, created.error, null);
|
|
480
|
+
let createdIdentity: ResourceIdentity;
|
|
481
|
+
try {
|
|
482
|
+
createdIdentity = providerResourceIdentity(base.provider, 'issue', created.value.remoteId);
|
|
483
|
+
} catch (error) {
|
|
484
|
+
return result('failed', base.resolved.taskId, null, {
|
|
485
|
+
platform: base.context.platform, capabilities: base.context.capabilities,
|
|
486
|
+
error: { code: 'ISSUE_CREATE_RESPONSE_INVALID', message: error instanceof Error ? error.message : String(error), retryable: false }
|
|
487
|
+
});
|
|
366
488
|
}
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
489
|
+
const inspected = base.provider.issues?.inspect
|
|
490
|
+
? await base.provider.issues.inspect({
|
|
491
|
+
context: providerOperationContext(base.loadedContext),
|
|
492
|
+
target: createdIdentity
|
|
493
|
+
})
|
|
494
|
+
: unsupportedProviderOperation(base.provider, 'issues.inspect');
|
|
495
|
+
if (!inspected.ok) return providerIssueError(base, inspected.error, createdIdentity);
|
|
496
|
+
const issueNumber = resourceIdentityNumber(inspected.value.identity || createdIdentity);
|
|
497
|
+
const written = writeTask({
|
|
498
|
+
taskRef: base.resolved.taskId, expectedState: 'active', mutations: [{ kind: 'frontmatter', set: { platform_issue_identity: serializeResourceIdentity(inspected.value.identity || createdIdentity) } }]
|
|
499
|
+
}, { repoRoot: base.resolved.repoRoot });
|
|
500
|
+
if (written.status === 'failed') return result('failed', base.resolved.taskId, null, {
|
|
371
501
|
platform: base.context.platform, capabilities: base.context.capabilities,
|
|
372
|
-
|
|
502
|
+
resource: { kind: 'issue', number: issueNumber, identity: inspected.value.identity || createdIdentity },
|
|
503
|
+
error: { code: 'ISSUE_CREATED_BIND_FAILED', message: written.error.message, retryable: false }
|
|
504
|
+
});
|
|
505
|
+
return result('applied', base.resolved.taskId, issueNumber, {
|
|
506
|
+
changed: true,
|
|
507
|
+
platform: base.context.platform,
|
|
508
|
+
capabilities: base.context.capabilities,
|
|
509
|
+
resource: { kind: 'issue', number: issueNumber, identity: inspected.value.identity || createdIdentity },
|
|
510
|
+
operations: [{ name: 'issue:create', status: 'applied', reasonCode: null }, { name: 'task:bind', status: 'applied', reasonCode: null }],
|
|
511
|
+
issue: normalizeProviderIssue(inspected.value, repository, issueNumber),
|
|
512
|
+
error: null
|
|
373
513
|
});
|
|
374
514
|
}
|
|
375
|
-
const written = writeTask({
|
|
376
|
-
taskRef: base.resolved.taskId, expectedState: 'active', mutations: [{ kind: 'frontmatter', set: { issue_number: issueNumber } }]
|
|
377
|
-
}, { repoRoot: base.resolved.repoRoot });
|
|
378
|
-
if (written.status === 'failed') return result('failed', base.resolved.taskId, null, {
|
|
379
|
-
platform: base.context.platform, capabilities: base.context.capabilities,
|
|
380
|
-
resource: { kind: 'issue', number: issueNumber },
|
|
381
|
-
operations: [{ name: 'issue:create', status: 'applied', reasonCode: null }, { name: 'task:bind', status: 'failed', reasonCode: written.error.code }],
|
|
382
|
-
error: { code: 'ISSUE_CREATED_BIND_FAILED', message: `${created.value.html_url}: ${written.error.message}`, retryable: false }
|
|
383
|
-
});
|
|
384
|
-
return result('applied', base.resolved.taskId, issueNumber, {
|
|
385
|
-
changed: true, platform: base.context.platform, capabilities: base.context.capabilities,
|
|
386
|
-
resource: { kind: 'issue', number: issueNumber },
|
|
387
|
-
operations: [{ name: 'issue:create', status: 'applied', reasonCode: null }, { name: 'task:bind', status: 'applied', reasonCode: null }],
|
|
388
|
-
issue: normalizeIssue({
|
|
389
|
-
...created.value,
|
|
390
|
-
state: 'open',
|
|
391
|
-
title: payload.title,
|
|
392
|
-
body: payload.body,
|
|
393
|
-
labels: [],
|
|
394
|
-
assignees: payload.assignees.map((login) => ({ login })),
|
|
395
|
-
milestone: null
|
|
396
|
-
}, repository),
|
|
397
|
-
error: null
|
|
398
|
-
});
|
|
399
515
|
}
|
|
400
516
|
|
|
401
|
-
function bindPlatformIssue(taskRef: string, options: BindOptions): IssueResult {
|
|
402
|
-
const base = resolvedContext(taskRef, options);
|
|
517
|
+
async function bindPlatformIssue(taskRef: string, options: BindOptions): Promise<IssueResult> {
|
|
518
|
+
const base = await resolvedContext(taskRef, options);
|
|
403
519
|
if (!base.ok) return base.output;
|
|
404
|
-
|
|
405
|
-
|
|
520
|
+
let identity: ResourceIdentity;
|
|
521
|
+
try { identity = providerResourceToken(base.provider, 'issue', String(options.issue)); }
|
|
522
|
+
catch (error) { return result('failed', base.resolved.taskId, base.issueNumber, { error: { code: 'PLATFORM_IDENTITY_TOKEN_INVALID', message: error instanceof Error ? error.message : String(error), retryable: false } }); }
|
|
523
|
+
if (base.issueIdentity && !resourceIdentityEquals(base.issueIdentity, identity)) return result('failed', base.resolved.taskId, base.issueNumber, {
|
|
524
|
+
error: { code: 'ISSUE_BIND_CONFLICT', message: 'Task is already bound to a different Issue identity', retryable: false }
|
|
406
525
|
});
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
if (base.issueNumber === options.issue) return result('no-op', base.resolved.taskId, options.issue, { issue, error: null });
|
|
415
|
-
const written = writeTask({ taskRef: base.resolved.taskId, expectedState: 'active', dryRun: options.dryRun, mutations: [{ kind: 'frontmatter', set: { issue_number: options.issue } }] }, { repoRoot: base.resolved.repoRoot });
|
|
526
|
+
const fetched = base.provider.issues?.inspect
|
|
527
|
+
? await base.provider.issues.inspect({ context: providerOperationContext(base.loadedContext), target: identity })
|
|
528
|
+
: unsupportedProviderOperation(base.provider, 'issues.inspect');
|
|
529
|
+
if (!fetched.ok) return providerIssueError(base, fetched.error, identity);
|
|
530
|
+
const issue = normalizeProviderIssue(fetched.value, base.context.platform.repository!, resourceIdentityNumber(identity));
|
|
531
|
+
if (base.issueIdentity && resourceIdentityEquals(base.issueIdentity, identity)) return result('no-op', base.resolved.taskId, resourceIdentityNumber(identity), { issue, error: null });
|
|
532
|
+
const written = writeTask({ taskRef: base.resolved.taskId, expectedState: 'active', dryRun: options.dryRun, mutations: [{ kind: 'frontmatter', set: { platform_issue_identity: serializeResourceIdentity(identity) } }] }, { repoRoot: base.resolved.repoRoot });
|
|
416
533
|
if (written.status === 'failed') return result('failed', base.resolved.taskId, null, { issue, error: { code: written.error.code, message: written.error.message, retryable: false } });
|
|
417
|
-
return result(options.dryRun ? 'planned' : 'applied', base.resolved.taskId, options.dryRun ? null :
|
|
418
|
-
changed: !options.dryRun, issue, operations: [{ name: 'task:bind', status: options.dryRun ? 'planned' : 'applied', reasonCode: null }], error: null
|
|
534
|
+
return result(options.dryRun ? 'planned' : 'applied', base.resolved.taskId, options.dryRun ? null : resourceIdentityNumber(identity), {
|
|
535
|
+
changed: !options.dryRun, issue, resource: { kind: 'issue', number: resourceIdentityNumber(identity), identity }, operations: [{ name: 'task:bind', status: options.dryRun ? 'planned' : 'applied', reasonCode: null }], error: null
|
|
419
536
|
});
|
|
420
537
|
}
|
|
421
538
|
|
|
@@ -466,29 +583,34 @@ function applyRestOperations(snapshot: IssueSnapshot, operations: PlannedOperati
|
|
|
466
583
|
let labelsChanged = false;
|
|
467
584
|
for (const operation of operations) {
|
|
468
585
|
if (operation.status !== 'planned') continue;
|
|
586
|
+
if (operation.name === 'assignees') payload.assignees = operation.value;
|
|
587
|
+
if (operation.name === 'requirements') payload.body = operation.value;
|
|
588
|
+
if (operation.name === 'state') payload.state = operation.value;
|
|
589
|
+
if (operation.name === 'milestone') payload.milestone = operation.value;
|
|
469
590
|
if (operation.name === 'labels:status') {
|
|
470
|
-
labels = [
|
|
471
|
-
...labels.filter((label) => !label.startsWith('status:')),
|
|
472
|
-
...(operation.value as string[]).filter((label) => label.startsWith('status:'))
|
|
473
|
-
];
|
|
591
|
+
labels = applyLabelDeltaToSnapshot(labels, operation.value as string[], 'status:');
|
|
474
592
|
labelsChanged = true;
|
|
475
593
|
}
|
|
476
594
|
if (operation.name === 'labels:in') {
|
|
477
|
-
labels = [
|
|
478
|
-
...labels.filter((label) => !label.startsWith('in:')),
|
|
479
|
-
...(operation.value as string[]).filter((label) => label.startsWith('in:'))
|
|
480
|
-
];
|
|
595
|
+
labels = applyLabelDeltaToSnapshot(labels, operation.value as string[], 'in:');
|
|
481
596
|
labelsChanged = true;
|
|
482
597
|
}
|
|
483
|
-
if (operation.name === '
|
|
484
|
-
if (operation.name === '
|
|
485
|
-
if (operation.name === 'state') payload.state = operation.value;
|
|
486
|
-
if (operation.name === 'milestone') payload.milestone = operation.value;
|
|
598
|
+
if (operation.name === 'issue-type') payload.issueType = operation.value;
|
|
599
|
+
if (operation.name === 'fields') payload.fields = operation.value;
|
|
487
600
|
}
|
|
488
|
-
if (labelsChanged) payload.labels =
|
|
601
|
+
if (labelsChanged) payload.labels = labels;
|
|
489
602
|
return payload;
|
|
490
603
|
}
|
|
491
604
|
|
|
605
|
+
function applyLabelDeltaToSnapshot(current: string[], target: readonly string[], prefix: string): string[] {
|
|
606
|
+
const delta = labelDelta(current, target, prefix);
|
|
607
|
+
const targetLabels = [...new Set(target.filter((label) => label.startsWith(prefix)))];
|
|
608
|
+
return [...new Set([
|
|
609
|
+
...current.filter((label) => !delta.remove.includes(label)),
|
|
610
|
+
...targetLabels
|
|
611
|
+
])].sort();
|
|
612
|
+
}
|
|
613
|
+
|
|
492
614
|
type DesiredFieldValue = {
|
|
493
615
|
fieldId: string;
|
|
494
616
|
name: string;
|
|
@@ -587,26 +709,27 @@ function executeGraphOperation(
|
|
|
587
709
|
});
|
|
588
710
|
}
|
|
589
711
|
|
|
590
|
-
function syncPlatformIssue(taskRef: string, options: SyncOptions): IssueResult {
|
|
591
|
-
const base = resolvedContext(taskRef, options);
|
|
712
|
+
async function syncPlatformIssue(taskRef: string, options: SyncOptions): Promise<IssueResult> {
|
|
713
|
+
const base = await resolvedContext(taskRef, options);
|
|
592
714
|
if (!base.ok) return base.output;
|
|
593
|
-
if (!base.
|
|
715
|
+
if (!base.issueIdentity) return result('failed', base.resolved.taskId, null, { error: { code: 'ISSUE_NOT_LINKED', message: 'Task has no valid platform issue identity', retryable: false } });
|
|
594
716
|
const repository = base.context.platform.repository!;
|
|
595
|
-
const
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
if (!
|
|
717
|
+
const inspected = base.provider.issues?.inspect
|
|
718
|
+
? await base.provider.issues.inspect({ context: providerOperationContext(base.loadedContext), target: base.issueIdentity })
|
|
719
|
+
: unsupportedProviderOperation(base.provider, 'issues.inspect');
|
|
720
|
+
if (!inspected.ok) return providerIssueError(base, inspected.error, base.issueIdentity);
|
|
721
|
+
const snapshot = normalizeProviderIssue(inspected.value, repository, resourceIdentityNumber(base.issueIdentity));
|
|
722
|
+
let providerMetadata: RepositoryMetadataSnapshot | null = null;
|
|
599
723
|
let repositoryLabels: string[] = [];
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
milestones = flattenPages(listed.value).map((item) => ({ title: String((item as { title?: string }).title || ''), number: Number((item as { number?: number }).number) })).filter((item) => item.title && Number.isInteger(item.number));
|
|
724
|
+
let milestones: string[] = [];
|
|
725
|
+
if (base.context.capabilities.triage && (options.status !== undefined || options.inLabels !== undefined || options.milestone !== undefined || options.issueType || options.fields)) {
|
|
726
|
+
const metadata = base.provider.issues?.describeRepository
|
|
727
|
+
? await base.provider.issues.describeRepository({ context: providerOperationContext(base.loadedContext) })
|
|
728
|
+
: unsupportedProviderOperation(base.provider, 'issues.describeRepository');
|
|
729
|
+
if (!metadata.ok) return providerIssueError(base, metadata.error, base.issueIdentity);
|
|
730
|
+
providerMetadata = metadata.value;
|
|
731
|
+
repositoryLabels = metadata.value.labels.map((label) => label.name);
|
|
732
|
+
milestones = metadata.value.milestones.map((milestone) => milestone.title);
|
|
610
733
|
}
|
|
611
734
|
const desired: IssueDesiredState = {
|
|
612
735
|
status: options.status,
|
|
@@ -619,7 +742,7 @@ function syncPlatformIssue(taskRef: string, options: SyncOptions): IssueResult {
|
|
|
619
742
|
snapshot: options.milestone === 'specific'
|
|
620
743
|
? { ...snapshot, milestone: milestoneBasis(base.resolved.repoRoot, snapshot.milestone) }
|
|
621
744
|
: snapshot,
|
|
622
|
-
desired, repositoryLabels: new Set(repositoryLabels), milestones
|
|
745
|
+
desired, repositoryLabels: new Set(repositoryLabels), milestones,
|
|
623
746
|
currentUser: base.context.platform.currentUser,
|
|
624
747
|
requirementAnchors: requirementSectionAnchors(base.resolved.repoRoot, base.frontmatter.type || 'task'),
|
|
625
748
|
capabilities: base.context.capabilities
|
|
@@ -634,48 +757,94 @@ function syncPlatformIssue(taskRef: string, options: SyncOptions): IssueResult {
|
|
|
634
757
|
: { name: 'labels:in', status: 'skipped', reasonCode: 'TRIAGE_REQUIRED' });
|
|
635
758
|
}
|
|
636
759
|
if (options.inLabels === 'from-diff') {
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
760
|
+
const taskBase = typeof base.frontmatter.delivery_base_ref === 'string'
|
|
761
|
+
? base.frontmatter.delivery_base_ref.trim()
|
|
762
|
+
: '';
|
|
763
|
+
if (!taskBase) {
|
|
764
|
+
plan.operations.push({ name: 'labels:in', status: 'failed', reasonCode: 'IN_LABEL_SYNC_BASE_MISSING' });
|
|
765
|
+
} else if (options.base && options.base !== taskBase) {
|
|
766
|
+
plan.operations.push({ name: 'labels:in', status: 'failed', reasonCode: 'IN_LABEL_SYNC_BASE_MISMATCH' });
|
|
767
|
+
} else {
|
|
768
|
+
try {
|
|
769
|
+
const changed = execFileSync('git', ['diff', `${taskBase}...HEAD`, '--name-only'], {
|
|
770
|
+
cwd: base.resolved.repoRoot, encoding: 'utf8'
|
|
771
|
+
}).trim().split(/\r?\n/).filter(Boolean);
|
|
772
|
+
const config = JSON.parse(fs.readFileSync(`${base.resolved.repoRoot}/.agents/.airc.json`, 'utf8')) as {
|
|
773
|
+
labels?: { in?: Record<string, unknown> };
|
|
774
|
+
};
|
|
775
|
+
const mapping = validateInLabelMapping(config.labels?.in);
|
|
776
|
+
if (!mapping.ok) {
|
|
777
|
+
plan.operations.push({ name: 'labels:in', status: 'failed', reasonCode: mapping.error.code });
|
|
778
|
+
} else {
|
|
779
|
+
const planned = planInLabelUpdate({
|
|
780
|
+
changedFiles: changed,
|
|
781
|
+
currentLabels: snapshot.labels,
|
|
782
|
+
mapping: mapping.value,
|
|
783
|
+
repositoryLabels: new Set(repositoryLabels)
|
|
784
|
+
});
|
|
785
|
+
if (planned.error) {
|
|
786
|
+
plan.operations.push({ name: 'labels:in', status: 'failed', reasonCode: planned.error.code });
|
|
787
|
+
} else {
|
|
788
|
+
plan.operations.push(planned.changed
|
|
789
|
+
? { name: 'labels:in', status: 'planned', reasonCode: null, value: planned.labels }
|
|
790
|
+
: { name: 'labels:in', status: 'no-op', reasonCode: null });
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
} catch {
|
|
794
|
+
if (!plan.operations.some((operation) => operation.name === 'labels:in' && operation.status === 'failed')) {
|
|
795
|
+
plan.operations.push({ name: 'labels:in', status: 'failed', reasonCode: 'IN_LABEL_SYNC_EVIDENCE_UNAVAILABLE' });
|
|
796
|
+
}
|
|
797
|
+
}
|
|
647
798
|
}
|
|
648
799
|
}
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
800
|
+
if (options.issueType || options.fields) {
|
|
801
|
+
plan.operations.push(...(providerMetadata
|
|
802
|
+
? planProviderMetadata(inspected.value, providerMetadata, base.frontmatter, options, base.context.capabilities)
|
|
803
|
+
: [{ name: 'issue-type', status: 'skipped' as const, reasonCode: 'ISSUE_TYPES_UNSUPPORTED' }]));
|
|
804
|
+
}
|
|
654
805
|
const failure = plan.operations.find((operation) => operation.status === 'failed');
|
|
655
806
|
if (failure) return result('failed', base.resolved.taskId, base.issueNumber, { issue: snapshot, operations: plan.operations, error: { code: failure.reasonCode || 'ISSUE_SYNC_FAILED', message: `Operation ${failure.name} failed`, retryable: false } });
|
|
656
807
|
const planned = plan.operations.filter((operation) => operation.status === 'planned');
|
|
657
808
|
const skipped = plan.operations.some((operation) => operation.status === 'skipped');
|
|
658
809
|
if (options.dryRun) return result(planned.length ? 'planned' : skipped ? 'degraded' : 'no-op', base.resolved.taskId, base.issueNumber, { issue: snapshot, operations: plan.operations, error: null });
|
|
659
810
|
if (planned.length === 0) return result(skipped ? 'degraded' : 'no-op', base.resolved.taskId, base.issueNumber, { issue: snapshot, operations: plan.operations, error: null });
|
|
660
|
-
const
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
811
|
+
const payload = applyRestOperations(snapshot, planned) as Partial<{
|
|
812
|
+
title: string; body: string; labels: string[]; assignees: string[];
|
|
813
|
+
milestone: string | null; state: 'open' | 'closed'; fields: Record<string, string | number | null>;
|
|
814
|
+
}>;
|
|
815
|
+
const updated = base.provider.issues?.update
|
|
816
|
+
? await base.provider.issues.update({ context: providerOperationContext(base.loadedContext), target: base.issueIdentity, currentLabels: snapshot.labels, patch: payload, mutation: { idempotencyKey: `issue:update:${base.resolved.taskId}` } })
|
|
817
|
+
: unsupportedProviderOperation(base.provider, 'issues.update');
|
|
818
|
+
if (!updated.ok) return providerIssueError(base, updated.error, base.issueIdentity);
|
|
819
|
+
const labelOperations = planned.filter((operation) => operation.name === 'labels:status' || operation.name === 'labels:in');
|
|
820
|
+
let finalIssue = snapshot;
|
|
821
|
+
if (labelOperations.length > 0) {
|
|
822
|
+
const reread = base.provider.issues?.inspect
|
|
823
|
+
? await base.provider.issues.inspect({ context: providerOperationContext(base.loadedContext), target: base.issueIdentity })
|
|
824
|
+
: unsupportedProviderOperation(base.provider, 'issues.inspect');
|
|
825
|
+
if (!reread.ok) return result('blocked', base.resolved.taskId, base.issueNumber, {
|
|
826
|
+
changed: updated.value.changed,
|
|
827
|
+
issue: snapshot,
|
|
828
|
+
operations: plan.operations,
|
|
829
|
+
error: { ...reread.error, code: 'IN_LABEL_SYNC_PARTIAL', message: `In-label synchronization is partial or unknown: ${reread.error.message}` }
|
|
830
|
+
});
|
|
831
|
+
finalIssue = normalizeProviderIssue(reread.value, repository, resourceIdentityNumber(base.issueIdentity));
|
|
832
|
+
let expectedLabels = [...snapshot.labels];
|
|
833
|
+
for (const operation of labelOperations) {
|
|
834
|
+
expectedLabels = applyLabelDeltaToSnapshot(expectedLabels, operation.value as string[], operation.name === 'labels:status' ? 'status:' : 'in:');
|
|
835
|
+
}
|
|
836
|
+
const expected = expectedLabels.filter((label) => label.startsWith('status:') || label.startsWith('in:')).sort();
|
|
837
|
+
const actual = finalIssue.labels.filter((label) => label.startsWith('status:') || label.startsWith('in:')).sort();
|
|
838
|
+
if (expected.join('\0') !== actual.join('\0')) return result('blocked', base.resolved.taskId, base.issueNumber, {
|
|
839
|
+
changed: updated.value.changed,
|
|
840
|
+
issue: finalIssue,
|
|
841
|
+
operations: plan.operations,
|
|
842
|
+
error: { code: 'IN_LABEL_SYNC_PARTIAL', message: 'Issue labels did not converge after update', retryable: true }
|
|
668
843
|
});
|
|
669
|
-
if (!patched.ok) return result(patched.error.retryable ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, { issue: snapshot, operations: plan.operations, error: patched.error });
|
|
670
|
-
}
|
|
671
|
-
for (const operation of graphPlanned) {
|
|
672
|
-
if (!graph.issueId) return result('failed', base.resolved.taskId, base.issueNumber, { issue: snapshot, operations: plan.operations, error: { code: 'ISSUE_GRAPH_IDENTITY_INVALID', message: 'Issue GraphQL identity is unavailable', retryable: false } });
|
|
673
|
-
const written = executeGraphOperation(base.client, base.resolved.repoRoot, graph.issueId, operation);
|
|
674
|
-
if (!written.ok) return result(written.error.retryable ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, { issue: snapshot, operations: plan.operations, error: written.error });
|
|
675
844
|
}
|
|
676
845
|
return result(skipped ? 'degraded' : 'applied', base.resolved.taskId, base.issueNumber, {
|
|
677
|
-
changed:
|
|
678
|
-
resource: { kind: 'issue', number: base.issueNumber }, issue:
|
|
846
|
+
changed: updated.value.changed, platform: base.context.platform, capabilities: base.context.capabilities,
|
|
847
|
+
resource: { kind: 'issue', number: base.issueNumber, identity: base.issueIdentity }, issue: finalIssue,
|
|
679
848
|
operations: plan.operations.map((operation) => operation.status === 'planned' ? { ...operation, status: 'applied' } : operation), error: null
|
|
680
849
|
});
|
|
681
850
|
}
|
|
@@ -683,6 +852,7 @@ function syncPlatformIssue(taskRef: string, options: SyncOptions): IssueResult {
|
|
|
683
852
|
export {
|
|
684
853
|
bindPlatformIssue,
|
|
685
854
|
createPlatformIssue,
|
|
855
|
+
graphState,
|
|
686
856
|
inspectGitHubIssue,
|
|
687
857
|
inspectGitHubIssueMetadata,
|
|
688
858
|
inspectPlatformIssue,
|