@generacy-ai/orchestrator 0.6.0 → 0.8.0
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/dist/config/loader.d.ts.map +1 -1
- package/dist/config/loader.js +32 -1
- package/dist/config/loader.js.map +1 -1
- package/dist/config/schema.d.ts +436 -7
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/launcher/agent-launcher.d.ts +15 -16
- package/dist/launcher/agent-launcher.d.ts.map +1 -1
- package/dist/launcher/agent-launcher.js +54 -23
- package/dist/launcher/agent-launcher.js.map +1 -1
- package/dist/launcher/constants.d.ts +5 -0
- package/dist/launcher/constants.d.ts.map +1 -0
- package/dist/launcher/constants.js +5 -0
- package/dist/launcher/constants.js.map +1 -0
- package/dist/launcher/errors.d.ts +15 -0
- package/dist/launcher/errors.d.ts.map +1 -0
- package/dist/launcher/errors.js +25 -0
- package/dist/launcher/errors.js.map +1 -0
- package/dist/launcher/generic-subprocess-plugin.d.ts +1 -0
- package/dist/launcher/generic-subprocess-plugin.d.ts.map +1 -1
- package/dist/launcher/generic-subprocess-plugin.js +2 -0
- package/dist/launcher/generic-subprocess-plugin.js.map +1 -1
- package/dist/launcher/index.d.ts +2 -1
- package/dist/launcher/index.d.ts.map +1 -1
- package/dist/launcher/index.js +1 -0
- package/dist/launcher/index.js.map +1 -1
- package/dist/launcher/types.d.ts +81 -4
- package/dist/launcher/types.d.ts.map +1 -1
- package/dist/routes/health.d.ts.map +1 -1
- package/dist/routes/health.js +5 -0
- package/dist/routes/health.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +130 -26
- package/dist/server.js.map +1 -1
- package/dist/services/base-advance-monitor-service.d.ts +78 -0
- package/dist/services/base-advance-monitor-service.d.ts.map +1 -0
- package/dist/services/base-advance-monitor-service.js +233 -0
- package/dist/services/base-advance-monitor-service.js.map +1 -0
- package/dist/services/boot-resume-service.d.ts +19 -0
- package/dist/services/boot-resume-service.d.ts.map +1 -0
- package/dist/services/boot-resume-service.js +87 -0
- package/dist/services/boot-resume-service.js.map +1 -0
- package/dist/services/epic-completion-monitor-service.d.ts.map +1 -1
- package/dist/services/epic-completion-monitor-service.js +2 -0
- package/dist/services/epic-completion-monitor-service.js.map +1 -1
- package/dist/services/in-memory-queue-adapter.d.ts +4 -0
- package/dist/services/in-memory-queue-adapter.d.ts.map +1 -1
- package/dist/services/in-memory-queue-adapter.js +30 -3
- package/dist/services/in-memory-queue-adapter.js.map +1 -1
- package/dist/services/label-monitor-service.d.ts +3 -3
- package/dist/services/label-monitor-service.d.ts.map +1 -1
- package/dist/services/label-monitor-service.js +58 -41
- package/dist/services/label-monitor-service.js.map +1 -1
- package/dist/services/label-sync-service.d.ts.map +1 -1
- package/dist/services/label-sync-service.js +45 -10
- package/dist/services/label-sync-service.js.map +1 -1
- package/dist/services/merge-conflict-monitor-service.d.ts +73 -0
- package/dist/services/merge-conflict-monitor-service.d.ts.map +1 -0
- package/dist/services/merge-conflict-monitor-service.js +295 -0
- package/dist/services/merge-conflict-monitor-service.js.map +1 -0
- package/dist/services/orchestrator-version.d.ts +2 -0
- package/dist/services/orchestrator-version.d.ts.map +1 -0
- package/dist/services/orchestrator-version.js +23 -0
- package/dist/services/orchestrator-version.js.map +1 -0
- package/dist/services/phase-tracker-service.d.ts +10 -0
- package/dist/services/phase-tracker-service.d.ts.map +1 -1
- package/dist/services/phase-tracker-service.js +38 -0
- package/dist/services/phase-tracker-service.js.map +1 -1
- package/dist/services/post-activation-dispatch.d.ts +18 -0
- package/dist/services/post-activation-dispatch.d.ts.map +1 -0
- package/dist/services/post-activation-dispatch.js +30 -0
- package/dist/services/post-activation-dispatch.js.map +1 -0
- package/dist/services/post-activation-retry.d.ts +3 -0
- package/dist/services/post-activation-retry.d.ts.map +1 -1
- package/dist/services/post-activation-retry.js +34 -2
- package/dist/services/post-activation-retry.js.map +1 -1
- package/dist/services/pr-feedback-monitor-service.d.ts +13 -4
- package/dist/services/pr-feedback-monitor-service.d.ts.map +1 -1
- package/dist/services/pr-feedback-monitor-service.js +212 -26
- package/dist/services/pr-feedback-monitor-service.js.map +1 -1
- package/dist/services/redis-queue-adapter.d.ts +4 -0
- package/dist/services/redis-queue-adapter.d.ts.map +1 -1
- package/dist/services/redis-queue-adapter.js +90 -10
- package/dist/services/redis-queue-adapter.js.map +1 -1
- package/dist/services/worker-dispatcher.d.ts +14 -2
- package/dist/services/worker-dispatcher.d.ts.map +1 -1
- package/dist/services/worker-dispatcher.js +55 -2
- package/dist/services/worker-dispatcher.js.map +1 -1
- package/dist/types/api.d.ts +3 -0
- package/dist/types/api.d.ts.map +1 -1
- package/dist/types/api.js +1 -0
- package/dist/types/api.js.map +1 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js.map +1 -1
- package/dist/types/monitor.d.ts +72 -4
- package/dist/types/monitor.d.ts.map +1 -1
- package/dist/worker/base-merge.d.ts +54 -0
- package/dist/worker/base-merge.d.ts.map +1 -0
- package/dist/worker/base-merge.js +99 -0
- package/dist/worker/base-merge.js.map +1 -0
- package/dist/worker/clarification-markers.d.ts +29 -0
- package/dist/worker/clarification-markers.d.ts.map +1 -0
- package/dist/worker/clarification-markers.js +44 -0
- package/dist/worker/clarification-markers.js.map +1 -0
- package/dist/worker/clarification-poster.d.ts.map +1 -1
- package/dist/worker/clarification-poster.js +266 -25
- package/dist/worker/clarification-poster.js.map +1 -1
- package/dist/worker/claude-cli-worker.d.ts +21 -2
- package/dist/worker/claude-cli-worker.d.ts.map +1 -1
- package/dist/worker/claude-cli-worker.js +150 -4
- package/dist/worker/claude-cli-worker.js.map +1 -1
- package/dist/worker/cli-spawner.d.ts +4 -0
- package/dist/worker/cli-spawner.d.ts.map +1 -1
- package/dist/worker/cli-spawner.js +53 -7
- package/dist/worker/cli-spawner.js.map +1 -1
- package/dist/worker/config.d.ts +420 -8
- package/dist/worker/config.d.ts.map +1 -1
- package/dist/worker/config.js +179 -2
- package/dist/worker/config.js.map +1 -1
- package/dist/worker/evidence-hash.d.ts +32 -0
- package/dist/worker/evidence-hash.d.ts.map +1 -0
- package/dist/worker/evidence-hash.js +139 -0
- package/dist/worker/evidence-hash.js.map +1 -0
- package/dist/worker/handler-outcome-assertion.d.ts +38 -0
- package/dist/worker/handler-outcome-assertion.d.ts.map +1 -0
- package/dist/worker/handler-outcome-assertion.js +58 -0
- package/dist/worker/handler-outcome-assertion.js.map +1 -0
- package/dist/worker/handler-outcome.d.ts +50 -0
- package/dist/worker/handler-outcome.d.ts.map +1 -0
- package/dist/worker/handler-outcome.js +2 -0
- package/dist/worker/handler-outcome.js.map +1 -0
- package/dist/worker/label-manager.d.ts +58 -2
- package/dist/worker/label-manager.d.ts.map +1 -1
- package/dist/worker/label-manager.js +225 -27
- package/dist/worker/label-manager.js.map +1 -1
- package/dist/worker/merge-conflict-handler.d.ts +141 -0
- package/dist/worker/merge-conflict-handler.d.ts.map +1 -0
- package/dist/worker/merge-conflict-handler.js +642 -0
- package/dist/worker/merge-conflict-handler.js.map +1 -0
- package/dist/worker/merge-conflict-prompt.d.ts +35 -0
- package/dist/worker/merge-conflict-prompt.d.ts.map +1 -0
- package/dist/worker/merge-conflict-prompt.js +71 -0
- package/dist/worker/merge-conflict-prompt.js.map +1 -0
- package/dist/worker/merge-conflict-remedy.d.ts +42 -0
- package/dist/worker/merge-conflict-remedy.d.ts.map +1 -0
- package/dist/worker/merge-conflict-remedy.js +28 -0
- package/dist/worker/merge-conflict-remedy.js.map +1 -0
- package/dist/worker/output-tail-synthesis.d.ts +15 -0
- package/dist/worker/output-tail-synthesis.d.ts.map +1 -0
- package/dist/worker/output-tail-synthesis.js +25 -0
- package/dist/worker/output-tail-synthesis.js.map +1 -0
- package/dist/worker/output-tail.d.ts +13 -0
- package/dist/worker/output-tail.d.ts.map +1 -0
- package/dist/worker/output-tail.js +31 -0
- package/dist/worker/output-tail.js.map +1 -0
- package/dist/worker/pause-context.d.ts +30 -0
- package/dist/worker/pause-context.d.ts.map +1 -0
- package/dist/worker/pause-context.js +98 -0
- package/dist/worker/pause-context.js.map +1 -0
- package/dist/worker/phase-loop.d.ts +66 -0
- package/dist/worker/phase-loop.d.ts.map +1 -1
- package/dist/worker/phase-loop.js +329 -30
- package/dist/worker/phase-loop.js.map +1 -1
- package/dist/worker/pr-feedback-handler.d.ts +46 -46
- package/dist/worker/pr-feedback-handler.d.ts.map +1 -1
- package/dist/worker/pr-feedback-handler.js +286 -108
- package/dist/worker/pr-feedback-handler.js.map +1 -1
- package/dist/worker/pr-manager.d.ts +5 -0
- package/dist/worker/pr-manager.d.ts.map +1 -1
- package/dist/worker/pr-manager.js +7 -0
- package/dist/worker/pr-manager.js.map +1 -1
- package/dist/worker/product-diff.d.ts +39 -0
- package/dist/worker/product-diff.d.ts.map +1 -0
- package/dist/worker/product-diff.js +43 -0
- package/dist/worker/product-diff.js.map +1 -0
- package/dist/worker/provisioning-failure.d.ts +14 -0
- package/dist/worker/provisioning-failure.d.ts.map +1 -0
- package/dist/worker/provisioning-failure.js +2 -0
- package/dist/worker/provisioning-failure.js.map +1 -0
- package/dist/worker/repo-checkout.d.ts +14 -0
- package/dist/worker/repo-checkout.d.ts.map +1 -1
- package/dist/worker/repo-checkout.js +19 -1
- package/dist/worker/repo-checkout.js.map +1 -1
- package/dist/worker/stage-comment-manager.d.ts +42 -1
- package/dist/worker/stage-comment-manager.d.ts.map +1 -1
- package/dist/worker/stage-comment-manager.js +205 -1
- package/dist/worker/stage-comment-manager.js.map +1 -1
- package/dist/worker/terminal-label-op-error.d.ts +23 -0
- package/dist/worker/terminal-label-op-error.d.ts.map +1 -0
- package/dist/worker/terminal-label-op-error.js +27 -0
- package/dist/worker/terminal-label-op-error.js.map +1 -0
- package/dist/worker/types.d.ts +181 -2
- package/dist/worker/types.d.ts.map +1 -1
- package/dist/worker/types.js +11 -0
- package/dist/worker/types.js.map +1 -1
- package/dist/worker/validate-fix-handler.d.ts +79 -0
- package/dist/worker/validate-fix-handler.d.ts.map +1 -0
- package/dist/worker/validate-fix-handler.js +320 -0
- package/dist/worker/validate-fix-handler.js.map +1 -0
- package/dist/worker/worker-result.d.ts +39 -0
- package/dist/worker/worker-result.d.ts.map +1 -0
- package/dist/worker/worker-result.js +10 -0
- package/dist/worker/worker-result.js.map +1 -0
- package/package.json +12 -7
|
@@ -10,48 +10,22 @@ import type { AgentLauncher } from '../launcher/agent-launcher.js';
|
|
|
10
10
|
* 1. Extract PR number from queue item metadata
|
|
11
11
|
* 2. Fetch the PR to get the branch name
|
|
12
12
|
* 3. Switch to the PR branch (not default branch)
|
|
13
|
-
* 4. Fetch fresh unresolved review threads
|
|
14
|
-
* 5. Build a structured prompt with all unresolved comments
|
|
13
|
+
* 4. Fetch fresh unresolved review threads (trust-filtered per-thread)
|
|
14
|
+
* 5. Build a structured prompt with all trusted unresolved comments
|
|
15
15
|
* 6. Spawn Claude CLI to address the feedback
|
|
16
|
-
* 7.
|
|
17
|
-
* 8.
|
|
18
|
-
*
|
|
16
|
+
* 7. Commit + push. If CLI failed OR no diff → Disposition B (blocked)
|
|
17
|
+
* 8. Otherwise: for each trusted unresolved thread — post one reply targeting
|
|
18
|
+
* the root comment, then call `resolveReviewThread(thread.id)`
|
|
19
|
+
* 9. Strict-decrease success test — R = count of resolves that succeeded.
|
|
20
|
+
* R === 0 → Disposition B (blocked); R ≥ 1 → Disposition A (success):
|
|
21
|
+
* warn once per persistently-failed thread, then remove the
|
|
22
|
+
* `waiting-for:address-pr-feedback` label
|
|
19
23
|
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
* - Label retention ensures the task will be retried on next detection cycle
|
|
26
|
-
* - Log timeout warning with structured context
|
|
27
|
-
*
|
|
28
|
-
* **FR-007: Reply failure handling**
|
|
29
|
-
* - If posting replies fails for some threads: still remove label
|
|
30
|
-
* - Log warnings for each failed reply
|
|
31
|
-
* - Partial reply success is acceptable (code changes were pushed successfully)
|
|
32
|
-
* - Handler does not throw on reply failures
|
|
33
|
-
*
|
|
34
|
-
* **SC-006: Thread resolution prevention**
|
|
35
|
-
* - Only use `replyToPRComment()` API — never call any thread-resolve API
|
|
36
|
-
* - Human reviewer will resolve threads after verifying changes
|
|
37
|
-
* - Comments in code explicitly reference SC-006 for clarity
|
|
38
|
-
*
|
|
39
|
-
* **Structured logging**
|
|
40
|
-
* - All operations logged with structured context (prNumber, issueNumber, owner, repo)
|
|
41
|
-
* - Error logs include full error messages and relevant context
|
|
42
|
-
* - Success/failure states clearly logged for observability
|
|
43
|
-
* - All FR/SC references included in log messages for traceability
|
|
44
|
-
*
|
|
45
|
-
* **Critical operation failures**
|
|
46
|
-
* - PR fetch failure: throw (cannot proceed without PR details)
|
|
47
|
-
* - Branch switch failure: throw (wrong branch would cause issues)
|
|
48
|
-
* - Comment fetch failure: throw (cannot address unknown feedback)
|
|
49
|
-
* - Push failure: log error but don't throw (allows retry via label)
|
|
50
|
-
*
|
|
51
|
-
* **Non-critical operation failures**
|
|
52
|
-
* - No unresolved threads: remove label, early return (success case)
|
|
53
|
-
* - Reply posting: log warning, continue (FR-007)
|
|
54
|
-
* - Label removal: log warning, continue (non-fatal)
|
|
24
|
+
* Disposition B (blocked, #883): add `blocked:stuck-feedback-loop` and leave
|
|
25
|
+
* `waiting-for:address-pr-feedback` in place. The monitor's pre-enqueue
|
|
26
|
+
* `blocked:*` check keeps the loop paused until an operator removes the
|
|
27
|
+
* label. This ends the runaway "reply-only" cycle (5→10→20→…) observed on
|
|
28
|
+
* christrudelpw/sniplink#4.
|
|
55
29
|
*/
|
|
56
30
|
export declare class PrFeedbackHandler {
|
|
57
31
|
private readonly config;
|
|
@@ -91,16 +65,42 @@ export declare class PrFeedbackHandler {
|
|
|
91
65
|
*/
|
|
92
66
|
private commitAndPushChanges;
|
|
93
67
|
/**
|
|
94
|
-
* Post a reply
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
* FR-007: Partial failure is acceptable: if some replies fail, log warnings but continue.
|
|
98
|
-
* The handler will still remove the label even if some replies fail.
|
|
68
|
+
* Post a single reply targeting the root comment of a review thread.
|
|
69
|
+
* Returns a discriminated result — failures do not throw so the caller
|
|
70
|
+
* (#883 per-thread outcome loop) can aggregate results.
|
|
99
71
|
*/
|
|
100
|
-
private
|
|
72
|
+
private tryPostReply;
|
|
73
|
+
/**
|
|
74
|
+
* Delegate to `github.resolveReviewThread` (which owns the 3× retry). Wraps
|
|
75
|
+
* the result in a discriminated union so the caller can aggregate outcomes
|
|
76
|
+
* without a try/catch (#883).
|
|
77
|
+
*/
|
|
78
|
+
private tryResolveReviewThread;
|
|
79
|
+
/**
|
|
80
|
+
* Read the short SHA of the just-pushed HEAD commit. Returns null when the
|
|
81
|
+
* git command fails; caller falls back to `<unknown>` in the reply body —
|
|
82
|
+
* the SHA is decoration, not termination logic (#883).
|
|
83
|
+
*/
|
|
84
|
+
private getHeadShortSha;
|
|
85
|
+
/**
|
|
86
|
+
* Add the `blocked:stuck-feedback-loop` label to signal that the fix cycle
|
|
87
|
+
* cannot advance and must not be re-enqueued until the operator removes the
|
|
88
|
+
* label. Non-fatal on failure — leaving `waiting-for:*` in place is the
|
|
89
|
+
* fallback safety net (#883).
|
|
90
|
+
*/
|
|
91
|
+
private addBlockedStuckFeedbackLoopLabel;
|
|
101
92
|
/**
|
|
102
93
|
* Remove the `waiting-for:address-pr-feedback` label from the linked issue.
|
|
103
94
|
*/
|
|
104
95
|
private removeFeedbackLabel;
|
|
96
|
+
/**
|
|
97
|
+
* Structural clear of `agent:in-progress` on the linked issue (#926 SC-004).
|
|
98
|
+
* Called from the shared `finally` block in `handle()` — runs on every
|
|
99
|
+
* terminal exit path (Cases A/B, both blocked-stuck dispositions, happy
|
|
100
|
+
* path, and thrown errors). Idempotent: GitHub's `removeLabels` is a no-op
|
|
101
|
+
* when the label is already absent, so the happy-path coalesced removal +
|
|
102
|
+
* this `finally` clear together produce at most one truthful post-state.
|
|
103
|
+
*/
|
|
104
|
+
private clearInProgressLabel;
|
|
105
105
|
}
|
|
106
106
|
//# sourceMappingURL=pr-feedback-handler.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pr-feedback-handler.d.ts","sourceRoot":"","sources":["../../src/worker/pr-feedback-handler.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"pr-feedback-handler.d.ts","sourceRoot":"","sources":["../../src/worker/pr-feedback-handler.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,SAAS,EAAsB,MAAM,mBAAmB,CAAC;AACvE,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AACzC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEhD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAC3D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,+BAA+B,CAAC;AA6BnE;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,qBAAa,iBAAiB;IAI1B,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,aAAa;IAC9B,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAC;IAN9B,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAe;gBAGzB,MAAM,EAAE,YAAY,EACpB,MAAM,EAAE,MAAM,EACd,aAAa,EAAE,aAAa,EAC5B,UAAU,CAAC,EAAE,eAAe,YAAA;IAK/C;;;;;OAKG;IACG,MAAM,CAAC,IAAI,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IA8UlE;;;;;;;;OAQG;IACH,OAAO,CAAC,mBAAmB;IAgC3B;;;;;OAKG;YACW,sBAAsB;IAkIpC;;;;OAIG;YACW,oBAAoB;IA2ElC;;;;OAIG;YACW,YAAY;IAgB1B;;;;OAIG;YACW,sBAAsB;IAYpC;;;;OAIG;YACW,eAAe;IAe7B;;;;;OAKG;YACW,gCAAgC;IAoB9C;;OAEG;YACW,mBAAmB;IAiBjC;;;;;;;OAOG;YACW,oBAAoB;CAenC"}
|
|
@@ -1,7 +1,19 @@
|
|
|
1
|
-
import { createGitHubClient } from '@generacy-ai/workflow-engine';
|
|
1
|
+
import { createGitHubClient, executeCommand, isTrustedCommentAuthor, tryLoadCommentTrustConfig, wrapUntrustedData, } from '@generacy-ai/workflow-engine';
|
|
2
|
+
import { resolveAgentForPhase } from './config.js';
|
|
2
3
|
import { OutputCapture } from './output-capture.js';
|
|
3
4
|
import { RepoCheckout } from './repo-checkout.js';
|
|
4
5
|
import { buildLaunchCredentials } from './credentials-helper.js';
|
|
6
|
+
/** Label added by the handler when the fix cycle cannot advance (#883). */
|
|
7
|
+
const BLOCKED_STUCK_FEEDBACK_LOOP_LABEL = 'blocked:stuck-feedback-loop';
|
|
8
|
+
/**
|
|
9
|
+
* `agent:in-progress` label — cleared structurally at the single shared exit
|
|
10
|
+
* path (#926 SC-004/SC-005). Extracted to a module constant so the literal
|
|
11
|
+
* appears at exactly one code site: the coalesced happy-path removal and the
|
|
12
|
+
* `clearInProgressLabel` fallback both reference this constant.
|
|
13
|
+
*/
|
|
14
|
+
const AGENT_IN_PROGRESS_LABEL = 'agent:in-progress';
|
|
15
|
+
/** Waiting gate cleared alongside `agent:in-progress` on the happy path. */
|
|
16
|
+
const WAITING_FOR_ADDRESS_PR_FEEDBACK_LABEL = 'waiting-for:address-pr-feedback';
|
|
5
17
|
/**
|
|
6
18
|
* Handles the `address-pr-feedback` command.
|
|
7
19
|
*
|
|
@@ -9,48 +21,22 @@ import { buildLaunchCredentials } from './credentials-helper.js';
|
|
|
9
21
|
* 1. Extract PR number from queue item metadata
|
|
10
22
|
* 2. Fetch the PR to get the branch name
|
|
11
23
|
* 3. Switch to the PR branch (not default branch)
|
|
12
|
-
* 4. Fetch fresh unresolved review threads
|
|
13
|
-
* 5. Build a structured prompt with all unresolved comments
|
|
24
|
+
* 4. Fetch fresh unresolved review threads (trust-filtered per-thread)
|
|
25
|
+
* 5. Build a structured prompt with all trusted unresolved comments
|
|
14
26
|
* 6. Spawn Claude CLI to address the feedback
|
|
15
|
-
* 7.
|
|
16
|
-
* 8.
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* - If CLI times out: push any partial changes that were made
|
|
23
|
-
* - Keep `waiting-for:address-pr-feedback` label (don't remove)
|
|
24
|
-
* - Label retention ensures the task will be retried on next detection cycle
|
|
25
|
-
* - Log timeout warning with structured context
|
|
26
|
-
*
|
|
27
|
-
* **FR-007: Reply failure handling**
|
|
28
|
-
* - If posting replies fails for some threads: still remove label
|
|
29
|
-
* - Log warnings for each failed reply
|
|
30
|
-
* - Partial reply success is acceptable (code changes were pushed successfully)
|
|
31
|
-
* - Handler does not throw on reply failures
|
|
32
|
-
*
|
|
33
|
-
* **SC-006: Thread resolution prevention**
|
|
34
|
-
* - Only use `replyToPRComment()` API — never call any thread-resolve API
|
|
35
|
-
* - Human reviewer will resolve threads after verifying changes
|
|
36
|
-
* - Comments in code explicitly reference SC-006 for clarity
|
|
37
|
-
*
|
|
38
|
-
* **Structured logging**
|
|
39
|
-
* - All operations logged with structured context (prNumber, issueNumber, owner, repo)
|
|
40
|
-
* - Error logs include full error messages and relevant context
|
|
41
|
-
* - Success/failure states clearly logged for observability
|
|
42
|
-
* - All FR/SC references included in log messages for traceability
|
|
43
|
-
*
|
|
44
|
-
* **Critical operation failures**
|
|
45
|
-
* - PR fetch failure: throw (cannot proceed without PR details)
|
|
46
|
-
* - Branch switch failure: throw (wrong branch would cause issues)
|
|
47
|
-
* - Comment fetch failure: throw (cannot address unknown feedback)
|
|
48
|
-
* - Push failure: log error but don't throw (allows retry via label)
|
|
27
|
+
* 7. Commit + push. If CLI failed OR no diff → Disposition B (blocked)
|
|
28
|
+
* 8. Otherwise: for each trusted unresolved thread — post one reply targeting
|
|
29
|
+
* the root comment, then call `resolveReviewThread(thread.id)`
|
|
30
|
+
* 9. Strict-decrease success test — R = count of resolves that succeeded.
|
|
31
|
+
* R === 0 → Disposition B (blocked); R ≥ 1 → Disposition A (success):
|
|
32
|
+
* warn once per persistently-failed thread, then remove the
|
|
33
|
+
* `waiting-for:address-pr-feedback` label
|
|
49
34
|
*
|
|
50
|
-
*
|
|
51
|
-
* -
|
|
52
|
-
*
|
|
53
|
-
*
|
|
35
|
+
* Disposition B (blocked, #883): add `blocked:stuck-feedback-loop` and leave
|
|
36
|
+
* `waiting-for:address-pr-feedback` in place. The monitor's pre-enqueue
|
|
37
|
+
* `blocked:*` check keeps the loop paused until an operator removes the
|
|
38
|
+
* label. This ends the runaway "reply-only" cycle (5→10→20→…) observed on
|
|
39
|
+
* christrudelpw/sniplink#4.
|
|
54
40
|
*/
|
|
55
41
|
export class PrFeedbackHandler {
|
|
56
42
|
config;
|
|
@@ -80,8 +66,15 @@ export class PrFeedbackHandler {
|
|
|
80
66
|
const { prNumber } = metadata;
|
|
81
67
|
const workflowId = `${owner}/${repo}#${issueNumber}`;
|
|
82
68
|
this.logger.info({ prNumber, issueNumber, owner, repo }, 'Starting PR feedback addressing');
|
|
83
|
-
// Create GitHub client scoped to checkout path
|
|
69
|
+
// Create GitHub client scoped to checkout path. Hoisted above the try so
|
|
70
|
+
// the `finally` clear can call it on every exit path (#926 SC-004).
|
|
84
71
|
const github = createGitHubClient(checkoutPath);
|
|
72
|
+
// #926 SC-004: `agent:in-progress` is cleared structurally at a single
|
|
73
|
+
// shared exit path so no terminal return can leave the label pinned.
|
|
74
|
+
// Idempotency-safe: happy path already coalesces the clear into its own
|
|
75
|
+
// `removeLabels` call; this `finally` is a backstop for the four other
|
|
76
|
+
// exit paths (Cases A/B, both blocked-stuck dispositions, and thrown
|
|
77
|
+
// errors). Non-fatal on failure — mirrors `removeFeedbackLabel` shape.
|
|
85
78
|
try {
|
|
86
79
|
// 1. Fetch the PR to get branch name
|
|
87
80
|
let pr;
|
|
@@ -102,28 +95,113 @@ export class PrFeedbackHandler {
|
|
|
102
95
|
this.logger.error({ error: String(error), prNumber, branchName, checkoutPath }, 'Failed to switch to PR branch');
|
|
103
96
|
throw new Error(`Failed to switch to branch ${branchName}: ${String(error)}`);
|
|
104
97
|
}
|
|
105
|
-
// 3. Fetch fresh unresolved review threads
|
|
98
|
+
// 3. Fetch fresh unresolved review threads via GraphQL (#861).
|
|
99
|
+
// REST never populated `.resolved`, so the old filter always emitted []
|
|
100
|
+
// and the handler no-op'd. GraphQL exposes thread-level resolution.
|
|
106
101
|
let allComments;
|
|
102
|
+
let unresolvedThreadCount;
|
|
107
103
|
let unresolvedComments;
|
|
104
|
+
let trustedUnresolvedThreads;
|
|
105
|
+
let untrustedSkips;
|
|
108
106
|
try {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
107
|
+
const threads = await github.getPRReviewThreads(owner, repo, prNumber);
|
|
108
|
+
const unresolvedThreads = threads.filter(t => !t.isResolved);
|
|
109
|
+
allComments = threads.flatMap(t => t.comments);
|
|
110
|
+
unresolvedThreadCount = unresolvedThreads.length;
|
|
111
|
+
// Author-trust gating (#842, #869, #878). Log each skip and drop
|
|
112
|
+
// untrusted comments before the CLI ever sees them. `pr-feedback`
|
|
113
|
+
// surface honors widen-config from .agency/comment-trust.yaml. #878:
|
|
114
|
+
// self-authorship comes from GraphQL's `viewerDidAuthor` field on
|
|
115
|
+
// each comment, not a login-comparison rule threaded via context.
|
|
116
|
+
//
|
|
117
|
+
// #883: also track trusted-unresolved threads (not just comments) so
|
|
118
|
+
// the post-CLI batch can iterate one-reply-per-thread and call
|
|
119
|
+
// `resolveReviewThread` per thread.
|
|
120
|
+
const trustConfig = tryLoadCommentTrustConfig(checkoutPath, this.logger);
|
|
121
|
+
const botLogin = process.env['CLUSTER_GITHUB_USERNAME'] ?? process.env['GH_USERNAME'];
|
|
122
|
+
const trustedUnresolved = [];
|
|
123
|
+
const trustedThreads = [];
|
|
124
|
+
const skips = [];
|
|
125
|
+
for (const thread of unresolvedThreads) {
|
|
126
|
+
let threadHasTrusted = false;
|
|
127
|
+
for (const c of thread.comments) {
|
|
128
|
+
const decision = isTrustedCommentAuthor(c, 'pr-feedback', {
|
|
129
|
+
logger: this.logger,
|
|
130
|
+
...(botLogin ? { botLogin } : {}),
|
|
131
|
+
...(trustConfig ? { config: trustConfig } : {}),
|
|
132
|
+
});
|
|
133
|
+
if (decision.trusted) {
|
|
134
|
+
trustedUnresolved.push(c);
|
|
135
|
+
threadHasTrusted = true;
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
skips.push({
|
|
139
|
+
commentId: c.id,
|
|
140
|
+
author: c.author,
|
|
141
|
+
authorAssociation: c.authorAssociation,
|
|
142
|
+
reason: decision.reason,
|
|
143
|
+
viewerDidAuthor: c.viewerDidAuthor,
|
|
144
|
+
});
|
|
145
|
+
this.logger.info({
|
|
146
|
+
event: 'comment-skipped',
|
|
147
|
+
surface: 'pr-feedback',
|
|
148
|
+
commentId: c.id,
|
|
149
|
+
author: c.author,
|
|
150
|
+
authorAssociation: c.authorAssociation,
|
|
151
|
+
reason: decision.reason,
|
|
152
|
+
viewerDidAuthor: c.viewerDidAuthor ?? null,
|
|
153
|
+
}, 'Skipped PR review comment from untrusted author');
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
if (threadHasTrusted)
|
|
157
|
+
trustedThreads.push(thread);
|
|
158
|
+
}
|
|
159
|
+
unresolvedComments = trustedUnresolved;
|
|
160
|
+
trustedUnresolvedThreads = trustedThreads;
|
|
161
|
+
untrustedSkips = skips;
|
|
162
|
+
this.logger.info({
|
|
163
|
+
prNumber,
|
|
164
|
+
totalComments: allComments.length,
|
|
165
|
+
unresolvedThreads: unresolvedThreads.length,
|
|
166
|
+
trustedUnresolvedThreads: trustedThreads.length,
|
|
167
|
+
trustedUnresolvedComments: trustedUnresolved.length,
|
|
168
|
+
}, 'Fetched PR review threads (author-trust filtered)');
|
|
112
169
|
}
|
|
113
170
|
catch (error) {
|
|
114
|
-
this.logger.error({ error: String(error), prNumber, owner, repo }, 'Failed to fetch PR review
|
|
115
|
-
throw new Error(`Failed to fetch review
|
|
171
|
+
this.logger.error({ error: String(error), prNumber, owner, repo }, 'Failed to fetch PR review threads');
|
|
172
|
+
throw new Error(`Failed to fetch review threads for PR #${prNumber}: ${String(error)}`);
|
|
116
173
|
}
|
|
117
|
-
// 4.
|
|
118
|
-
|
|
174
|
+
// 4. Case A (#869): no unresolved threads at all — remove label and
|
|
175
|
+
// clear dedupe key.
|
|
176
|
+
if (unresolvedThreadCount === 0) {
|
|
119
177
|
this.logger.info({ prNumber, issueNumber }, 'No unresolved threads found — removing label and exiting');
|
|
120
178
|
await this.removeFeedbackLabel(github, owner, repo, issueNumber);
|
|
121
179
|
return;
|
|
122
180
|
}
|
|
181
|
+
// 4b. Case B (#869 / FR-002): unresolved threads exist, but every
|
|
182
|
+
// comment is untrusted (race-window residue that the monitor didn't
|
|
183
|
+
// catch, or degraded-identity mode). Retain the waiting-for label
|
|
184
|
+
// (FR-002) and clear the dedupe key so the next monitor poll can
|
|
185
|
+
// re-enqueue if the situation changes. Do NOT emit the "No unresolved
|
|
186
|
+
// threads found" log line (SC-002).
|
|
187
|
+
if (unresolvedComments.length === 0) {
|
|
188
|
+
this.logger.warn({
|
|
189
|
+
prNumber, issueNumber, owner, repo,
|
|
190
|
+
totalUnresolvedThreads: unresolvedThreadCount,
|
|
191
|
+
untrustedSkips: untrustedSkips.map((s) => ({
|
|
192
|
+
commentId: s.commentId,
|
|
193
|
+
author: s.author,
|
|
194
|
+
authorAssociation: s.authorAssociation,
|
|
195
|
+
reason: s.reason,
|
|
196
|
+
viewerDidAuthor: s.viewerDidAuthor ?? null,
|
|
197
|
+
})),
|
|
198
|
+
}, 'Zero-trusted unresolved threads — retaining waiting-for:address-pr-feedback label (FR-002)');
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
123
201
|
// 5. Build structured prompt
|
|
124
202
|
const prompt = this.buildFeedbackPrompt(unresolvedComments, prNumber, issueNumber);
|
|
125
203
|
// 6. Spawn Claude CLI to address feedback
|
|
126
|
-
const success = await this.spawnClaudeForFeedback(checkoutPath, prompt, workflowId, prNumber);
|
|
204
|
+
const success = await this.spawnClaudeForFeedback(checkoutPath, prompt, workflowId, prNumber, item.workflowName);
|
|
127
205
|
// 7. Commit and push changes (even on timeout — partial completion strategy)
|
|
128
206
|
// FR-013: Push partial changes on timeout to preserve work and enable retry
|
|
129
207
|
let hasChanges = false;
|
|
@@ -135,42 +213,92 @@ export class PrFeedbackHandler {
|
|
|
135
213
|
}
|
|
136
214
|
catch (error) {
|
|
137
215
|
this.logger.error({ error: String(error), prNumber, issueNumber, branchName }, 'Failed to commit and push changes — partial work may be lost');
|
|
138
|
-
// Don't throw here — we
|
|
139
|
-
|
|
216
|
+
// Don't throw here — we still need to run the blocked disposition.
|
|
217
|
+
}
|
|
218
|
+
// 7a. Disposition B short-circuit (#883): CLI did not complete cleanly OR
|
|
219
|
+
// there is no diff. Both mean the loop cannot advance on this cycle —
|
|
220
|
+
// add `blocked:stuck-feedback-loop` and leave `waiting-for:*` intact so
|
|
221
|
+
// the operator sees the pause. Do NOT reply, do NOT resolve, do NOT log
|
|
222
|
+
// success.
|
|
223
|
+
if (!success || !hasChanges) {
|
|
224
|
+
this.logger.warn({
|
|
225
|
+
prNumber,
|
|
226
|
+
issueNumber,
|
|
227
|
+
trigger: 'unresolvedThreads>0',
|
|
228
|
+
reason: !success ? 'cli-did-not-complete' : 'no-diff',
|
|
229
|
+
}, 'no-diff cycle — persisting trigger, entering blocked-stuck-feedback-loop disposition');
|
|
230
|
+
await this.addBlockedStuckFeedbackLoopLabel(github, owner, repo, issueNumber);
|
|
231
|
+
return;
|
|
140
232
|
}
|
|
141
|
-
|
|
142
|
-
|
|
233
|
+
// 7b. Happy path — CLI succeeded AND we have a real commit.
|
|
234
|
+
const shortSha = (await this.getHeadShortSha(checkoutPath)) ?? '<unknown>';
|
|
235
|
+
// 8. Interleaved reply→resolve per root thread (#883, Q4-C, FR-005,
|
|
236
|
+
// FR-007). Input-set closure: iterate `trustedUnresolvedThreads`
|
|
237
|
+
// captured at cycle start.
|
|
238
|
+
const outcomes = [];
|
|
239
|
+
for (const thread of trustedUnresolvedThreads) {
|
|
240
|
+
const replyBody = `Addressed in ${shortSha} — please review, and re-open this thread if it still falls short.`;
|
|
241
|
+
const replyResult = await this.tryPostReply(github, owner, repo, prNumber, thread.rootCommentId, replyBody);
|
|
242
|
+
const resolveResult = await this.tryResolveReviewThread(github, thread.id);
|
|
243
|
+
outcomes.push({
|
|
244
|
+
threadId: thread.id,
|
|
245
|
+
rootCommentId: thread.rootCommentId,
|
|
246
|
+
replyResult,
|
|
247
|
+
resolveResult,
|
|
248
|
+
});
|
|
143
249
|
}
|
|
144
|
-
//
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
// and remove the label. The code changes were pushed successfully.
|
|
153
|
-
this.logger.warn({ error: String(error), prNumber, issueNumber }, 'Failed to post some or all thread replies — partial success acceptable');
|
|
154
|
-
// Continue to label removal
|
|
155
|
-
}
|
|
250
|
+
// 9. Strict-decrease success test (#883, FR-006, FR-010).
|
|
251
|
+
const resolveSuccesses = outcomes.filter(o => o.resolveResult.ok).length;
|
|
252
|
+
const resolveFailures = outcomes.filter(o => !o.resolveResult.ok);
|
|
253
|
+
if (resolveSuccesses === 0) {
|
|
254
|
+
// FR-006 tail — commit landed but no thread transitioned.
|
|
255
|
+
this.logger.warn({ prNumber, issueNumber, outcomes }, 'commit pushed but resolve batch had zero successes — persisting trigger, entering blocked-stuck-feedback-loop disposition');
|
|
256
|
+
await this.addBlockedStuckFeedbackLoopLabel(github, owner, repo, issueNumber);
|
|
257
|
+
return;
|
|
156
258
|
}
|
|
157
|
-
|
|
158
|
-
|
|
259
|
+
// FR-010: one warn per persistently-failed thread, emitted BEFORE
|
|
260
|
+
// clearing the label so a silent partial failure is impossible.
|
|
261
|
+
for (const f of resolveFailures) {
|
|
262
|
+
this.logger.warn({
|
|
263
|
+
prNumber, issueNumber, owner, repo,
|
|
264
|
+
threadId: f.threadId,
|
|
265
|
+
rootCommentId: f.rootCommentId,
|
|
266
|
+
error: f.resolveResult.error,
|
|
267
|
+
remedy: 'Resolve the thread manually in the GitHub UI — the reply is already on the thread',
|
|
268
|
+
}, 'resolveReviewThread persistently failed after retries; label will still be cleared');
|
|
159
269
|
}
|
|
160
|
-
//
|
|
161
|
-
//
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
270
|
+
// 10. Label-clear LAST (Q4 tail). #926 FR-006: coalesce the happy-path
|
|
271
|
+
// clear into a single `removeLabels` call so `waiting-for:*` and
|
|
272
|
+
// `agent:in-progress` disappear in one request — no intermediate state
|
|
273
|
+
// where cockpit / auto observers see one label without the other.
|
|
274
|
+
// The `finally` clear becomes a no-op on this path (idempotent remove).
|
|
275
|
+
try {
|
|
276
|
+
await github.removeLabels(owner, repo, issueNumber, [
|
|
277
|
+
WAITING_FOR_ADDRESS_PR_FEEDBACK_LABEL,
|
|
278
|
+
AGENT_IN_PROGRESS_LABEL,
|
|
279
|
+
]);
|
|
280
|
+
this.logger.info({ issueNumber }, 'Removed waiting-for:address-pr-feedback + agent:in-progress labels (coalesced)');
|
|
165
281
|
}
|
|
166
|
-
|
|
167
|
-
this.logger.
|
|
282
|
+
catch (error) {
|
|
283
|
+
this.logger.warn({ error: String(error), issueNumber }, 'Failed to remove happy-path labels — non-fatal, finally will re-attempt in-progress clear');
|
|
168
284
|
}
|
|
285
|
+
this.logger.info({
|
|
286
|
+
prNumber, issueNumber,
|
|
287
|
+
resolveSuccesses,
|
|
288
|
+
resolveFailures: resolveFailures.length,
|
|
289
|
+
shortSha,
|
|
290
|
+
}, 'PR feedback cycle succeeded (strict decrease met)');
|
|
169
291
|
}
|
|
170
292
|
catch (error) {
|
|
171
293
|
this.logger.error({ error: String(error), prNumber, issueNumber, owner, repo }, 'Error processing PR feedback — task failed');
|
|
172
294
|
throw error;
|
|
173
295
|
}
|
|
296
|
+
finally {
|
|
297
|
+
// #926 SC-004: structural single-point clear. Every terminal exit
|
|
298
|
+
// (Case A, Case B, both blocked-stuck dispositions, happy path, and
|
|
299
|
+
// thrown errors) flows through here. Non-fatal on failure.
|
|
300
|
+
await this.clearInProgressLabel(github, owner, repo, issueNumber);
|
|
301
|
+
}
|
|
174
302
|
}
|
|
175
303
|
/**
|
|
176
304
|
* Build a structured prompt containing all unresolved review comments.
|
|
@@ -190,11 +318,13 @@ export class PrFeedbackHandler {
|
|
|
190
318
|
return `${idx + 1}. **${c.author}** (${location}):\n ${c.body}`;
|
|
191
319
|
})
|
|
192
320
|
.join('\n\n');
|
|
321
|
+
// #842: fence ingested thread content (author-trust filtered upstream).
|
|
322
|
+
const fenced = wrapUntrustedData(commentList, `PR #${prNumber} review comments`);
|
|
193
323
|
return `You are addressing PR review feedback for PR #${prNumber} (linked to issue #${issueNumber}).
|
|
194
324
|
|
|
195
325
|
The following unresolved review comments need to be addressed:
|
|
196
326
|
|
|
197
|
-
${
|
|
327
|
+
${fenced}
|
|
198
328
|
|
|
199
329
|
**Instructions:**
|
|
200
330
|
1. Read and understand each review comment above
|
|
@@ -210,8 +340,12 @@ Please proceed with addressing the feedback.`;
|
|
|
210
340
|
* Returns true if the CLI completed successfully, false on timeout or failure.
|
|
211
341
|
* FR-013: On timeout, partial changes are pushed and label is kept for retry.
|
|
212
342
|
*/
|
|
213
|
-
async spawnClaudeForFeedback(checkoutPath, prompt, workflowId, prNumber) {
|
|
214
|
-
|
|
343
|
+
async spawnClaudeForFeedback(checkoutPath, prompt, workflowId, prNumber, workflowName) {
|
|
344
|
+
// #814 / Q1→B: pr-feedback resolves `{ provider, model }` against the
|
|
345
|
+
// `implement` phase — pr-feedback revises the code `implement` produced,
|
|
346
|
+
// so the same agent/model that wrote the code should address review on it.
|
|
347
|
+
const { provider, model } = resolveAgentForPhase(this.config, workflowName, 'implement');
|
|
348
|
+
this.logger.info({ cwd: checkoutPath, timeoutMs: this.config.phaseTimeoutMs, provider, model }, 'Spawning Claude CLI for PR feedback');
|
|
215
349
|
let child;
|
|
216
350
|
try {
|
|
217
351
|
const handle = await this.agentLauncher.launch({
|
|
@@ -219,10 +353,12 @@ Please proceed with addressing the feedback.`;
|
|
|
219
353
|
kind: 'pr-feedback',
|
|
220
354
|
prNumber,
|
|
221
355
|
prompt,
|
|
356
|
+
...(model !== undefined ? { model } : {}),
|
|
222
357
|
},
|
|
223
358
|
cwd: checkoutPath,
|
|
224
359
|
env: {},
|
|
225
360
|
credentials: buildLaunchCredentials(this.config.credentialRole),
|
|
361
|
+
provider,
|
|
226
362
|
});
|
|
227
363
|
child = handle.process;
|
|
228
364
|
}
|
|
@@ -343,50 +479,92 @@ Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>`;
|
|
|
343
479
|
return true;
|
|
344
480
|
}
|
|
345
481
|
/**
|
|
346
|
-
* Post a reply
|
|
347
|
-
*
|
|
348
|
-
*
|
|
349
|
-
* FR-007: Partial failure is acceptable: if some replies fail, log warnings but continue.
|
|
350
|
-
* The handler will still remove the label even if some replies fail.
|
|
482
|
+
* Post a single reply targeting the root comment of a review thread.
|
|
483
|
+
* Returns a discriminated result — failures do not throw so the caller
|
|
484
|
+
* (#883 per-thread outcome loop) can aggregate results.
|
|
351
485
|
*/
|
|
352
|
-
async
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
const replyBody = `I've addressed this feedback in the latest commit. Please review the changes.`;
|
|
360
|
-
await github.replyToPRComment(owner, repo, prNumber, comment.id, replyBody);
|
|
361
|
-
this.logger.debug({ prNumber, commentId: comment.id, path: comment.path, line: comment.line }, 'Posted reply to review thread');
|
|
362
|
-
successCount++;
|
|
363
|
-
}
|
|
364
|
-
catch (error) {
|
|
365
|
-
// FR-007: Continue processing other threads even if this one fails
|
|
366
|
-
failureCount++;
|
|
367
|
-
this.logger.warn({ error: String(error), prNumber, commentId: comment.id, path: comment.path, line: comment.line }, 'Failed to post reply to review thread — continuing with remaining threads');
|
|
368
|
-
}
|
|
486
|
+
async tryPostReply(github, owner, repo, prNumber, rootCommentId, body) {
|
|
487
|
+
try {
|
|
488
|
+
await github.replyToPRComment(owner, repo, prNumber, rootCommentId, body);
|
|
489
|
+
return { ok: true };
|
|
490
|
+
}
|
|
491
|
+
catch (error) {
|
|
492
|
+
return { ok: false, error: String(error) };
|
|
369
493
|
}
|
|
370
|
-
|
|
371
|
-
|
|
494
|
+
}
|
|
495
|
+
/**
|
|
496
|
+
* Delegate to `github.resolveReviewThread` (which owns the 3× retry). Wraps
|
|
497
|
+
* the result in a discriminated union so the caller can aggregate outcomes
|
|
498
|
+
* without a try/catch (#883).
|
|
499
|
+
*/
|
|
500
|
+
async tryResolveReviewThread(github, threadId) {
|
|
501
|
+
try {
|
|
502
|
+
await github.resolveReviewThread(threadId);
|
|
503
|
+
return { ok: true };
|
|
504
|
+
}
|
|
505
|
+
catch (error) {
|
|
506
|
+
return { ok: false, error: String(error) };
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
/**
|
|
510
|
+
* Read the short SHA of the just-pushed HEAD commit. Returns null when the
|
|
511
|
+
* git command fails; caller falls back to `<unknown>` in the reply body —
|
|
512
|
+
* the SHA is decoration, not termination logic (#883).
|
|
513
|
+
*/
|
|
514
|
+
async getHeadShortSha(checkoutPath) {
|
|
515
|
+
try {
|
|
516
|
+
const result = await executeCommand('git', ['rev-parse', '--short', 'HEAD'], { cwd: checkoutPath });
|
|
517
|
+
if (result.exitCode !== 0)
|
|
518
|
+
return null;
|
|
519
|
+
const sha = result.stdout.trim();
|
|
520
|
+
return sha.length > 0 ? sha : null;
|
|
372
521
|
}
|
|
373
|
-
|
|
374
|
-
|
|
522
|
+
catch {
|
|
523
|
+
return null;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
/**
|
|
527
|
+
* Add the `blocked:stuck-feedback-loop` label to signal that the fix cycle
|
|
528
|
+
* cannot advance and must not be re-enqueued until the operator removes the
|
|
529
|
+
* label. Non-fatal on failure — leaving `waiting-for:*` in place is the
|
|
530
|
+
* fallback safety net (#883).
|
|
531
|
+
*/
|
|
532
|
+
async addBlockedStuckFeedbackLoopLabel(github, owner, repo, issueNumber) {
|
|
533
|
+
try {
|
|
534
|
+
await github.addLabels(owner, repo, issueNumber, [BLOCKED_STUCK_FEEDBACK_LOOP_LABEL]);
|
|
535
|
+
this.logger.info({ issueNumber, label: BLOCKED_STUCK_FEEDBACK_LOOP_LABEL }, 'Added blocked:stuck-feedback-loop label');
|
|
536
|
+
}
|
|
537
|
+
catch (error) {
|
|
538
|
+
this.logger.warn({ error: String(error), issueNumber, label: BLOCKED_STUCK_FEEDBACK_LOOP_LABEL }, 'Failed to add blocked:stuck-feedback-loop label — non-fatal, waiting-for label persists');
|
|
375
539
|
}
|
|
376
|
-
// FR-007: Even if all replies fail, we don't throw — the caller will still remove the label
|
|
377
|
-
// because the code changes were pushed successfully
|
|
378
540
|
}
|
|
379
541
|
/**
|
|
380
542
|
* Remove the `waiting-for:address-pr-feedback` label from the linked issue.
|
|
381
543
|
*/
|
|
382
544
|
async removeFeedbackLabel(github, owner, repo, issueNumber) {
|
|
383
545
|
try {
|
|
384
|
-
await github.removeLabels(owner, repo, issueNumber, [
|
|
546
|
+
await github.removeLabels(owner, repo, issueNumber, [WAITING_FOR_ADDRESS_PR_FEEDBACK_LABEL]);
|
|
385
547
|
this.logger.info({ issueNumber }, 'Removed waiting-for:address-pr-feedback label');
|
|
386
548
|
}
|
|
387
549
|
catch (error) {
|
|
388
550
|
this.logger.warn({ error: String(error), issueNumber }, 'Failed to remove waiting-for:address-pr-feedback label — non-fatal');
|
|
389
551
|
}
|
|
390
552
|
}
|
|
553
|
+
/**
|
|
554
|
+
* Structural clear of `agent:in-progress` on the linked issue (#926 SC-004).
|
|
555
|
+
* Called from the shared `finally` block in `handle()` — runs on every
|
|
556
|
+
* terminal exit path (Cases A/B, both blocked-stuck dispositions, happy
|
|
557
|
+
* path, and thrown errors). Idempotent: GitHub's `removeLabels` is a no-op
|
|
558
|
+
* when the label is already absent, so the happy-path coalesced removal +
|
|
559
|
+
* this `finally` clear together produce at most one truthful post-state.
|
|
560
|
+
*/
|
|
561
|
+
async clearInProgressLabel(github, owner, repo, issueNumber) {
|
|
562
|
+
try {
|
|
563
|
+
await github.removeLabels(owner, repo, issueNumber, [AGENT_IN_PROGRESS_LABEL]);
|
|
564
|
+
}
|
|
565
|
+
catch (error) {
|
|
566
|
+
this.logger.warn({ error: String(error), issueNumber }, 'Failed to remove agent:in-progress label — non-fatal');
|
|
567
|
+
}
|
|
568
|
+
}
|
|
391
569
|
}
|
|
392
570
|
//# sourceMappingURL=pr-feedback-handler.js.map
|