@fitlab-ai/agent-infra 0.12.0 → 0.12.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +0 -2
- package/README.zh-CN.md +0 -2
- package/dist/bin/internal-cli.js +2 -17
- package/dist/lib/agent-clients/adapter.js +0 -13
- package/dist/lib/agent-clients/adapters/antigravity-cli.js +0 -5
- package/dist/lib/agent-clients/adapters/claude-code-credentials.js +0 -17
- package/dist/lib/agent-clients/adapters/claude-code.js +0 -9
- package/dist/lib/agent-clients/adapters/codex.js +0 -52
- package/dist/lib/agent-clients/adapters/opencode.js +0 -5
- package/dist/lib/agent-clients/adapters/traecli.js +0 -5
- package/dist/lib/agent-clients/config.js +19 -54
- package/dist/lib/agent-clients/registry.js +1 -12
- package/dist/lib/agent-clients/schema.js +0 -18
- package/dist/lib/agent-clients/tokens.js +1 -2
- package/dist/lib/agent-clients/types.js +0 -1
- package/dist/lib/decide.js +2 -0
- package/dist/lib/git/worktree-identity.js +1 -9
- package/dist/lib/internal/agent-client.js +2 -62
- package/dist/lib/internal/cli-route-inventory.js +1 -5
- package/dist/lib/internal/manual-validation.js +29 -27
- package/dist/lib/internal/platform-comment.js +56 -7
- package/dist/lib/internal/task-create.js +1 -1
- package/dist/lib/internal/task-event.js +1 -8
- package/dist/lib/internal/task-finalization.js +0 -1
- package/dist/lib/internal/task-lifecycle.js +2 -2
- package/dist/lib/internal/task-operation-registry.js +4 -20
- package/dist/lib/internal/task-review.js +4 -4
- package/dist/lib/platform/comment-safety.js +1 -7
- package/dist/lib/platform/in-label-sync.js +1 -14
- package/dist/lib/platform/issue-comments.js +373 -282
- package/dist/lib/platform/issue-summary-content.js +112 -0
- package/dist/lib/platform/issues.js +1 -1
- package/dist/lib/platform/operation-coordinator.js +4 -2
- package/dist/lib/platform/pr-change-report.js +176 -30
- package/dist/lib/platform/pr-checks.js +2 -8
- package/dist/lib/platform/pr-summary-content.js +54 -0
- package/dist/lib/platform/pr-summary.js +318 -284
- package/dist/lib/platform/provider-validation.js +0 -20
- package/dist/lib/platform/pull-requests.js +73 -84
- package/dist/lib/platform/releases.js +1 -43
- package/dist/lib/platform/summary-content-pipeline.js +18 -0
- package/dist/lib/platform/verification-required.js +0 -2
- package/dist/lib/platform/verification-sync.js +1 -79
- package/dist/lib/process-data/normalize.js +1 -185
- package/dist/lib/process-data/privacy.js +1 -16
- package/dist/lib/process-data/sources.js +2 -279
- package/dist/lib/render.js +0 -12
- package/dist/lib/run/index.js +15 -10
- package/dist/lib/run/skills.js +0 -3
- package/dist/lib/sandbox/commands/create.js +40 -53
- package/dist/lib/sandbox/commands/list-running.js +0 -18
- package/dist/lib/sandbox/commands/start.js +7 -8
- package/dist/lib/sandbox/constants.js +0 -6
- package/dist/lib/sandbox/control/client.js +0 -1
- package/dist/lib/sandbox/control/container-identity.js +1 -16
- package/dist/lib/sandbox/control/executor.js +0 -4
- package/dist/lib/sandbox/control/identity-sentinel.js +0 -7
- package/dist/lib/sandbox/control/lifecycle.js +21 -37
- package/dist/lib/sandbox/control/native-file-lock.js +0 -3
- package/dist/lib/sandbox/control/server.js +18 -38
- package/dist/lib/sandbox/control/state.js +1 -9
- package/dist/lib/sandbox/dockerfile.js +0 -6
- package/dist/lib/sandbox/engine.js +0 -14
- package/dist/lib/sandbox/engines/docker-exact-discovery.js +1 -4
- package/dist/lib/sandbox/engines/wsl2-paths.js +0 -3
- package/dist/lib/sandbox/recovery.js +24 -26
- package/dist/lib/sandbox/removal.js +67 -518
- package/dist/lib/sandbox/task-resolver.js +0 -41
- package/dist/lib/sandbox/tools.js +1 -4
- package/dist/lib/sandbox/workspace-view.js +1 -2
- package/dist/lib/server/display.js +0 -6
- package/dist/lib/task/activity-intent.js +45 -40
- package/dist/lib/task/activity-log.js +9 -16
- package/dist/lib/task/artifact-command.js +1 -1
- package/dist/lib/task/artifact-lifecycle.js +61 -63
- package/dist/lib/task/artifact-name.js +0 -6
- package/dist/lib/task/artifact-operations.js +3 -3
- package/dist/lib/task/capabilities.js +4 -5
- package/dist/lib/task/commands/log.js +0 -6
- package/dist/lib/task/commands/status.js +17 -60
- package/dist/lib/task/commit-operation.js +28 -39
- package/dist/lib/task/completion-facts.js +56 -43
- package/dist/lib/task/control-authority.js +23 -256
- package/dist/lib/task/control-recovery.js +3 -5
- package/dist/lib/task/create.js +2 -4
- package/dist/lib/task/decision-intents.js +8 -9
- package/dist/lib/task/delivery-target.js +1 -40
- package/dist/lib/task/events.js +84 -107
- package/dist/lib/task/finalization.js +60 -15
- package/dist/lib/task/ledger-intents.js +1 -1
- package/dist/lib/task/lifecycle-execution.js +38 -33
- package/dist/lib/task/lifecycle.js +49 -41
- package/dist/lib/task/local-artifact-finalization.js +1 -4
- package/dist/lib/task/manual-validation-evidence.js +1 -37
- package/dist/lib/task/platform-operation-recovery.js +151 -108
- package/dist/lib/task/pr-delivery-fact.js +2 -5
- package/dist/lib/task/qualification-confirmation.js +5 -7
- package/dist/lib/task/resolve-ref.js +1 -25
- package/dist/lib/task/review-command.js +2 -2
- package/dist/lib/task/review-finalization.js +1 -21
- package/dist/lib/task/review-fingerprint.js +0 -4
- package/dist/lib/task/secure-fs.js +0 -56
- package/dist/lib/task/short-id.js +1 -39
- package/dist/lib/task/skill-scope.js +0 -1
- package/dist/lib/task/verification-engine.js +58 -282
- package/dist/lib/task/verification.js +6 -3
- package/dist/lib/task/write.js +11 -1
- package/lib/agent-clients/adapter.ts +0 -76
- package/lib/agent-clients/adapters/antigravity-cli.ts +0 -5
- package/lib/agent-clients/adapters/claude-code-credentials.ts +0 -19
- package/lib/agent-clients/adapters/claude-code.ts +0 -9
- package/lib/agent-clients/adapters/codex.ts +0 -56
- package/lib/agent-clients/adapters/opencode.ts +0 -5
- package/lib/agent-clients/adapters/traecli.ts +0 -5
- package/lib/agent-clients/config.ts +20 -61
- package/lib/agent-clients/registry.ts +1 -34
- package/lib/agent-clients/schema.ts +0 -18
- package/lib/agent-clients/tokens.ts +1 -2
- package/lib/agent-clients/types.ts +0 -15
- package/lib/decide.ts +1 -0
- package/lib/git/worktree-identity.ts +0 -10
- package/lib/internal/agent-client.ts +2 -70
- package/lib/internal/cli-route-inventory.ts +1 -4
- package/lib/internal/manual-validation.ts +28 -27
- package/lib/internal/platform-comment.ts +46 -4
- package/lib/internal/task-create.ts +0 -1
- package/lib/internal/task-event.ts +1 -5
- package/lib/internal/task-finalization.ts +0 -1
- package/lib/internal/task-lifecycle.ts +3 -5
- package/lib/internal/task-operation-registry.ts +3 -19
- package/lib/internal/task-review.ts +4 -4
- package/lib/platform/comment-safety.ts +0 -10
- package/lib/platform/github-provider.ts +0 -2
- package/lib/platform/in-label-sync.ts +0 -16
- package/lib/platform/issue-comments.ts +478 -301
- package/lib/platform/issue-summary-content.ts +132 -0
- package/lib/platform/issues.ts +0 -2
- package/lib/platform/operation-coordinator.ts +4 -2
- package/lib/platform/pr-change-report.ts +165 -25
- package/lib/platform/pr-checks.ts +1 -10
- package/lib/platform/pr-review.ts +1 -1
- package/lib/platform/pr-summary-content.ts +60 -0
- package/lib/platform/pr-summary.ts +364 -257
- package/lib/platform/provider-bridge.ts +0 -1
- package/lib/platform/provider-loader.ts +1 -5
- package/lib/platform/provider-validation.ts +1 -49
- package/lib/platform/pull-requests.ts +64 -99
- package/lib/platform/releases.ts +1 -40
- package/lib/platform/summary-content-pipeline.ts +23 -0
- package/lib/platform/verification-required.ts +0 -2
- package/lib/platform/verification-sync.ts +0 -94
- package/lib/process-data/normalize.ts +1 -186
- package/lib/process-data/privacy.ts +2 -19
- package/lib/process-data/sources.ts +3 -283
- package/lib/render.ts +0 -15
- package/lib/run/index.ts +13 -7
- package/lib/run/skills.ts +0 -4
- package/lib/sandbox/commands/create.ts +37 -74
- package/lib/sandbox/commands/list-running.ts +0 -19
- package/lib/sandbox/commands/start.ts +7 -10
- package/lib/sandbox/constants.ts +0 -8
- package/lib/sandbox/control/client.ts +0 -1
- package/lib/sandbox/control/container-identity.ts +1 -16
- package/lib/sandbox/control/executor.ts +1 -5
- package/lib/sandbox/control/identity-sentinel.ts +0 -8
- package/lib/sandbox/control/lifecycle.ts +18 -51
- package/lib/sandbox/control/native-file-lock.ts +0 -8
- package/lib/sandbox/control/protocol.ts +0 -2
- package/lib/sandbox/control/server.ts +13 -39
- package/lib/sandbox/control/state.ts +1 -10
- package/lib/sandbox/dockerfile.ts +0 -11
- package/lib/sandbox/engine.ts +0 -17
- package/lib/sandbox/engines/docker-exact-discovery.ts +1 -5
- package/lib/sandbox/engines/wsl2-paths.ts +0 -4
- package/lib/sandbox/recovery.ts +24 -26
- package/lib/sandbox/removal.ts +58 -687
- package/lib/sandbox/shell.ts +1 -1
- package/lib/sandbox/task-resolver.ts +0 -48
- package/lib/sandbox/tools.ts +1 -5
- package/lib/sandbox/workspace-view.ts +2 -4
- package/lib/server/display.ts +0 -14
- package/lib/server/process-control.ts +1 -1
- package/lib/task/activity-intent.ts +36 -44
- package/lib/task/activity-log.ts +9 -22
- package/lib/task/artifact-command.ts +1 -1
- package/lib/task/artifact-lifecycle.ts +58 -67
- package/lib/task/artifact-name.ts +0 -7
- package/lib/task/artifact-operations.ts +3 -3
- package/lib/task/capabilities.ts +5 -6
- package/lib/task/commands/log.ts +0 -7
- package/lib/task/commands/status.ts +14 -79
- package/lib/task/commit-operation.ts +29 -51
- package/lib/task/completion-facts.ts +45 -42
- package/lib/task/control-authority.ts +27 -317
- package/lib/task/control-recovery.ts +2 -5
- package/lib/task/create.ts +2 -5
- package/lib/task/decision-intents.ts +5 -5
- package/lib/task/delivery-target.ts +0 -40
- package/lib/task/events.ts +89 -123
- package/lib/task/finalization.ts +52 -15
- package/lib/task/ledger-intents.ts +1 -1
- package/lib/task/lifecycle-execution.ts +37 -89
- package/lib/task/lifecycle.ts +48 -44
- package/lib/task/local-artifact-finalization.ts +0 -7
- package/lib/task/manual-validation-evidence.ts +1 -47
- package/lib/task/platform-operation-recovery.ts +117 -104
- package/lib/task/pr-delivery-fact.ts +2 -11
- package/lib/task/qualification-confirmation.ts +6 -6
- package/lib/task/resolve-ref.ts +0 -23
- package/lib/task/review-command.ts +2 -2
- package/lib/task/review-finalization.ts +1 -27
- package/lib/task/review-fingerprint.ts +0 -8
- package/lib/task/secure-fs.ts +0 -42
- package/lib/task/short-id.ts +0 -43
- package/lib/task/skill-scope.ts +0 -1
- package/lib/task/verification-engine.ts +56 -287
- package/lib/task/verification.ts +5 -5
- package/lib/task/write.ts +12 -1
- package/package.json +2 -2
- package/templates/.agents/README.en.md +1 -3
- package/templates/.agents/README.zh-CN.md +0 -2
- package/templates/.agents/rules/README.en.md +0 -1
- package/templates/.agents/rules/README.zh-CN.md +0 -1
- package/templates/.agents/rules/commit-and-pr.en.md +1 -1
- package/templates/.agents/rules/commit-and-pr.zh-CN.md +1 -1
- package/templates/.agents/rules/no-mid-flow-questions.en.md +0 -10
- package/templates/.agents/rules/no-mid-flow-questions.zh-CN.md +0 -10
- package/templates/.agents/rules/pr-sync.en.md +4 -2
- package/templates/.agents/rules/pr-sync.zh-CN.md +6 -9
- package/templates/.agents/rules/task-management.en.md +3 -3
- package/templates/.agents/rules/task-management.zh-CN.md +3 -3
- package/templates/.agents/skills/analyze-task/SKILL.en.md +3 -3
- package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/cancel-task/SKILL.en.md +2 -2
- package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/code-task/SKILL.en.md +2 -2
- package/templates/.agents/skills/code-task/SKILL.zh-CN.md +4 -4
- package/templates/.agents/skills/commit/SKILL.en.md +2 -2
- package/templates/.agents/skills/commit/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/commit/reference/commit-orchestration.en.md +6 -6
- package/templates/.agents/skills/commit/reference/commit-orchestration.zh-CN.md +6 -6
- package/templates/.agents/skills/commit/reference/pr-summary-sync.en.md +1 -1
- package/templates/.agents/skills/commit/reference/pr-summary-sync.zh-CN.md +1 -1
- package/templates/.agents/skills/complete-manual-validation/reference/summary-update.en.md +2 -2
- package/templates/.agents/skills/complete-manual-validation/reference/summary-update.zh-CN.md +2 -2
- package/templates/.agents/skills/complete-task/SKILL.en.md +10 -25
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +11 -26
- package/templates/.agents/skills/create-pr/SKILL.en.md +1 -1
- package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/create-pr/reference/change-report.en.md +9 -2
- package/templates/.agents/skills/create-pr/reference/change-report.zh-CN.md +9 -2
- package/templates/.agents/skills/create-pr/reference/comment-publish.en.md +2 -2
- package/templates/.agents/skills/create-pr/reference/comment-publish.zh-CN.md +2 -2
- package/templates/.agents/skills/create-task/SKILL.en.md +0 -18
- package/templates/.agents/skills/create-task/SKILL.zh-CN.md +0 -18
- package/templates/.agents/skills/plan-task/SKILL.en.md +2 -2
- package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +2 -2
- 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 +5 -5
- package/templates/.agents/skills/review-code/SKILL.zh-CN.md +5 -5
- 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/review-pr/SKILL.en.md +4 -4
- package/templates/.agents/skills/review-pr/SKILL.zh-CN.md +4 -4
- package/templates/.agents/skills/run-manual-validation/SKILL.en.md +1 -1
- package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +3 -75
- package/templates/.agents/skills/watch-pr/SKILL.en.md +1 -1
- package/templates/.agents/skills/watch-pr/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.en.md +3 -3
- package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.zh-CN.md +3 -3
- package/templates/.agents/templates/task.en.md +1 -0
- package/templates/.agents/templates/task.zh-CN.md +1 -0
- package/templates/.claude/settings.json +0 -24
- package/templates/.codex/hooks.json +0 -50
- package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +0 -582
- package/dist/lib/agent-clients/adapters/codex-lifecycle/binding.js +0 -114
- package/dist/lib/agent-clients/adapters/codex-lifecycle/build-identity.js +0 -237
- package/dist/lib/agent-clients/adapters/codex-lifecycle/cli.js +0 -443
- package/dist/lib/agent-clients/adapters/codex-lifecycle/evidence.js +0 -236
- package/dist/lib/agent-clients/adapters/codex-lifecycle/recovery.js +0 -292
- package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +0 -267
- package/dist/lib/agent-clients/adapters/codex-orchestration.js +0 -466
- package/dist/lib/internal/task-orchestration.js +0 -71
- package/dist/lib/platform/verification.js +0 -6
- package/dist/lib/run/prompt.js +0 -2
- package/dist/lib/sandbox/engines/authority.js +0 -289
- package/dist/lib/task/delegation-receipts.js +0 -410
- package/dist/lib/task/orchestration.js +0 -1233
- package/dist/lib/task/verification-checks.js +0 -9
- package/dist/lib/task/workspace-snapshot.js +0 -121
- package/dist/lifecycle-build-manifest.json +0 -6
- package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +0 -688
- package/lib/agent-clients/adapters/codex-lifecycle/binding.ts +0 -147
- package/lib/agent-clients/adapters/codex-lifecycle/build-identity.ts +0 -349
- package/lib/agent-clients/adapters/codex-lifecycle/cli.ts +0 -506
- package/lib/agent-clients/adapters/codex-lifecycle/evidence.ts +0 -385
- package/lib/agent-clients/adapters/codex-lifecycle/manifest-files.json +0 -41
- package/lib/agent-clients/adapters/codex-lifecycle/recovery.ts +0 -418
- package/lib/agent-clients/adapters/codex-lifecycle/store.ts +0 -354
- package/lib/agent-clients/adapters/codex-orchestration.ts +0 -630
- package/lib/internal/task-orchestration.ts +0 -74
- package/lib/platform/verification.ts +0 -11
- package/lib/run/prompt.ts +0 -1
- package/lib/sandbox/engines/authority.ts +0 -365
- package/lib/task/delegation-receipts.ts +0 -547
- package/lib/task/orchestration.ts +0 -1651
- package/lib/task/verification-checks.ts +0 -10
- package/lib/task/workspace-snapshot.ts +0 -144
- package/templates/.agents/hooks/lifecycle-delegation.js +0 -140
- package/templates/.agents/rules/lifecycle-orchestration.en.md +0 -44
- package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +0 -44
- package/templates/.agents/skills/run-task/SKILL.en.md +0 -29
- package/templates/.agents/skills/run-task/SKILL.zh-CN.md +0 -29
- package/templates/.agents/skills/run-task/config/verify.en.json +0 -10
- package/templates/.agents/skills/run-task/config/verify.zh-CN.json +0 -10
- package/templates/.agents/skills/run-task/reference/host-validation.en.md +0 -24
- package/templates/.agents/skills/run-task/reference/host-validation.zh-CN.md +0 -24
- package/templates/.claude/agents/agent-infra-lifecycle-executor.en.md +0 -6
- package/templates/.claude/agents/agent-infra-lifecycle-executor.zh-CN.md +0 -6
- package/templates/.claude/agents/agent-infra-lifecycle-reviewer.en.md +0 -6
- package/templates/.claude/agents/agent-infra-lifecycle-reviewer.zh-CN.md +0 -6
- package/templates/.claude/commands/run-task.en.md +0 -8
- package/templates/.claude/commands/run-task.zh-CN.md +0 -8
- package/templates/.codex/agents/agent-infra-lifecycle-executor.en.toml +0 -3
- package/templates/.codex/agents/agent-infra-lifecycle-executor.zh-CN.toml +0 -3
- package/templates/.codex/agents/agent-infra-lifecycle-reviewer.en.toml +0 -3
- package/templates/.codex/agents/agent-infra-lifecycle-reviewer.zh-CN.toml +0 -3
- package/templates/.opencode/commands/run-task.en.md +0 -11
- package/templates/.opencode/commands/run-task.zh-CN.md +0 -11
|
@@ -5,11 +5,13 @@ import { createHash } from 'node:crypto';
|
|
|
5
5
|
import { parseTaskFrontmatter } from '../task/frontmatter.ts';
|
|
6
6
|
import { resolveTaskRef } from '../task/resolve-ref.ts';
|
|
7
7
|
import { coordinatePlatformWrite } from './operation-coordinator.ts';
|
|
8
|
+
import type { PlatformOperation as JournalOperation, RecordOperationInput } from '../task/platform-operation-journal.ts';
|
|
8
9
|
import { resolvePlatformProviderContext } from './context.ts';
|
|
9
|
-
import type { PlatformClient } from './context.ts';
|
|
10
|
+
import type { LoadedContext, PlatformClient } from './context.ts';
|
|
10
11
|
import { providerErrorRetryable } from './provider-validation.ts';
|
|
11
12
|
import { platformResult } from './types.ts';
|
|
12
13
|
import type { PlatformOperation, PlatformResult } from './types.ts';
|
|
14
|
+
import type { ResourceIdentity } from './resource-identity.ts';
|
|
13
15
|
import {
|
|
14
16
|
providerError,
|
|
15
17
|
providerOperationContext,
|
|
@@ -58,9 +60,39 @@ type SyncOptions = {
|
|
|
58
60
|
runtimeVersion?: string;
|
|
59
61
|
summaryAuthorization?: { sha256: string };
|
|
60
62
|
dependency?: 'deferred' | 'required';
|
|
61
|
-
|
|
63
|
+
queuedOperation?: JournalOperation;
|
|
62
64
|
taskProjection?: { content: string; sha256: string };
|
|
63
65
|
};
|
|
66
|
+
type CommentPlanStrategy = Readonly<{
|
|
67
|
+
taskId: string;
|
|
68
|
+
cwd: string;
|
|
69
|
+
agent: string;
|
|
70
|
+
parentKind: 'issue' | 'pull-request';
|
|
71
|
+
kind: CommentKind;
|
|
72
|
+
client?: PlatformClient;
|
|
73
|
+
resolved?: { loaded: LoadedContext; context: PlatformResult };
|
|
74
|
+
backfill?: boolean;
|
|
75
|
+
markerPrefix: string;
|
|
76
|
+
prepare: () => Promise<{ parent: ResourceIdentity; items: RenderedChunk[] }>;
|
|
77
|
+
validate?: () => Promise<{ code: string; message: string; retryable: boolean } | null>;
|
|
78
|
+
summaryPosition?: boolean;
|
|
79
|
+
authorizeReposition?: (comment: RemoteComment, currentUser: string | null) => { code: string; message: string; retryable: boolean } | null;
|
|
80
|
+
verifyReadback?: boolean;
|
|
81
|
+
dryRun?: boolean;
|
|
82
|
+
}>;
|
|
83
|
+
|
|
84
|
+
async function syncCommentPlan(
|
|
85
|
+
strategy: CommentPlanStrategy,
|
|
86
|
+
operation: Omit<RecordOperationInput, 'state' | 'lastCode'>,
|
|
87
|
+
queuedOperation?: JournalOperation
|
|
88
|
+
): Promise<PlatformResult> {
|
|
89
|
+
const execute = () => executeCommentPlan(strategy, strategy.client);
|
|
90
|
+
if (strategy.dryRun) return execute();
|
|
91
|
+
if (queuedOperation) return execute();
|
|
92
|
+
return coordinatePlatformWrite({ operation, agent: strategy.agent, client: strategy.client, execute,
|
|
93
|
+
block: (error) => platformResult('blocked', { error }),
|
|
94
|
+
persistenceFailure: (error) => platformResult('failed', { error }) });
|
|
95
|
+
}
|
|
64
96
|
|
|
65
97
|
function providerCommentId(id: string, provider: { identity?: { comment?: string } }): number | string {
|
|
66
98
|
return provider.identity?.comment === 'number' && /^\d+$/.test(id) ? Number(id) : id;
|
|
@@ -146,10 +178,6 @@ function renderTaskComment(content: string, taskId: string, _agent: string, lang
|
|
|
146
178
|
return renderTaskCommentResult(content, taskId, language).body;
|
|
147
179
|
}
|
|
148
180
|
|
|
149
|
-
function isTaskCommentTooLarge(content: string, taskId = 'TASK-UNKNOWN', _agent = 'codex'): boolean {
|
|
150
|
-
return renderTaskCommentResult(content, taskId).byteLength > COMMENT_BYTE_LIMIT;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
181
|
function taskCommentLanguage(repoRoot: string): string {
|
|
154
182
|
try {
|
|
155
183
|
const config = JSON.parse(fs.readFileSync(path.join(repoRoot, '.agents', '.airc.json'), 'utf8')) as { language?: unknown };
|
|
@@ -377,21 +405,6 @@ function findMarkerComments(comments: RemoteComment[], marker: string): RemoteCo
|
|
|
377
405
|
return comments.filter((comment) => normalizeCommentContent(String(comment.body || '')).split('\n', 1)[0] === marker);
|
|
378
406
|
}
|
|
379
407
|
|
|
380
|
-
function flattenComments(value: unknown): RemoteComment[] {
|
|
381
|
-
if (!Array.isArray(value)) return [];
|
|
382
|
-
const flattened = value.flatMap((entry) => Array.isArray(entry) ? entry : [entry]);
|
|
383
|
-
return flattened.filter((entry): entry is RemoteComment =>
|
|
384
|
-
Boolean(entry && typeof entry === 'object' && (typeof entry.id === 'number' || typeof entry.id === 'string') && typeof entry.body === 'string')
|
|
385
|
-
);
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
function listRemoteComments(client: PlatformClient, repo: string, issue: number, cwd: string) {
|
|
389
|
-
const result = client.json<unknown>([
|
|
390
|
-
'api', '--paginate', '--slurp', `repos/${repo}/issues/${issue}/comments?per_page=100`
|
|
391
|
-
], { cwd });
|
|
392
|
-
return result.ok ? { ok: true as const, value: flattenComments(result.value) } : result;
|
|
393
|
-
}
|
|
394
|
-
|
|
395
408
|
function contextFields(context: PlatformResult): Partial<PlatformResult> {
|
|
396
409
|
return {
|
|
397
410
|
platform: context.platform,
|
|
@@ -472,38 +485,370 @@ function relatedComments(comments: RemoteComment[], prefix: string): RemoteComme
|
|
|
472
485
|
});
|
|
473
486
|
}
|
|
474
487
|
|
|
475
|
-
function
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
488
|
+
async function listedComments(provider: any, loaded: any, parent: ResourceIdentity): Promise<any> {
|
|
489
|
+
return provider.comments?.list
|
|
490
|
+
? provider.comments.list({ context: providerOperationContext(loaded), parent }).then((response: any) => response.ok
|
|
491
|
+
? { ok: true as const, value: response.value.map((comment: any) => parentComment(comment, provider)) }
|
|
492
|
+
: { ...response, error: { ...response.error, retryable: isRetryable(response.error) } })
|
|
493
|
+
: unsupportedProviderOperation(provider, 'comments.list');
|
|
480
494
|
}
|
|
481
495
|
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
496
|
+
type CommentPlanSelection = Readonly<{
|
|
497
|
+
existing: RemoteComment[];
|
|
498
|
+
byMarker: Map<string, RemoteComment>;
|
|
499
|
+
reposition: boolean;
|
|
500
|
+
}>;
|
|
501
|
+
|
|
502
|
+
function markerSetError(strategy: CommentPlanStrategy, context: PlatformResult, resource: PlatformResult['resource'], existing: RemoteComment[]): PlatformResult | null {
|
|
503
|
+
if (validateRelatedMarkerSet(existing, strategy.markerPrefix).ok) return null;
|
|
504
|
+
return platformResult('failed', { ...contextFields(context), resource, error: {
|
|
505
|
+
code: strategy.parentKind === 'pull-request' ? 'PR_SUMMARY_MARKER_AMBIGUOUS' : 'COMMENT_MARKER_CONFLICT',
|
|
506
|
+
message: strategy.parentKind === 'pull-request' ? 'Multiple PR comments contain the summary marker' : 'Multiple comments use the same registered marker', retryable: false
|
|
507
|
+
} });
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
function taskCommentOwnerError(
|
|
511
|
+
strategy: CommentPlanStrategy,
|
|
512
|
+
context: PlatformResult,
|
|
513
|
+
resource: PlatformResult['resource'],
|
|
514
|
+
existing: RemoteComment[]
|
|
515
|
+
): PlatformResult | null {
|
|
516
|
+
if (strategy.kind !== 'task' || existing.length !== 1) return null;
|
|
517
|
+
const owner = existing[0]!.user?.login;
|
|
518
|
+
return owner && context.platform.currentUser && owner === context.platform.currentUser ? null : platformResult('blocked', {
|
|
519
|
+
...contextFields(context), resource, error: {
|
|
520
|
+
code: 'COMMENT_OWNER_CONFLICT', message: owner ? `Task comment is owned by '${owner}'` : 'Task comment owner is unavailable', retryable: false
|
|
521
|
+
}
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
async function strategyValidationError(
|
|
526
|
+
strategy: CommentPlanStrategy,
|
|
527
|
+
context: PlatformResult,
|
|
528
|
+
resource: PlatformResult['resource']
|
|
529
|
+
): Promise<PlatformResult | null> {
|
|
530
|
+
const error = strategy.validate ? await strategy.validate() : null;
|
|
531
|
+
if (!error) return null;
|
|
532
|
+
return platformResult(error.retryable ? 'blocked' : 'failed', { ...contextFields(context), resource, error });
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function artifactBackfillResult(
|
|
536
|
+
strategy: CommentPlanStrategy,
|
|
537
|
+
context: PlatformResult,
|
|
538
|
+
resource: PlatformResult['resource'],
|
|
539
|
+
existing: RemoteComment[],
|
|
540
|
+
desired: RenderedChunk[]
|
|
541
|
+
): PlatformResult | null {
|
|
542
|
+
if (!strategy.backfill || strategy.kind !== 'artifact' || existing.length === 0) return null;
|
|
543
|
+
const operations = existing.map((comment): PlatformOperation => ({
|
|
544
|
+
name: `comment:${normalizeCommentContent(comment.body).split('\n', 1)[0]}`,
|
|
545
|
+
status: 'no-op',
|
|
546
|
+
reasonCode: 'BACKFILL_ALREADY_PRESENT'
|
|
547
|
+
}));
|
|
548
|
+
return platformResult('no-op', {
|
|
549
|
+
...contextFields(context),
|
|
550
|
+
resource,
|
|
551
|
+
operations,
|
|
552
|
+
comment: {
|
|
553
|
+
kind: strategy.kind,
|
|
554
|
+
marker: desired[0]?.marker ?? strategy.markerPrefix,
|
|
555
|
+
ids: existing.map((item) => item.id),
|
|
556
|
+
parts: existing.length
|
|
557
|
+
},
|
|
558
|
+
error: null
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
function repositionAuthorizationError(
|
|
563
|
+
strategy: CommentPlanStrategy,
|
|
564
|
+
context: PlatformResult,
|
|
565
|
+
resource: PlatformResult['resource'],
|
|
566
|
+
comments: RemoteComment[],
|
|
567
|
+
byMarker: Map<string, RemoteComment>,
|
|
568
|
+
desired: RenderedChunk[]
|
|
569
|
+
): { error: PlatformResult | null; reposition: boolean } {
|
|
570
|
+
const marker = desired[0]?.marker;
|
|
571
|
+
const current = marker ? byMarker.get(marker) : undefined;
|
|
572
|
+
const position = strategy.summaryPosition && current
|
|
573
|
+
? issueSummaryPosition(strategy.taskId, comments, marker!)
|
|
574
|
+
: true;
|
|
575
|
+
if (position === null) return {
|
|
576
|
+
error: platformResult('blocked', { ...contextFields(context), resource, error: {
|
|
577
|
+
code: 'SUMMARY_POSITION_UNVERIFIED', message: 'Summary comment ordering cannot be proven from the provider snapshot', retryable: true
|
|
578
|
+
} }),
|
|
579
|
+
reposition: false
|
|
580
|
+
};
|
|
581
|
+
const reposition = Boolean(strategy.summaryPosition && current && !position);
|
|
582
|
+
if (!reposition || !current) return { error: null, reposition: false };
|
|
583
|
+
const error = strategy.authorizeReposition
|
|
584
|
+
? strategy.authorizeReposition(current, context.platform.currentUser ?? null)
|
|
585
|
+
: { code: 'SUMMARY_REPOSITION_UNAUTHORIZED', message: 'summary reposition requires current authorization and an owned summary comment', retryable: false };
|
|
586
|
+
return {
|
|
587
|
+
error: error ? platformResult(error.retryable ? 'blocked' : 'failed', { ...contextFields(context), resource, error }) : null,
|
|
588
|
+
reposition: true
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
async function inspectCommentPlan(
|
|
593
|
+
strategy: CommentPlanStrategy,
|
|
594
|
+
context: PlatformResult,
|
|
595
|
+
resource: PlatformResult['resource'],
|
|
596
|
+
comments: RemoteComment[],
|
|
597
|
+
desired: RenderedChunk[]
|
|
598
|
+
): Promise<{ ok: true; value: CommentPlanSelection; result?: PlatformResult } | { ok: false; result: PlatformResult }> {
|
|
599
|
+
const existing = relatedComments(comments, strategy.markerPrefix);
|
|
600
|
+
const markerError = markerSetError(strategy, context, resource, existing);
|
|
601
|
+
if (markerError) return { ok: false, result: markerError };
|
|
602
|
+
const ownerError = taskCommentOwnerError(strategy, context, resource, existing);
|
|
603
|
+
if (ownerError) return { ok: false, result: ownerError };
|
|
604
|
+
const validationError = await strategyValidationError(strategy, context, resource);
|
|
605
|
+
if (validationError) return { ok: false, result: validationError };
|
|
606
|
+
const backfill = artifactBackfillResult(strategy, context, resource, existing, desired);
|
|
607
|
+
if (backfill) return { ok: true, value: { existing, byMarker: new Map(), reposition: false }, result: backfill };
|
|
608
|
+
const byMarker = new Map(existing.map((comment) => [normalizeCommentContent(comment.body).split('\n', 1)[0]!, comment]));
|
|
609
|
+
const reposition = repositionAuthorizationError(strategy, context, resource, comments, byMarker, desired);
|
|
610
|
+
if (reposition.error) return { ok: false, result: reposition.error };
|
|
611
|
+
return { ok: true, value: { existing, byMarker, reposition: reposition.reposition } };
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
async function repositionSummaryComment(
|
|
615
|
+
strategy: CommentPlanStrategy,
|
|
616
|
+
loaded: LoadedContext,
|
|
617
|
+
context: PlatformResult,
|
|
618
|
+
parent: ResourceIdentity,
|
|
619
|
+
resource: PlatformResult['resource'],
|
|
620
|
+
current: RemoteComment | undefined,
|
|
621
|
+
operations: PlatformOperation[]
|
|
622
|
+
): Promise<PlatformResult | null> {
|
|
623
|
+
if (!current) return null;
|
|
624
|
+
const deleted = loaded.provider.comments?.delete
|
|
625
|
+
? await loaded.provider.comments.delete({ context: providerOperationContext(loaded), parent,
|
|
626
|
+
comment: providerResourceToken(loaded.provider, 'comment', String(current.id)),
|
|
627
|
+
mutation: { idempotencyKey: `comment-reposition-delete:${strategy.taskId}:${current.id}` } })
|
|
628
|
+
: unsupportedProviderOperation(loaded.provider, 'comments.delete');
|
|
629
|
+
if (!deleted.ok) return platformResult(isRetryable(deleted.error) ? 'blocked' : 'failed', {
|
|
630
|
+
...contextFields(context), resource, error: deleted.error
|
|
631
|
+
});
|
|
632
|
+
operations.push({
|
|
633
|
+
name: `comment:${current.id}`,
|
|
634
|
+
status: 'applied',
|
|
635
|
+
reasonCode: 'SUMMARY_REPOSITIONED'
|
|
636
|
+
});
|
|
637
|
+
return null;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
async function observeCommentWrite(
|
|
641
|
+
strategy: CommentPlanStrategy,
|
|
642
|
+
loaded: LoadedContext,
|
|
643
|
+
parent: ResourceIdentity,
|
|
644
|
+
item: RenderedChunk
|
|
645
|
+
): Promise<{ valid: true } | { valid: false; error: PlatformResult['error'] }> {
|
|
646
|
+
const after = await listedComments(loaded.provider, loaded, parent);
|
|
647
|
+
const comments = after.ok ? after.value as RemoteComment[] : [];
|
|
648
|
+
const found = comments.filter((comment) => normalizeCommentContent(comment.body).split('\n', 1)[0] === item.marker);
|
|
649
|
+
const positionOk = !strategy.summaryPosition || issueSummaryPosition(strategy.taskId, comments, item.marker) === true;
|
|
650
|
+
const validation = strategy.validate ? await strategy.validate() : null;
|
|
651
|
+
if (after.ok
|
|
652
|
+
&& found.length === 1
|
|
653
|
+
&& normalizeCommentContent(found[0]!.body) === normalizeCommentContent(item.body)
|
|
654
|
+
&& positionOk
|
|
655
|
+
&& !validation) return { valid: true };
|
|
656
|
+
const error = validation ?? {
|
|
657
|
+
code: strategy.kind === 'summary' || strategy.kind === 'cancel' ? 'SUMMARY_BODY_READBACK_FAILED' : 'COMMENT_READBACK_FAILED',
|
|
658
|
+
message: after.ok ? 'Remote comment did not match the requested body' : after.error.message,
|
|
659
|
+
retryable: after.ok || isRetryable(after.error)
|
|
660
|
+
};
|
|
661
|
+
return { valid: false, error };
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
async function verifyIssueSummaryReadback(
|
|
665
|
+
strategy: CommentPlanStrategy,
|
|
666
|
+
loaded: LoadedContext,
|
|
667
|
+
context: PlatformResult,
|
|
668
|
+
parent: ResourceIdentity,
|
|
669
|
+
resource: PlatformResult['resource'],
|
|
670
|
+
desired: RenderedChunk[],
|
|
671
|
+
operations: PlatformOperation[]
|
|
672
|
+
): Promise<PlatformResult | null> {
|
|
673
|
+
if (strategy.parentKind !== 'issue' || (strategy.kind !== 'summary' && strategy.kind !== 'cancel')) return null;
|
|
674
|
+
const after = await listedComments(loaded.provider, loaded, parent);
|
|
675
|
+
if (!after.ok) return platformResult(isRetryable(after.error) ? 'blocked' : 'failed', {
|
|
676
|
+
...contextFields(context), resource, operations, error: after.error
|
|
677
|
+
});
|
|
678
|
+
const item = desired[0];
|
|
679
|
+
const matching = item
|
|
680
|
+
? after.value.filter((comment: RemoteComment) => normalizeCommentContent(comment.body).split('\n', 1)[0] === item.marker)
|
|
681
|
+
: [];
|
|
682
|
+
if (!item || matching.length !== 1 || normalizeCommentContent(matching[0]!.body) !== normalizeCommentContent(item.body)) {
|
|
683
|
+
return platformResult('blocked', { ...contextFields(context), resource, operations, error: {
|
|
684
|
+
code: 'SUMMARY_BODY_READBACK_FAILED',
|
|
685
|
+
message: strategy.kind === 'cancel' ? 'Remote cancellation body did not match the rendered cancellation summary' : 'Remote summary body did not match the rendered delivery summary',
|
|
686
|
+
retryable: true
|
|
687
|
+
} });
|
|
488
688
|
}
|
|
489
|
-
|
|
490
|
-
|
|
689
|
+
if (strategy.summaryPosition && issueSummaryPosition(strategy.taskId, after.value, item.marker) !== true) {
|
|
690
|
+
return platformResult('blocked', { ...contextFields(context), resource, operations, error: {
|
|
691
|
+
code: 'SUMMARY_POSITION_UNVERIFIED', message: 'Summary comment is not provably last among task-managed comments', retryable: true
|
|
692
|
+
} });
|
|
693
|
+
}
|
|
694
|
+
return null;
|
|
491
695
|
}
|
|
492
696
|
|
|
493
|
-
async function
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
697
|
+
async function compensatePullRequestComment(
|
|
698
|
+
strategy: CommentPlanStrategy,
|
|
699
|
+
loaded: LoadedContext,
|
|
700
|
+
parent: ResourceIdentity,
|
|
701
|
+
current: RemoteComment | undefined,
|
|
702
|
+
id: number | string,
|
|
703
|
+
cause: PlatformResult['error']
|
|
704
|
+
): Promise<PlatformResult['error']> {
|
|
705
|
+
const compensation = current
|
|
706
|
+
? await loaded.provider.comments?.write?.({ context: providerOperationContext(loaded), parent, body: current.body,
|
|
707
|
+
existingComment: providerResourceToken(loaded.provider, 'comment', String(current.id)),
|
|
708
|
+
mutation: { idempotencyKey: `pr-summary-restore:${strategy.taskId}` } })
|
|
709
|
+
: await loaded.provider.comments?.delete?.({ context: providerOperationContext(loaded), parent,
|
|
710
|
+
comment: providerResourceToken(loaded.provider, 'comment', String(id)),
|
|
711
|
+
mutation: { idempotencyKey: `pr-summary-delete:${strategy.taskId}:${id}` } });
|
|
712
|
+
if (!compensation?.ok) return {
|
|
713
|
+
code: 'PR_SUMMARY_RACE_COMPENSATION_FAILED',
|
|
714
|
+
message: compensation?.error?.message ?? 'Provider does not support comment compensation',
|
|
715
|
+
retryable: true
|
|
716
|
+
};
|
|
717
|
+
if (cause?.code === 'PR_SUMMARY_HEAD_RACE') return cause;
|
|
718
|
+
return {
|
|
719
|
+
code: 'PR_SUMMARY_POSTWRITE_VERIFY_FAILED',
|
|
720
|
+
message: cause?.message ?? 'Post-write summary verification failed',
|
|
721
|
+
retryable: true
|
|
722
|
+
};
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
async function verifyCommentWrite(
|
|
726
|
+
strategy: CommentPlanStrategy,
|
|
727
|
+
loaded: LoadedContext,
|
|
728
|
+
parent: ResourceIdentity,
|
|
729
|
+
item: RenderedChunk,
|
|
730
|
+
current: RemoteComment | undefined,
|
|
731
|
+
id: number | string
|
|
732
|
+
): Promise<PlatformResult['error']> {
|
|
733
|
+
if (!strategy.verifyReadback || strategy.parentKind === 'issue') return null;
|
|
734
|
+
const observed = await observeCommentWrite(strategy, loaded, parent, item);
|
|
735
|
+
if (observed.valid) return null;
|
|
736
|
+
if (strategy.parentKind === 'pull-request') {
|
|
737
|
+
return compensatePullRequestComment(strategy, loaded, parent, current, id, observed.error);
|
|
738
|
+
}
|
|
739
|
+
return observed.error;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
async function writeDesiredComments(
|
|
743
|
+
strategy: CommentPlanStrategy,
|
|
744
|
+
loaded: LoadedContext,
|
|
745
|
+
context: PlatformResult,
|
|
746
|
+
parent: ResourceIdentity,
|
|
747
|
+
resource: PlatformResult['resource'],
|
|
748
|
+
desired: RenderedChunk[],
|
|
749
|
+
selection: CommentPlanSelection,
|
|
750
|
+
operations: PlatformOperation[]
|
|
751
|
+
): Promise<{ ok: true; ids: Array<number | string> } | { ok: false; result: PlatformResult }> {
|
|
752
|
+
const ids: Array<number | string> = [];
|
|
753
|
+
for (const item of desired) {
|
|
754
|
+
const current = selection.reposition && item === desired[0] ? undefined : selection.byMarker.get(item.marker);
|
|
755
|
+
if (current && normalizeCommentContent(current.body) === normalizeCommentContent(item.body)) {
|
|
756
|
+
ids.push(current.id);
|
|
757
|
+
operations.push({ name: `comment:${item.marker}`, status: 'no-op', reasonCode: null });
|
|
758
|
+
continue;
|
|
759
|
+
}
|
|
760
|
+
const written = loaded.provider.comments?.write
|
|
761
|
+
? await loaded.provider.comments.write({ context: providerOperationContext(loaded), parent, body: item.body,
|
|
762
|
+
...(current ? { existingComment: providerResourceToken(loaded.provider, 'comment', String(current.id)) } : {}),
|
|
763
|
+
mutation: { idempotencyKey: `comment:${strategy.taskId}:${item.marker}` } })
|
|
764
|
+
: unsupportedProviderOperation(loaded.provider, 'comments.write');
|
|
765
|
+
if (!written.ok) return { ok: false, result: platformResult(isRetryable(written.error) ? 'blocked' : 'failed', {
|
|
766
|
+
...contextFields(context), resource, operations, error: written.error
|
|
767
|
+
}) };
|
|
768
|
+
const id = providerCommentId(written.value.remoteId, loaded.provider);
|
|
769
|
+
ids.push(id);
|
|
770
|
+
operations.push({ name: `comment:${item.marker}`, status: 'applied', reasonCode: null });
|
|
771
|
+
const error = await verifyCommentWrite(strategy, loaded, parent, item, current, id);
|
|
772
|
+
if (error) return { ok: false, result: platformResult(error.retryable ? 'blocked' : 'failed', {
|
|
773
|
+
...contextFields(context), resource, operations, error
|
|
774
|
+
}) };
|
|
775
|
+
}
|
|
776
|
+
return { ok: true, ids };
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
async function deleteStaleComments(
|
|
780
|
+
strategy: CommentPlanStrategy,
|
|
781
|
+
loaded: LoadedContext,
|
|
782
|
+
context: PlatformResult,
|
|
783
|
+
parent: ResourceIdentity,
|
|
784
|
+
resource: PlatformResult['resource'],
|
|
785
|
+
existing: RemoteComment[],
|
|
786
|
+
desired: RenderedChunk[],
|
|
787
|
+
operations: PlatformOperation[]
|
|
788
|
+
): Promise<PlatformResult | null> {
|
|
789
|
+
const markers = new Set(desired.map((item) => item.marker));
|
|
790
|
+
for (const stale of existing.filter((comment) => !markers.has(normalizeCommentContent(comment.body).split('\n', 1)[0]!))) {
|
|
791
|
+
const deleted = loaded.provider.comments?.delete
|
|
792
|
+
? await loaded.provider.comments.delete({ context: providerOperationContext(loaded), parent,
|
|
793
|
+
comment: providerResourceToken(loaded.provider, 'comment', String(stale.id)),
|
|
794
|
+
mutation: { idempotencyKey: `comment-delete:${strategy.taskId}:${stale.id}` } })
|
|
795
|
+
: unsupportedProviderOperation(loaded.provider, 'comments.delete');
|
|
796
|
+
if (!deleted.ok) return platformResult(isRetryable(deleted.error) ? 'blocked' : 'failed', {
|
|
797
|
+
...contextFields(context), resource, operations, error: deleted.error
|
|
798
|
+
});
|
|
799
|
+
operations.push({ name: `comment:${stale.id}`, status: 'applied', reasonCode: 'STALE_CHUNK_DELETED' });
|
|
800
|
+
}
|
|
801
|
+
return null;
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
type PreparedCommentExecution = Readonly<{
|
|
805
|
+
loaded: LoadedContext;
|
|
806
|
+
context: PlatformResult;
|
|
807
|
+
parent: ResourceIdentity;
|
|
808
|
+
desired: RenderedChunk[];
|
|
809
|
+
resource: PlatformResult['resource'];
|
|
810
|
+
selection: CommentPlanSelection;
|
|
811
|
+
result?: PlatformResult;
|
|
812
|
+
}>;
|
|
813
|
+
|
|
814
|
+
async function prepareCommentExecution(
|
|
815
|
+
strategy: CommentPlanStrategy,
|
|
816
|
+
client?: PlatformClient
|
|
817
|
+
): Promise<{ ok: true; value: PreparedCommentExecution } | { ok: false; result: PlatformResult }> {
|
|
818
|
+
const resolved = strategy.resolved
|
|
819
|
+
? { ok: true as const, value: strategy.resolved.loaded }
|
|
820
|
+
: await resolvePlatformProviderContext({ cwd: strategy.cwd, client });
|
|
821
|
+
const loaded = resolved;
|
|
822
|
+
const context = strategy.resolved?.context ?? (loaded.ok ? loaded.value.context : loaded.context);
|
|
823
|
+
if (!loaded.ok || !hasResolvedPlatformContext(context)) return { ok: false, result: context };
|
|
824
|
+
let parent: ResourceIdentity;
|
|
825
|
+
let desired: RenderedChunk[];
|
|
826
|
+
try { ({ parent, items: desired } = await strategy.prepare()); }
|
|
827
|
+
catch (error) {
|
|
828
|
+
const value = error as { code?: string; message?: string };
|
|
829
|
+
return { ok: false, result: platformResult('failed', { ...contextFields(context), error: {
|
|
830
|
+
code: value.code || 'COMMENT_PAYLOAD_INVALID', message: value.message || String(error), retryable: false
|
|
831
|
+
} }) };
|
|
832
|
+
}
|
|
833
|
+
const resource = { kind: strategy.parentKind, number: resourceIdentityNumber(parent), identity: parent } as const;
|
|
834
|
+
const listed = await listedComments(loaded.value.provider, loaded.value, parent);
|
|
835
|
+
if (!listed.ok) return { ok: false, result: platformResult(isRetryable(listed.error) ? 'blocked' : 'failed', {
|
|
836
|
+
...contextFields(context), resource, error: listed.error
|
|
837
|
+
}) };
|
|
838
|
+
const selection = await inspectCommentPlan(strategy, context, resource, listed.value, desired);
|
|
839
|
+
if (!selection.ok) return selection;
|
|
840
|
+
return {
|
|
841
|
+
ok: true,
|
|
842
|
+
value: {
|
|
843
|
+
loaded: loaded.value,
|
|
844
|
+
context,
|
|
845
|
+
parent,
|
|
846
|
+
desired,
|
|
847
|
+
resource,
|
|
848
|
+
selection: selection.value,
|
|
849
|
+
result: selection.result
|
|
850
|
+
}
|
|
851
|
+
};
|
|
507
852
|
}
|
|
508
853
|
|
|
509
854
|
function validateRelatedMarkerSet(comments: RemoteComment[], prefix: string): { ok: boolean; code: string | null } {
|
|
@@ -525,268 +870,102 @@ function validateRelatedMarkerSet(comments: RemoteComment[], prefix: string): {
|
|
|
525
870
|
return complete ? { ok: true, code: null } : { ok: false, code: 'COMMENT_MARKER_CONFLICT' };
|
|
526
871
|
}
|
|
527
872
|
|
|
528
|
-
function
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
id?: number
|
|
535
|
-
) {
|
|
536
|
-
const endpoint = id ? `repos/${repo}/issues/comments/${id}` : `repos/${repo}/issues/${issue}/comments`;
|
|
537
|
-
return client.json<{ id?: number }>(
|
|
538
|
-
['api', endpoint, '-X', id ? 'PATCH' : 'POST', '--input', '-'],
|
|
539
|
-
{ cwd, method: id ? 'PATCH' : 'POST', input: JSON.stringify({ body }) }
|
|
540
|
-
);
|
|
873
|
+
function parentComment(comment: any, provider: any): RemoteComment {
|
|
874
|
+
return {
|
|
875
|
+
id: providerCommentId(comment.id, provider), body: comment.body,
|
|
876
|
+
user: comment.author?.name ? { login: comment.author.name } : undefined,
|
|
877
|
+
createdSequence: comment.createdSequence
|
|
878
|
+
};
|
|
541
879
|
}
|
|
542
880
|
|
|
543
|
-
|
|
544
|
-
const
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
}
|
|
550
|
-
const taskContent = fs.readFileSync(resolved.taskMdPath, 'utf8');
|
|
551
|
-
let issueIdentityFromTask: ReturnType<typeof taskIssueIdentity>;
|
|
552
|
-
try { issueIdentityFromTask = taskIssueIdentity(parseTaskFrontmatter(taskContent)); }
|
|
553
|
-
catch (error) { return platformResult('failed', { error: { ...taskIssueIdentityError(error), retryable: false } }); }
|
|
554
|
-
if (!issueIdentityFromTask) {
|
|
555
|
-
return platformResult('no-op', {
|
|
556
|
-
error: { code: 'ISSUE_NOT_LINKED', message: 'Task has no valid platform issue identity', retryable: false }
|
|
557
|
-
});
|
|
558
|
-
}
|
|
559
|
-
let desired: RenderedChunk[];
|
|
560
|
-
try {
|
|
561
|
-
const projectedContent = options.kind === 'task' && options.taskProjection ? options.taskProjection.content : taskContent;
|
|
562
|
-
if (options.kind === 'task' && options.taskProjection
|
|
563
|
-
&& createHash('sha256').update(projectedContent).digest('hex') !== options.taskProjection.sha256) {
|
|
564
|
-
throw Object.assign(new Error('finalization task projection digest does not match content'), { code: 'FINALIZATION_PROJECTION_INVALID' });
|
|
565
|
-
}
|
|
566
|
-
desired = expectedComments(resolved.taskId, projectedContent, resolved.taskDir, resolved.repoRoot, options);
|
|
567
|
-
} catch (error) {
|
|
568
|
-
return platformResult('failed', {
|
|
569
|
-
resource: { kind: 'issue', number: resourceIdentityNumber(issueIdentityFromTask) },
|
|
570
|
-
error: { code: 'COMMENT_PAYLOAD_INVALID', message: error instanceof Error ? error.message : String(error), retryable: false }
|
|
571
|
-
});
|
|
572
|
-
}
|
|
573
|
-
if (options.kind === 'task' && Buffer.byteLength(desired[0]!.body, 'utf8') > COMMENT_BYTE_LIMIT) {
|
|
574
|
-
return platformResult('failed', {
|
|
575
|
-
resource: { kind: 'issue', number: resourceIdentityNumber(issueIdentityFromTask), identity: issueIdentityFromTask },
|
|
576
|
-
operations: [{ name: `comment:${MARKERS.task(resolved.taskId)}`, status: 'skipped', reasonCode: 'COMMENT_PAYLOAD_TOO_LARGE' }],
|
|
577
|
-
error: { code: 'COMMENT_PAYLOAD_TOO_LARGE', message: 'Projected task comment exceeds the platform byte limit', retryable: false }
|
|
578
|
-
});
|
|
579
|
-
}
|
|
580
|
-
const loaded = await resolvePlatformProviderContext({ cwd: resolved.repoRoot, client: options.client });
|
|
581
|
-
const context = loaded.ok ? loaded.value.context : loaded.context;
|
|
582
|
-
if (!hasResolvedPlatformContext(context) || !loaded.ok) return context;
|
|
583
|
-
const issue = resourceIdentityNumber(issueIdentityFromTask);
|
|
584
|
-
const listed = await listedComments(loaded.value.provider, loaded.value, issueIdentityFromTask);
|
|
585
|
-
if (!listed.ok) {
|
|
586
|
-
return platformResult(isRetryable(listed.error) ? 'blocked' : 'failed', {
|
|
587
|
-
...contextFields(context), resource: { kind: 'issue', number: issue }, error: listed.error
|
|
588
|
-
});
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
const existing = relatedComments(listed.value, markerPrefix(resolved.taskId, options));
|
|
592
|
-
if (!validateRelatedMarkerSet(existing, markerPrefix(resolved.taskId, options)).ok) {
|
|
593
|
-
return platformResult('failed', {
|
|
594
|
-
...contextFields(context),
|
|
595
|
-
resource: { kind: 'issue', number: issue },
|
|
596
|
-
error: { code: 'COMMENT_MARKER_CONFLICT', message: 'Multiple comments use the same registered marker', retryable: false }
|
|
597
|
-
});
|
|
598
|
-
}
|
|
599
|
-
|
|
600
|
-
if (options.kind === 'task' && existing.length === 1) {
|
|
601
|
-
const owner = existing[0]!.user?.login;
|
|
602
|
-
if (!owner || !context.platform.currentUser || owner !== context.platform.currentUser) {
|
|
603
|
-
return platformResult('blocked', {
|
|
604
|
-
...contextFields(context),
|
|
605
|
-
resource: { kind: 'issue', number: issue, identity: issueIdentityFromTask },
|
|
606
|
-
error: {
|
|
607
|
-
code: 'COMMENT_OWNER_CONFLICT',
|
|
608
|
-
message: owner ? `Task comment is owned by '${owner}'` : 'Task comment owner is unavailable',
|
|
609
|
-
retryable: false
|
|
610
|
-
}
|
|
611
|
-
});
|
|
612
|
-
}
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
// Backfill only supplies missing artifact comments; valid existing marker sets stay untouched.
|
|
616
|
-
if (options.kind === 'artifact' && options.backfill && existing.length > 0) {
|
|
617
|
-
const operations = existing.map((comment): PlatformOperation => ({
|
|
618
|
-
name: `comment:${normalizeCommentContent(comment.body).split('\n', 1)[0]}`,
|
|
619
|
-
status: 'no-op',
|
|
620
|
-
reasonCode: 'BACKFILL_ALREADY_PRESENT'
|
|
621
|
-
}));
|
|
622
|
-
return platformResult('no-op', {
|
|
623
|
-
...contextFields(context),
|
|
624
|
-
changed: false,
|
|
625
|
-
resource: { kind: 'issue', number: issue },
|
|
626
|
-
operations,
|
|
627
|
-
comment: {
|
|
628
|
-
kind: options.kind,
|
|
629
|
-
marker: desired[0]!.marker,
|
|
630
|
-
ids: existing.map((comment) => comment.id),
|
|
631
|
-
parts: existing.length
|
|
632
|
-
},
|
|
633
|
-
error: null
|
|
634
|
-
});
|
|
635
|
-
}
|
|
881
|
+
function issueSummaryPosition(taskId: string, comments: readonly RemoteComment[], marker: string): boolean | null {
|
|
882
|
+
const managed = comments.filter((comment) => comment.body.split('\n', 1)[0]?.startsWith(`<!-- sync-issue:${taskId}:`));
|
|
883
|
+
const summary = managed.filter((comment) => comment.body.split('\n', 1)[0] === marker);
|
|
884
|
+
if (summary.length !== 1 || managed.some((comment) => !Number.isSafeInteger(comment.createdSequence) || comment.createdSequence === null)) return null;
|
|
885
|
+
return managed.every((comment) => summary[0]!.createdSequence! >= comment.createdSequence!);
|
|
886
|
+
}
|
|
636
887
|
|
|
888
|
+
async function executeCommentPlan(
|
|
889
|
+
strategy: CommentPlanStrategy,
|
|
890
|
+
client?: PlatformClient
|
|
891
|
+
): Promise<PlatformResult> {
|
|
892
|
+
const prepared = await prepareCommentExecution(strategy, client);
|
|
893
|
+
if (!prepared.ok) return prepared.result;
|
|
894
|
+
const { loaded, context, parent, desired, resource, selection, result } = prepared.value;
|
|
895
|
+
if (result) return result;
|
|
896
|
+
if (strategy.dryRun) return platformResult('planned', {
|
|
897
|
+
...contextFields(context), resource,
|
|
898
|
+
operations: desired.map((item) => ({ name: `comment:${item.marker}`, status: 'planned', reasonCode: null })), error: null
|
|
899
|
+
});
|
|
637
900
|
const operations: PlatformOperation[] = [];
|
|
638
|
-
const
|
|
639
|
-
|
|
640
|
-
const
|
|
641
|
-
if (
|
|
642
|
-
ids.push(current.id);
|
|
643
|
-
operations.push({ name: `comment:${item.marker}`, status: 'no-op', reasonCode: null });
|
|
644
|
-
continue;
|
|
645
|
-
}
|
|
646
|
-
const written = loaded.value.provider.comments?.write
|
|
647
|
-
? await loaded.value.provider.comments.write({
|
|
648
|
-
context: providerOperationContext(loaded.value),
|
|
649
|
-
parent: issueIdentityFromTask,
|
|
650
|
-
body: item.body,
|
|
651
|
-
...(current ? { existingComment: { kind: 'id' as const, value: String(current.id) } } : {}),
|
|
652
|
-
mutation: { idempotencyKey: `comment:${resolved.taskId}:${item.marker}` }
|
|
653
|
-
})
|
|
654
|
-
: unsupportedProviderOperation(loaded.value.provider, 'comments.write');
|
|
655
|
-
if (!written.ok) {
|
|
656
|
-
if (!current && isRetryable(written.error)) {
|
|
657
|
-
// A provider owns reconciliation because only it knows how to address the resource.
|
|
658
|
-
}
|
|
659
|
-
return platformResult(isRetryable(written.error) ? 'blocked' : 'failed', {
|
|
660
|
-
...contextFields(context),
|
|
661
|
-
resource: { kind: 'issue', number: issue },
|
|
662
|
-
operations,
|
|
663
|
-
error: written.error
|
|
664
|
-
});
|
|
665
|
-
}
|
|
666
|
-
ids.push(providerCommentId((written.value as { remoteId: string }).remoteId, loaded.value.provider));
|
|
667
|
-
operations.push({ name: `comment:${item.marker}`, status: 'applied', reasonCode: null });
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
const desiredMarkers = new Set(desired.map((item) => item.marker));
|
|
671
|
-
const stale = existing.filter((comment) => !desiredMarkers.has(normalizeCommentContent(comment.body).split('\n', 1)[0]!));
|
|
672
|
-
for (const comment of stale) {
|
|
673
|
-
const deleted = loaded.value.provider.comments?.delete
|
|
674
|
-
? await loaded.value.provider.comments.delete({
|
|
675
|
-
context: providerOperationContext(loaded.value),
|
|
676
|
-
parent: issueIdentityFromTask,
|
|
677
|
-
comment: { kind: 'id', value: String(comment.id) },
|
|
678
|
-
mutation: { idempotencyKey: `comment-delete:${resolved.taskId}:${comment.id}` }
|
|
679
|
-
})
|
|
680
|
-
: unsupportedProviderOperation(loaded.value.provider, 'comments.delete');
|
|
681
|
-
if (!deleted.ok) {
|
|
682
|
-
return platformResult(isRetryable(deleted.error) ? 'blocked' : 'failed', {
|
|
683
|
-
...contextFields(context),
|
|
684
|
-
resource: { kind: 'issue', number: issue },
|
|
685
|
-
operations,
|
|
686
|
-
error: deleted.error
|
|
687
|
-
});
|
|
688
|
-
}
|
|
689
|
-
operations.push({ name: `comment:${comment.id}`, status: 'applied', reasonCode: 'STALE_CHUNK_DELETED' });
|
|
690
|
-
}
|
|
691
|
-
if (options.kind === 'summary') {
|
|
692
|
-
const refreshed = await listedComments(loaded.value.provider, loaded.value, issueIdentityFromTask);
|
|
693
|
-
if (!refreshed.ok) {
|
|
694
|
-
return platformResult(isRetryable(refreshed.error) ? 'blocked' : 'failed', {
|
|
695
|
-
...contextFields(context), resource: { kind: 'issue', number: issue }, operations, error: refreshed.error
|
|
696
|
-
});
|
|
697
|
-
}
|
|
698
|
-
const position = summaryPosition(refreshed.value, resolved.taskId);
|
|
699
|
-
if (!position.ok && !position.known) {
|
|
700
|
-
return platformResult('blocked', {
|
|
701
|
-
...contextFields(context), resource: { kind: 'issue', number: issue }, operations,
|
|
702
|
-
error: { code: 'SUMMARY_POSITION_UNVERIFIED', message: 'Summary comment ordering cannot be proven from the provider snapshot', retryable: true }
|
|
703
|
-
});
|
|
704
|
-
}
|
|
705
|
-
if (!position.ok && position.summary) {
|
|
706
|
-
const owner = position.summary.user?.login;
|
|
707
|
-
if (!options.summaryAuthorization || options.summaryAuthorization.sha256 !== createHash('sha256').update(options.body ?? '').digest('hex')
|
|
708
|
-
|| !owner || owner !== context.platform.currentUser) {
|
|
709
|
-
return platformResult('failed', {
|
|
710
|
-
...contextFields(context), resource: { kind: 'issue', number: issue }, operations,
|
|
711
|
-
error: { code: 'SUMMARY_REPOSITION_UNAUTHORIZED', message: 'summary reposition requires current finalization authorization and an owned durable summary', retryable: false }
|
|
712
|
-
});
|
|
713
|
-
}
|
|
714
|
-
const deleted = loaded.value.provider.comments?.delete
|
|
715
|
-
? await loaded.value.provider.comments.delete({
|
|
716
|
-
context: providerOperationContext(loaded.value), parent: issueIdentityFromTask,
|
|
717
|
-
comment: { kind: 'id', value: String(position.summary.id) },
|
|
718
|
-
mutation: { idempotencyKey: `summary-reposition-delete:${resolved.taskId}:${position.summary.id}` }
|
|
719
|
-
})
|
|
720
|
-
: unsupportedProviderOperation(loaded.value.provider, 'comments.delete');
|
|
721
|
-
if (!deleted.ok) {
|
|
722
|
-
return platformResult(isRetryable(deleted.error) ? 'blocked' : 'failed', {
|
|
723
|
-
...contextFields(context), resource: { kind: 'issue', number: issue }, operations, error: deleted.error
|
|
724
|
-
});
|
|
725
|
-
}
|
|
726
|
-
const written = loaded.value.provider.comments?.write
|
|
727
|
-
? await loaded.value.provider.comments.write({
|
|
728
|
-
context: providerOperationContext(loaded.value), parent: issueIdentityFromTask, body: desired[0]!.body,
|
|
729
|
-
mutation: { idempotencyKey: `summary-reposition-write:${resolved.taskId}` }
|
|
730
|
-
})
|
|
731
|
-
: unsupportedProviderOperation(loaded.value.provider, 'comments.write');
|
|
732
|
-
if (!written.ok) {
|
|
733
|
-
return platformResult(isRetryable(written.error) ? 'blocked' : 'failed', {
|
|
734
|
-
...contextFields(context), resource: { kind: 'issue', number: issue }, operations, error: written.error
|
|
735
|
-
});
|
|
736
|
-
}
|
|
737
|
-
ids.splice(0, ids.length, providerCommentId((written.value as { remoteId: string }).remoteId, loaded.value.provider));
|
|
738
|
-
operations.push({ name: `comment:${desired[0]!.marker}`, status: 'applied', reasonCode: 'SUMMARY_REPOSITIONED' });
|
|
739
|
-
const reconciled = await listedComments(loaded.value.provider, loaded.value, issueIdentityFromTask);
|
|
740
|
-
if (!reconciled.ok || !summaryPosition(reconciled.value, resolved.taskId).ok) {
|
|
741
|
-
const error = reconciled.ok
|
|
742
|
-
? { code: 'SUMMARY_POSITION_UNVERIFIED', message: 'Summary comment is not provably last among task-managed comments', retryable: true }
|
|
743
|
-
: reconciled.error;
|
|
744
|
-
return platformResult(isRetryable(error) ? 'blocked' : 'failed', {
|
|
745
|
-
...contextFields(context), resource: { kind: 'issue', number: issue }, operations, error
|
|
746
|
-
});
|
|
747
|
-
}
|
|
748
|
-
}
|
|
901
|
+
const currentSummary = desired[0] ? selection.byMarker.get(desired[0].marker) : undefined;
|
|
902
|
+
if (selection.reposition) {
|
|
903
|
+
const repositioned = await repositionSummaryComment(strategy, loaded, context, parent, resource, currentSummary, operations);
|
|
904
|
+
if (repositioned) return repositioned;
|
|
749
905
|
}
|
|
906
|
+
const written = await writeDesiredComments(strategy, loaded, context, parent, resource, desired, selection, operations);
|
|
907
|
+
if (!written.ok) return written.result;
|
|
908
|
+
const deleted = await deleteStaleComments(strategy, loaded, context, parent, resource, selection.existing, desired, operations);
|
|
909
|
+
if (deleted) return deleted;
|
|
910
|
+
const readback = await verifyIssueSummaryReadback(strategy, loaded, context, parent, resource, desired, operations);
|
|
911
|
+
if (readback) return readback;
|
|
750
912
|
const changed = operations.some((operation) => operation.status === 'applied');
|
|
751
|
-
|
|
752
|
-
...contextFields(context),
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
913
|
+
return platformResult(changed ? 'applied' : 'no-op', {
|
|
914
|
+
...contextFields(context), changed, resource, operations,
|
|
915
|
+
comment: {
|
|
916
|
+
kind: strategy.kind,
|
|
917
|
+
marker: desired[0]?.marker ?? strategy.markerPrefix,
|
|
918
|
+
ids: written.ids,
|
|
919
|
+
parts: desired.length
|
|
920
|
+
},
|
|
757
921
|
error: null
|
|
758
922
|
});
|
|
759
|
-
return result;
|
|
760
923
|
}
|
|
761
924
|
|
|
762
|
-
|
|
925
|
+
function syncPlatformComment(taskRef: string, options: SyncOptions): Promise<PlatformResult> {
|
|
763
926
|
const resolved = resolveTaskRef(taskRef, options.cwd ? { repoRoot: options.cwd } : {});
|
|
764
|
-
if (!resolved.ok) return
|
|
765
|
-
const
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
927
|
+
if (!resolved.ok) return Promise.resolve(platformResult('failed', { error: { code: resolved.code, message: resolved.message, retryable: false } }));
|
|
928
|
+
const readStrategy = (): CommentPlanStrategy => {
|
|
929
|
+
const prefix = markerPrefix(resolved.taskId, options);
|
|
930
|
+
return {
|
|
931
|
+
taskId: resolved.taskId, cwd: resolved.repoRoot, agent: options.agent, client: options.client,
|
|
932
|
+
parentKind: 'issue', kind: options.kind, markerPrefix: prefix,
|
|
933
|
+
backfill: options.backfill,
|
|
934
|
+
prepare: async () => {
|
|
935
|
+
const taskContent = fs.readFileSync(resolved.taskMdPath, 'utf8');
|
|
936
|
+
const parent = taskIssueIdentity(parseTaskFrontmatter(taskContent));
|
|
937
|
+
if (!parent) throw Object.assign(new Error('Task has no valid platform issue identity'), { code: 'ISSUE_NOT_LINKED' });
|
|
938
|
+
const source = options.kind === 'task' && options.taskProjection ? options.taskProjection.content : taskContent;
|
|
939
|
+
if (options.kind === 'task' && options.taskProjection && createHash('sha256').update(source).digest('hex') !== options.taskProjection.sha256) {
|
|
940
|
+
throw Object.assign(new Error('finalization task projection digest does not match content'), { code: 'FINALIZATION_PROJECTION_INVALID' });
|
|
941
|
+
}
|
|
942
|
+
const items = expectedComments(resolved.taskId, source, resolved.taskDir, resolved.repoRoot, options);
|
|
943
|
+
if (options.kind === 'task' && Buffer.byteLength(items[0]!.body, 'utf8') > COMMENT_BYTE_LIMIT) throw Object.assign(new Error('Projected task comment exceeds the platform byte limit'), { code: 'COMMENT_PAYLOAD_TOO_LARGE' });
|
|
944
|
+
return { parent, items };
|
|
945
|
+
},
|
|
946
|
+
summaryPosition: options.kind === 'summary',
|
|
947
|
+
verifyReadback: options.kind === 'summary' || options.kind === 'cancel',
|
|
948
|
+
authorizeReposition: (comment, currentUser) => options.summaryAuthorization
|
|
949
|
+
&& options.summaryAuthorization.sha256 === createHash('sha256').update(options.body ?? '').digest('hex')
|
|
950
|
+
&& comment.user?.login && currentUser && comment.user.login === currentUser
|
|
951
|
+
? null : { code: 'SUMMARY_REPOSITION_UNAUTHORIZED', message: 'summary reposition requires current finalization authorization and an owned durable summary', retryable: false }
|
|
952
|
+
};
|
|
953
|
+
};
|
|
954
|
+
let strategy: CommentPlanStrategy;
|
|
955
|
+
let operation: Omit<RecordOperationInput, 'state' | 'lastCode'>;
|
|
769
956
|
try {
|
|
770
|
-
|
|
771
|
-
const
|
|
772
|
-
const
|
|
773
|
-
const
|
|
774
|
-
|
|
957
|
+
strategy = readStrategy();
|
|
958
|
+
const source = options.kind === 'task' && options.taskProjection ? options.taskProjection.content : fs.readFileSync(resolved.taskMdPath, 'utf8');
|
|
959
|
+
const expected = expectedComments(resolved.taskId, source, resolved.taskDir, resolved.repoRoot, options);
|
|
960
|
+
const digest = createHash('sha256').update(expected.map((item) => item.content).join('\0')).digest('hex');
|
|
961
|
+
const kind: JournalOperation['kind'] = options.kind === 'task' ? 'task-comment' : options.kind === 'artifact' ? 'artifact-comment' : options.kind === 'summary' ? 'summary-comment' : 'cancel-comment';
|
|
775
962
|
const target = options.kind === 'artifact' ? options.artifact! : options.kind;
|
|
776
|
-
operation = { kind, target, expectedDigest:
|
|
777
|
-
} catch {
|
|
778
|
-
|
|
963
|
+
operation = { taskRef: resolved.taskId, cwd: resolved.repoRoot, kind, target, expectedDigest: digest, dependency: options.dependency ?? 'deferred' };
|
|
964
|
+
} catch (error) {
|
|
965
|
+
const value = error as { code?: string; message?: string };
|
|
966
|
+
return Promise.resolve(platformResult('failed', { error: { code: value.code || 'COMMENT_PAYLOAD_INVALID', message: value.message || String(error), retryable: false } }));
|
|
779
967
|
}
|
|
780
|
-
|
|
781
|
-
const execute = () => syncPlatformCommentImpl(taskRef, options);
|
|
782
|
-
if (options.skipQueue) return execute();
|
|
783
|
-
return coordinatePlatformWrite({
|
|
784
|
-
operation: { taskRef: resolved.taskId, cwd: resolved.repoRoot, ...operation, dependency: options.dependency ?? 'deferred' },
|
|
785
|
-
agent: options.agent,
|
|
786
|
-
execute,
|
|
787
|
-
block: (error) => platformResult('blocked', { error }),
|
|
788
|
-
persistenceFailure: (error) => platformResult('failed', { error })
|
|
789
|
-
});
|
|
968
|
+
return syncCommentPlan(strategy, operation, options.queuedOperation);
|
|
790
969
|
}
|
|
791
970
|
|
|
792
971
|
function inspectPlatformCommentOperation(taskRef: string, options: SyncOptions): { kind: 'task-comment' | 'artifact-comment' | 'summary-comment' | 'cancel-comment'; target: string; expectedDigest: string; id: string } | null {
|
|
@@ -862,15 +1041,13 @@ export {
|
|
|
862
1041
|
chunkArtifactComment,
|
|
863
1042
|
inspectPlatformCommentOperation,
|
|
864
1043
|
findMarkerComments,
|
|
865
|
-
listRemoteComments,
|
|
866
1044
|
checkPlatformCommentOwner,
|
|
867
1045
|
listPlatformComments,
|
|
868
1046
|
normalizeCommentContent,
|
|
869
1047
|
renderTaskComment,
|
|
870
1048
|
renderTaskCommentResult,
|
|
871
|
-
|
|
1049
|
+
syncCommentPlan,
|
|
872
1050
|
syncPlatformComment,
|
|
873
|
-
validateRelatedMarkerSet
|
|
874
|
-
writeComment
|
|
1051
|
+
validateRelatedMarkerSet
|
|
875
1052
|
};
|
|
876
|
-
export type { CommentKind, RemoteComment, RenderedChunk, SyncOptions };
|
|
1053
|
+
export type { CommentKind, CommentPlanStrategy, RemoteComment, RenderedChunk, SyncOptions };
|