@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
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { computeInLabels } from './metadata-labels.ts';
|
|
2
|
+
import type { PlatformError } from './types.ts';
|
|
3
|
+
|
|
4
|
+
type InLabelPlan = {
|
|
5
|
+
current: string[];
|
|
6
|
+
target: string[];
|
|
7
|
+
labels: string[];
|
|
8
|
+
changed: boolean;
|
|
9
|
+
error: InLabelValidationError | null;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
type InLabelResource = {
|
|
13
|
+
kind: 'issue' | 'pull-request';
|
|
14
|
+
number: number;
|
|
15
|
+
labels: string[];
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
type InLabelValidationError = {
|
|
19
|
+
code: string;
|
|
20
|
+
message: string;
|
|
21
|
+
retryable: boolean;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
type LabelPrefix = string | readonly string[];
|
|
25
|
+
|
|
26
|
+
type LabelDelta = {
|
|
27
|
+
add: string[];
|
|
28
|
+
remove: string[];
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
type LabelDeltaResult = {
|
|
32
|
+
status: 'applied' | 'no-op' | 'failed' | 'blocked';
|
|
33
|
+
changed: boolean;
|
|
34
|
+
error: PlatformError | null;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
type LabelSyncClient = {
|
|
38
|
+
json<T = unknown>(args: string[], options?: { cwd?: string; method?: string; input?: string }): any;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
function inLabels(labels: readonly string[]): string[] {
|
|
42
|
+
return [...new Set(labels.filter((label) => label.startsWith('in:')))].sort();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function mergeInLabels(labels: readonly string[], target: readonly string[]): string[] {
|
|
46
|
+
return [...new Set([
|
|
47
|
+
...labels.filter((label) => !label.startsWith('in:')),
|
|
48
|
+
...target
|
|
49
|
+
])].sort();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function invalidInLabelInput(code: string, message: string): { ok: false; error: InLabelValidationError } {
|
|
53
|
+
return { ok: false, error: { code, message, retryable: false } };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function validateInLabelMapping(value: unknown): { ok: true; value: Record<string, string[]> } | { ok: false; error: InLabelValidationError } {
|
|
57
|
+
if (value === undefined) return { ok: true, value: {} };
|
|
58
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
59
|
+
return invalidInLabelInput('IN_LABEL_SYNC_MAPPING_INVALID', 'labels.in must be an object of non-empty string arrays');
|
|
60
|
+
}
|
|
61
|
+
const normalized: Record<string, string[]> = {};
|
|
62
|
+
for (const [name, rawPrefixes] of Object.entries(value)) {
|
|
63
|
+
if (!name.trim() || !Array.isArray(rawPrefixes) || rawPrefixes.length === 0 || rawPrefixes.some((prefix) => typeof prefix !== 'string' || !prefix.trim())) {
|
|
64
|
+
return invalidInLabelInput('IN_LABEL_SYNC_MAPPING_INVALID', `labels.in entry '${name}' must be a non-empty string array`);
|
|
65
|
+
}
|
|
66
|
+
normalized[name] = rawPrefixes.map((prefix) => prefix.trim());
|
|
67
|
+
}
|
|
68
|
+
return { ok: true, value: normalized };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function repositoryLabelPages(value: unknown): unknown[][] | null {
|
|
72
|
+
if (!Array.isArray(value)) return null;
|
|
73
|
+
if (value.length === 0) return [];
|
|
74
|
+
if (value.every((page) => Array.isArray(page))) return value as unknown[][];
|
|
75
|
+
if (value.every((entry) => entry && typeof entry === 'object' && !Array.isArray(entry))) return [value];
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function validateRepositoryLabelPayload(value: unknown): { ok: true; value: string[] } | { ok: false; error: InLabelValidationError } {
|
|
80
|
+
const pages = repositoryLabelPages(value);
|
|
81
|
+
if (!pages) return invalidInLabelInput('IN_LABEL_SYNC_LABELS_INVALID', 'Repository labels response must be an array of pages');
|
|
82
|
+
const names: string[] = [];
|
|
83
|
+
for (const page of pages) {
|
|
84
|
+
for (const entry of page) {
|
|
85
|
+
if (!entry || typeof entry !== 'object' || Array.isArray(entry) || typeof (entry as { name?: unknown }).name !== 'string' || !(entry as { name: string }).name.trim()) {
|
|
86
|
+
return invalidInLabelInput('IN_LABEL_SYNC_LABELS_INVALID', 'Repository labels response contains an entry without a valid name');
|
|
87
|
+
}
|
|
88
|
+
names.push((entry as { name: string }).name);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return { ok: true, value: [...new Set(names)].sort() };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function validateRepositoryLabelSet(value: unknown): { ok: true; value: Set<string> } | { ok: false; error: InLabelValidationError } {
|
|
95
|
+
if (!(value instanceof Set) || [...value].some((label) => typeof label !== 'string' || !label.trim())) {
|
|
96
|
+
return invalidInLabelInput('IN_LABEL_SYNC_LABELS_INVALID', 'Repository labels must be a set of non-empty names');
|
|
97
|
+
}
|
|
98
|
+
return { ok: true, value: new Set([...value] as string[]) };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function planInLabelUpdate(input: {
|
|
102
|
+
changedFiles: readonly string[];
|
|
103
|
+
currentLabels: readonly string[];
|
|
104
|
+
mapping: Record<string, unknown>;
|
|
105
|
+
repositoryLabels: Set<string>;
|
|
106
|
+
}): InLabelPlan {
|
|
107
|
+
const current = inLabels(input.currentLabels);
|
|
108
|
+
const mapping = validateInLabelMapping(input.mapping);
|
|
109
|
+
const repositoryLabels = validateRepositoryLabelSet(input.repositoryLabels);
|
|
110
|
+
if (!mapping.ok) return { current, target: [...current], labels: [...input.currentLabels].sort(), changed: false, error: mapping.error };
|
|
111
|
+
if (!repositoryLabels.ok) return { current, target: [...current], labels: [...input.currentLabels].sort(), changed: false, error: repositoryLabels.error };
|
|
112
|
+
const target = computeInLabels([...input.changedFiles], mapping.value, repositoryLabels.value);
|
|
113
|
+
const labels = mergeInLabels(input.currentLabels, target);
|
|
114
|
+
return { current, target, labels, changed: labels.join('\0') !== [...input.currentLabels].sort().join('\0'), error: null };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function startsWithPrefix(label: string, prefix: LabelPrefix): boolean {
|
|
118
|
+
return (Array.isArray(prefix) ? prefix : [prefix]).some((value) => label.startsWith(value));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function labelDelta(current: readonly string[], target: readonly string[], prefix: LabelPrefix = 'in:'): LabelDelta {
|
|
122
|
+
const currentLabels = [...new Set(current)].sort();
|
|
123
|
+
const targetLabels = [...new Set(target.filter((label) => startsWithPrefix(label, prefix)))].sort();
|
|
124
|
+
return {
|
|
125
|
+
add: targetLabels.filter((label) => !currentLabels.includes(label)),
|
|
126
|
+
remove: currentLabels.filter((label) => startsWithPrefix(label, prefix) && !targetLabels.includes(label))
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function syncLabelDelta(
|
|
131
|
+
client: LabelSyncClient,
|
|
132
|
+
repository: string,
|
|
133
|
+
number: number,
|
|
134
|
+
cwd: string,
|
|
135
|
+
current: readonly string[],
|
|
136
|
+
target: readonly string[],
|
|
137
|
+
prefix: LabelPrefix = 'in:'
|
|
138
|
+
): LabelDeltaResult {
|
|
139
|
+
const delta = labelDelta(current, target, prefix);
|
|
140
|
+
let changed = false;
|
|
141
|
+
const failure = (error: PlatformError): LabelDeltaResult => {
|
|
142
|
+
if (changed || error.retryable) {
|
|
143
|
+
return {
|
|
144
|
+
status: 'blocked',
|
|
145
|
+
changed,
|
|
146
|
+
error: { ...error, code: 'IN_LABEL_SYNC_PARTIAL', message: `In-label synchronization is partial or unknown: ${error.message}` }
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
return { status: 'failed', changed: false, error };
|
|
150
|
+
};
|
|
151
|
+
for (const label of delta.remove) {
|
|
152
|
+
const removed = client.json<unknown>([
|
|
153
|
+
'api', `repos/${repository}/issues/${number}/labels/${encodeURIComponent(label)}`, '-X', 'DELETE'
|
|
154
|
+
], { cwd, method: 'DELETE' });
|
|
155
|
+
if (!removed.ok) return failure(removed.error);
|
|
156
|
+
changed = true;
|
|
157
|
+
}
|
|
158
|
+
for (const label of delta.add) {
|
|
159
|
+
const added = client.json<unknown>([
|
|
160
|
+
'api', `repos/${repository}/issues/${number}/labels`, '-X', 'POST', '--input', '-'
|
|
161
|
+
], { cwd, method: 'POST', input: JSON.stringify({ labels: [label] }) });
|
|
162
|
+
if (!added.ok) return failure(added.error);
|
|
163
|
+
changed = true;
|
|
164
|
+
}
|
|
165
|
+
return { status: changed ? 'applied' : 'no-op', changed, error: null };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function flattenPages(value: unknown): unknown[] {
|
|
169
|
+
if (!Array.isArray(value)) return [];
|
|
170
|
+
return value.flatMap((page) => Array.isArray(page) ? page : [page]);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function extractRepositoryLabelNames(value: unknown): string[] {
|
|
174
|
+
return flattenPages(value).flatMap((entry) => {
|
|
175
|
+
if (typeof entry === 'string') return [entry];
|
|
176
|
+
if (entry && typeof entry === 'object' && typeof (entry as { name?: unknown }).name === 'string') {
|
|
177
|
+
return [(entry as { name: string }).name];
|
|
178
|
+
}
|
|
179
|
+
return [];
|
|
180
|
+
}).filter(Boolean).sort();
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function extractPullRequestFileNames(value: unknown): string[] | null {
|
|
184
|
+
const entries = flattenPages(value);
|
|
185
|
+
if (!Array.isArray(value) || entries.some((entry) => !entry || typeof entry !== 'object')) return null;
|
|
186
|
+
const names = entries.map((entry) => (entry as { filename?: unknown }).filename);
|
|
187
|
+
if (names.some((name) => typeof name !== 'string' || !name.trim())) return null;
|
|
188
|
+
return [...new Set(names as string[])].sort();
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function resourceSnapshot(kind: InLabelResource['kind'], number: number, labels: readonly string[]): InLabelResource {
|
|
192
|
+
return { kind, number, labels: [...labels].sort() };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export {
|
|
196
|
+
extractPullRequestFileNames,
|
|
197
|
+
extractRepositoryLabelNames,
|
|
198
|
+
flattenPages,
|
|
199
|
+
inLabels,
|
|
200
|
+
labelDelta,
|
|
201
|
+
mergeInLabels,
|
|
202
|
+
planInLabelUpdate,
|
|
203
|
+
resourceSnapshot,
|
|
204
|
+
syncLabelDelta,
|
|
205
|
+
validateInLabelMapping,
|
|
206
|
+
validateRepositoryLabelPayload
|
|
207
|
+
};
|
|
208
|
+
export type { InLabelPlan, InLabelResource, InLabelValidationError, LabelDelta, LabelDeltaResult, LabelPrefix };
|
|
@@ -3,13 +3,21 @@ import path from 'node:path';
|
|
|
3
3
|
|
|
4
4
|
import { parseTaskFrontmatter } from '../task/frontmatter.ts';
|
|
5
5
|
import { resolveTaskRef } from '../task/resolve-ref.ts';
|
|
6
|
-
import {
|
|
7
|
-
import type {
|
|
8
|
-
import { resolvePlatformContext } from './context.ts';
|
|
6
|
+
import { resolvePlatformProviderContext } from './context.ts';
|
|
7
|
+
import type { PlatformClient } from './context.ts';
|
|
9
8
|
import { platformResult } from './types.ts';
|
|
10
9
|
import type { PlatformOperation, PlatformResult } from './types.ts';
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
import {
|
|
11
|
+
providerError,
|
|
12
|
+
providerOperationContext,
|
|
13
|
+
providerStatus,
|
|
14
|
+
providerResourceToken,
|
|
15
|
+
unsupportedProviderOperation
|
|
16
|
+
} from './provider-bridge.ts';
|
|
17
|
+
import { resourceIdentityNumber } from './resource-identity.ts';
|
|
18
|
+
import { taskIssueIdentity, taskIssueIdentityError } from './task-identities.ts';
|
|
19
|
+
|
|
20
|
+
type RemoteComment = { id: number | string; body: string; user?: { login?: string } };
|
|
13
21
|
type RenderedChunk = { marker: string; body: string; content: string; part: number; total: number };
|
|
14
22
|
type CommentKind = 'task' | 'artifact' | 'summary' | 'cancel';
|
|
15
23
|
type SyncOptions = {
|
|
@@ -19,9 +27,14 @@ type SyncOptions = {
|
|
|
19
27
|
body?: string;
|
|
20
28
|
cwd?: string;
|
|
21
29
|
backfill?: boolean;
|
|
22
|
-
client?:
|
|
30
|
+
client?: PlatformClient;
|
|
31
|
+
runtimeVersion?: string;
|
|
23
32
|
};
|
|
24
33
|
|
|
34
|
+
function providerCommentId(id: string, provider: { identity?: { comment?: string } }): number | string {
|
|
35
|
+
return provider.identity?.comment === 'number' && /^\d+$/.test(id) ? Number(id) : id;
|
|
36
|
+
}
|
|
37
|
+
|
|
25
38
|
const MARKERS = {
|
|
26
39
|
task: (taskId: string) => `<!-- sync-issue:${taskId}:task -->`,
|
|
27
40
|
artifact: (taskId: string, stem: string) => `<!-- sync-issue:${taskId}:${stem} -->`,
|
|
@@ -181,7 +194,7 @@ function flattenComments(value: unknown): RemoteComment[] {
|
|
|
181
194
|
if (!Array.isArray(value)) return [];
|
|
182
195
|
const flattened = value.flatMap((entry) => Array.isArray(entry) ? entry : [entry]);
|
|
183
196
|
return flattened.filter((entry): entry is RemoteComment =>
|
|
184
|
-
Boolean(entry && typeof entry === 'object' && typeof entry.id === 'number' && typeof entry.body === 'string')
|
|
197
|
+
Boolean(entry && typeof entry === 'object' && (typeof entry.id === 'number' || typeof entry.id === 'string') && typeof entry.body === 'string')
|
|
185
198
|
);
|
|
186
199
|
}
|
|
187
200
|
|
|
@@ -191,7 +204,7 @@ function issueNumberFromTask(content: string): number | null {
|
|
|
191
204
|
return Number.isInteger(number) && number > 0 ? number : null;
|
|
192
205
|
}
|
|
193
206
|
|
|
194
|
-
function listRemoteComments(client:
|
|
207
|
+
function listRemoteComments(client: PlatformClient, repo: string, issue: number, cwd: string) {
|
|
195
208
|
const result = client.json<unknown>([
|
|
196
209
|
'api', '--paginate', '--slurp', `repos/${repo}/issues/${issue}/comments?per_page=100`
|
|
197
210
|
], { cwd });
|
|
@@ -291,7 +304,7 @@ function validateRelatedMarkerSet(comments: RemoteComment[], prefix: string): {
|
|
|
291
304
|
}
|
|
292
305
|
|
|
293
306
|
function writeComment(
|
|
294
|
-
client:
|
|
307
|
+
client: PlatformClient,
|
|
295
308
|
repo: string,
|
|
296
309
|
issue: number,
|
|
297
310
|
cwd: string,
|
|
@@ -305,7 +318,7 @@ function writeComment(
|
|
|
305
318
|
);
|
|
306
319
|
}
|
|
307
320
|
|
|
308
|
-
function syncPlatformComment(taskRef: string, options: SyncOptions): PlatformResult {
|
|
321
|
+
async function syncPlatformComment(taskRef: string, options: SyncOptions): Promise<PlatformResult> {
|
|
309
322
|
const resolved = resolveTaskRef(taskRef, options.cwd ? { repoRoot: options.cwd } : {});
|
|
310
323
|
if (!resolved.ok) {
|
|
311
324
|
return platformResult('failed', {
|
|
@@ -313,16 +326,30 @@ function syncPlatformComment(taskRef: string, options: SyncOptions): PlatformRes
|
|
|
313
326
|
});
|
|
314
327
|
}
|
|
315
328
|
const taskContent = fs.readFileSync(resolved.taskMdPath, 'utf8');
|
|
316
|
-
|
|
317
|
-
|
|
329
|
+
let issueIdentityFromTask: ReturnType<typeof taskIssueIdentity>;
|
|
330
|
+
try { issueIdentityFromTask = taskIssueIdentity(parseTaskFrontmatter(taskContent), undefined, options.runtimeVersion); }
|
|
331
|
+
catch (error) { return platformResult('failed', { error: { ...taskIssueIdentityError(error), retryable: false } }); }
|
|
332
|
+
if (!issueIdentityFromTask) {
|
|
318
333
|
return platformResult('no-op', {
|
|
319
334
|
error: { code: 'ISSUE_NOT_LINKED', message: 'Task has no valid issue_number', retryable: false }
|
|
320
335
|
});
|
|
321
336
|
}
|
|
322
|
-
const
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
const
|
|
337
|
+
const loaded = await resolvePlatformProviderContext({ cwd: resolved.repoRoot, client: options.client });
|
|
338
|
+
const context = loaded.ok ? loaded.value.context : loaded.context;
|
|
339
|
+
if (!hasResolvedPlatformContext(context) || !loaded.ok) return context;
|
|
340
|
+
const issue = resourceIdentityNumber(issueIdentityFromTask);
|
|
341
|
+
const listed = loaded.value.provider.comments?.list
|
|
342
|
+
? await loaded.value.provider.comments.list({
|
|
343
|
+
context: providerOperationContext(loaded.value),
|
|
344
|
+
parent: issueIdentityFromTask
|
|
345
|
+
}).then((response) => response.ok
|
|
346
|
+
? { ok: true as const, value: response.value.map((comment) => ({
|
|
347
|
+
id: providerCommentId(comment.id, loaded.value.provider),
|
|
348
|
+
body: comment.body,
|
|
349
|
+
user: comment.author?.name ? { login: comment.author.name } : undefined
|
|
350
|
+
})) }
|
|
351
|
+
: response)
|
|
352
|
+
: unsupportedProviderOperation(loaded.value.provider, 'comments.list');
|
|
326
353
|
if (!listed.ok) {
|
|
327
354
|
return platformResult(listed.error.retryable ? 'blocked' : 'failed', {
|
|
328
355
|
...contextFields(context), resource: { kind: 'issue', number: issue }, error: listed.error
|
|
@@ -370,9 +397,8 @@ function syncPlatformComment(taskRef: string, options: SyncOptions): PlatformRes
|
|
|
370
397
|
});
|
|
371
398
|
}
|
|
372
399
|
|
|
373
|
-
const repo = context.platform.repository!;
|
|
374
400
|
const operations: PlatformOperation[] = [];
|
|
375
|
-
const ids: number
|
|
401
|
+
const ids: Array<number | string> = [];
|
|
376
402
|
for (const item of desired) {
|
|
377
403
|
const current = existing.find((comment) => normalizeCommentContent(comment.body).split('\n', 1)[0] === item.marker);
|
|
378
404
|
if (current && normalizeCommentContent(current.body) === normalizeCommentContent(item.body)) {
|
|
@@ -380,16 +406,18 @@ function syncPlatformComment(taskRef: string, options: SyncOptions): PlatformRes
|
|
|
380
406
|
operations.push({ name: `comment:${item.marker}`, status: 'no-op', reasonCode: null });
|
|
381
407
|
continue;
|
|
382
408
|
}
|
|
383
|
-
const written =
|
|
409
|
+
const written = loaded.value.provider.comments?.write
|
|
410
|
+
? await loaded.value.provider.comments.write({
|
|
411
|
+
context: providerOperationContext(loaded.value),
|
|
412
|
+
parent: issueIdentityFromTask,
|
|
413
|
+
body: item.body,
|
|
414
|
+
...(current ? { existingComment: { kind: 'id' as const, value: String(current.id) } } : {}),
|
|
415
|
+
mutation: { idempotencyKey: `comment:${resolved.taskId}:${item.marker}` }
|
|
416
|
+
})
|
|
417
|
+
: unsupportedProviderOperation(loaded.value.provider, 'comments.write');
|
|
384
418
|
if (!written.ok) {
|
|
385
419
|
if (!current && written.error.retryable) {
|
|
386
|
-
|
|
387
|
-
const found = reconciled.ok ? findMarkerComments(reconciled.value, item.marker) : [];
|
|
388
|
-
if (found.length === 1) {
|
|
389
|
-
ids.push(found[0]!.id);
|
|
390
|
-
operations.push({ name: `comment:${item.marker}`, status: 'applied', reasonCode: 'CREATE_RECONCILED' });
|
|
391
|
-
continue;
|
|
392
|
-
}
|
|
420
|
+
// A provider owns reconciliation because only it knows how to address the resource.
|
|
393
421
|
}
|
|
394
422
|
return platformResult(written.error.retryable ? 'blocked' : 'failed', {
|
|
395
423
|
...contextFields(context),
|
|
@@ -398,16 +426,21 @@ function syncPlatformComment(taskRef: string, options: SyncOptions): PlatformRes
|
|
|
398
426
|
error: written.error
|
|
399
427
|
});
|
|
400
428
|
}
|
|
401
|
-
ids.push(
|
|
429
|
+
ids.push(providerCommentId((written.value as { remoteId: string }).remoteId, loaded.value.provider));
|
|
402
430
|
operations.push({ name: `comment:${item.marker}`, status: 'applied', reasonCode: null });
|
|
403
431
|
}
|
|
404
432
|
|
|
405
433
|
const desiredMarkers = new Set(desired.map((item) => item.marker));
|
|
406
434
|
const stale = existing.filter((comment) => !desiredMarkers.has(normalizeCommentContent(comment.body).split('\n', 1)[0]!));
|
|
407
435
|
for (const comment of stale) {
|
|
408
|
-
const deleted =
|
|
409
|
-
|
|
410
|
-
|
|
436
|
+
const deleted = loaded.value.provider.comments?.delete
|
|
437
|
+
? await loaded.value.provider.comments.delete({
|
|
438
|
+
context: providerOperationContext(loaded.value),
|
|
439
|
+
parent: issueIdentityFromTask,
|
|
440
|
+
comment: { kind: 'id', value: String(comment.id) },
|
|
441
|
+
mutation: { idempotencyKey: `comment-delete:${resolved.taskId}:${comment.id}` }
|
|
442
|
+
})
|
|
443
|
+
: unsupportedProviderOperation(loaded.value.provider, 'comments.delete');
|
|
411
444
|
if (!deleted.ok) {
|
|
412
445
|
return platformResult(deleted.error.retryable ? 'blocked' : 'failed', {
|
|
413
446
|
...contextFields(context),
|
|
@@ -429,28 +462,45 @@ function syncPlatformComment(taskRef: string, options: SyncOptions): PlatformRes
|
|
|
429
462
|
});
|
|
430
463
|
}
|
|
431
464
|
|
|
432
|
-
function listPlatformComments(issue: number, cwd = process.cwd(), client
|
|
433
|
-
const
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
465
|
+
async function listPlatformComments(issue: string | number, cwd = process.cwd(), client?: PlatformClient): Promise<PlatformResult & { comments?: RemoteComment[] }> {
|
|
466
|
+
const loaded = await resolvePlatformProviderContext({ cwd, client });
|
|
467
|
+
const context = loaded.ok ? loaded.value.context : loaded.context;
|
|
468
|
+
if (!hasResolvedPlatformContext(context) || !loaded.ok) return context;
|
|
469
|
+
let identity;
|
|
470
|
+
try { identity = loaded.ok ? providerResourceToken(loaded.value.provider, 'issue', String(issue)) : null; }
|
|
471
|
+
catch (error) {
|
|
472
|
+
return platformResult('failed', { ...contextFields(context), error: { code: 'PLATFORM_IDENTITY_TOKEN_INVALID', message: error instanceof Error ? error.message : String(error), retryable: false } });
|
|
473
|
+
}
|
|
474
|
+
if (!loaded.ok || !identity) return context;
|
|
475
|
+
const listed = loaded.value.provider.comments?.list
|
|
476
|
+
? await loaded.value.provider.comments.list({ context: providerOperationContext(loaded.value), parent: identity }).then((response) => response.ok
|
|
477
|
+
? { ok: true as const, value: response.value.map((comment) => ({ id: comment.id, body: comment.body, user: comment.author?.name ? { login: comment.author.name } : undefined })) }
|
|
478
|
+
: response)
|
|
479
|
+
: unsupportedProviderOperation(loaded.value.provider, 'comments.list');
|
|
480
|
+
if (!listed.ok) {
|
|
481
|
+
return platformResult(providerStatus(listed.error), {
|
|
482
|
+
...contextFields(context), resource: { kind: 'issue', number: resourceIdentityNumber(identity), identity }, error: providerError(listed.error, 'PLATFORM_PROVIDER_OPERATION_FAILED')
|
|
439
483
|
});
|
|
440
484
|
}
|
|
441
|
-
return { ...platformResult('no-op', { ...contextFields(context), resource: { kind: 'issue', number:
|
|
485
|
+
return { ...platformResult('no-op', { ...contextFields(context), resource: { kind: 'issue', number: resourceIdentityNumber(identity), identity }, error: null }), comments: listed.value };
|
|
442
486
|
}
|
|
443
487
|
|
|
444
|
-
function checkPlatformCommentOwner(taskRef: string, options: { cwd?: string; client?:
|
|
488
|
+
async function checkPlatformCommentOwner(taskRef: string, options: { cwd?: string; client?: PlatformClient; runtimeVersion?: string } = {}): Promise<PlatformResult> {
|
|
445
489
|
const resolved = resolveTaskRef(taskRef, options.cwd ? { repoRoot: options.cwd } : {});
|
|
446
490
|
if (!resolved.ok) return platformResult('failed', { error: { code: resolved.code, message: resolved.message, retryable: false } });
|
|
447
491
|
const content = fs.readFileSync(resolved.taskMdPath, 'utf8');
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
const
|
|
492
|
+
let issueIdentity: ReturnType<typeof taskIssueIdentity>;
|
|
493
|
+
try { issueIdentity = taskIssueIdentity(parseTaskFrontmatter(content), undefined, options.runtimeVersion); }
|
|
494
|
+
catch (error) { return platformResult('failed', { error: { ...taskIssueIdentityError(error), retryable: false } }); }
|
|
495
|
+
if (!issueIdentity) return platformResult('no-op', { error: { code: 'ISSUE_NOT_LINKED', message: 'Task has no valid platform issue identity', retryable: false } });
|
|
496
|
+
const loaded = await resolvePlatformProviderContext({ cwd: resolved.repoRoot, client: options.client });
|
|
497
|
+
const context = loaded.ok ? loaded.value.context : loaded.context;
|
|
498
|
+
if (!hasResolvedPlatformContext(context) || !loaded.ok) return context;
|
|
499
|
+
const listed = loaded.value.provider.comments?.list
|
|
500
|
+
? await loaded.value.provider.comments.list({ context: providerOperationContext(loaded.value), parent: issueIdentity }).then((response) => response.ok
|
|
501
|
+
? { ok: true as const, value: response.value.map((comment) => ({ id: comment.id, body: comment.body, user: comment.author?.name ? { login: comment.author.name } : undefined })) }
|
|
502
|
+
: response)
|
|
503
|
+
: unsupportedProviderOperation(loaded.value.provider, 'comments.list');
|
|
454
504
|
if (!listed.ok) return platformResult(listed.error.retryable ? 'blocked' : 'failed', { ...contextFields(context), error: listed.error });
|
|
455
505
|
const matches = findMarkerComments(listed.value, MARKERS.task(resolved.taskId));
|
|
456
506
|
if (matches.length > 1) return platformResult('failed', { ...contextFields(context), error: { code: 'COMMENT_MARKER_CONFLICT', message: 'Multiple task comments use the registered marker', retryable: false } });
|
|
@@ -458,11 +508,11 @@ function checkPlatformCommentOwner(taskRef: string, options: { cwd?: string; cli
|
|
|
458
508
|
if (owner && owner !== context.platform.currentUser && !context.capabilities.triage) {
|
|
459
509
|
return platformResult('blocked', {
|
|
460
510
|
...contextFields(context),
|
|
461
|
-
resource: { kind: 'issue', number:
|
|
511
|
+
resource: { kind: 'issue', number: resourceIdentityNumber(issueIdentity), identity: issueIdentity },
|
|
462
512
|
error: { code: 'COMMENT_OWNER_CONFLICT', message: `Task comment is owned by '${owner}'`, retryable: false }
|
|
463
513
|
});
|
|
464
514
|
}
|
|
465
|
-
return platformResult('no-op', { ...contextFields(context), resource: { kind: 'issue', number:
|
|
515
|
+
return platformResult('no-op', { ...contextFields(context), resource: { kind: 'issue', number: resourceIdentityNumber(issueIdentity), identity: issueIdentity }, error: null });
|
|
466
516
|
}
|
|
467
517
|
|
|
468
518
|
export {
|