@bridge_gpt/mcp-server 0.2.60 → 0.2.65
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 +58 -11
- package/build/agent-launchers/claude-executor-adapter.js +134 -3
- package/build/agent-launchers/executor-adapter.js +54 -2
- package/build/agents.generated.js +10 -1
- package/build/bounded-wait.js +32 -1
- package/build/commands.generated.js +8 -7
- package/build/conductor/bridge-api-client.js +678 -14
- package/build/conductor/bring-up-facts.js +3 -1
- package/build/conductor/doctor.js +23 -14
- package/build/conductor/epic-child-continuation.js +324 -0
- package/build/conductor/epic-implementer-cli.js +142 -17
- package/build/conductor/git-hooks.js +529 -118
- package/build/conductor/hook-runtime-qualification.js +340 -0
- package/build/conductor/merge-execution-contract.js +30 -5
- package/build/conductor/merge-execution-eligibility.js +10 -9
- package/build/conductor/readiness-sections.js +104 -20
- package/build/conductor/readiness.js +129 -25
- package/build/conductor/recovery-cli.js +354 -13
- package/build/conductor/recovery-operations.js +251 -5
- package/build/conductor/repair-contract.js +8 -0
- package/build/conductor-bundle-artifacts.js +1 -0
- package/build/credential-store.js +194 -0
- package/build/docs.generated.js +2 -1
- package/build/doctor.js +161 -7
- package/build/drive-epic-key.js +12 -0
- package/build/drive-epic.js +1318 -111
- package/build/epic-coherence-input-cli.js +137 -0
- package/build/epic-coherence-input.js +920 -0
- package/build/epic-coherence-input.test-fixtures.js +175 -0
- package/build/epic-implementer-bin.js +35 -29
- package/build/epic-implementer-bundle-cli.js +14 -1
- package/build/epic-integration-pr.js +61 -15
- package/build/epic-launch-profile.js +464 -0
- package/build/epic-run-status.js +5 -0
- package/build/epic-shared/bridge-client.js +46 -4
- package/build/executor/cli.js +48 -0
- package/build/executor/completion-contract.js +423 -0
- package/build/executor/completion-supervision.js +286 -0
- package/build/executor/credentials.js +19 -1
- package/build/executor/deps.js +65 -1
- package/build/executor/env.js +6 -0
- package/build/executor/heartbeat.js +13 -3
- package/build/executor/http-client.js +117 -1
- package/build/executor/integration-job.js +759 -0
- package/build/executor/job-errors.js +48 -0
- package/build/executor/job-runner.js +1386 -35
- package/build/executor/job-types.js +10 -2
- package/build/executor/observation.js +114 -7
- package/build/executor/preflight.js +99 -0
- package/build/executor/process-containment.js +756 -0
- package/build/executor/process.js +235 -5
- package/build/executor/prompt-spec.js +262 -1
- package/build/executor/provider-checks.js +69 -0
- package/build/executor/rebase-finalization.js +351 -0
- package/build/executor/reconcile-overlap-job.js +77 -84
- package/build/executor/required-verification.js +313 -0
- package/build/executor/required-work-ledger.js +1121 -0
- package/build/executor/results.js +142 -0
- package/build/executor/resume-pre-spawn.js +87 -4
- package/build/executor/runner.js +246 -1
- package/build/executor/test-declaration.js +20 -0
- package/build/executor/types.js +55 -0
- package/build/executor/worker-command.js +49 -13
- package/build/executor/worker-finalization.js +88 -11
- package/build/executor/worker-guard-hook.js +29 -4
- package/build/executor/worktree.js +13 -0
- package/build/executor-launch.js +658 -0
- package/build/index.js +772 -954
- package/build/install-bridge-conductor.js +622 -40
- package/build/install-bridge.js +92 -16
- package/build/merge-pull-request.js +1 -1
- package/build/merge-workflow-config.js +54 -0
- package/build/merge-workflow-drift.js +35 -4
- package/build/merge-workflow.js +386 -2
- package/build/pipelines.generated.js +9 -8
- package/build/plan-discovery.js +137 -0
- package/build/plan-epic-implementer-eligibility.js +2 -2
- package/build/plane/cli.js +37 -3
- package/build/plane/member-logs.js +3 -0
- package/build/plane/member-roster.js +7 -1
- package/build/plane/shutdown.js +59 -1
- package/build/plane/supervisor.js +148 -0
- package/build/readme.generated.js +1 -1
- package/build/scripts/executor-protocol-contract-driver.js +13 -3
- package/build/setup-epic.js +130 -48
- package/build/version.generated.js +3 -3
- package/build/worker-guard-hook-bin.js +2 -2
- package/docs/CONDUCTOR.md +116 -2
- package/package.json +2 -1
|
@@ -96,10 +96,11 @@ import { mapConductorDoctorReportToReadinessChecks, } from "./doctor.js";
|
|
|
96
96
|
import { mapInstallStatusChecksToReadinessChecks, } from "../install-doctor.js";
|
|
97
97
|
import { mapPlanePreflightToReadinessChecks, } from "../plane/preflight.js";
|
|
98
98
|
import { runConductorInstallDoctor, } from "./install-doctor.js";
|
|
99
|
-
import { BRING_UP_SECTION_LABELS } from "./readiness-sections.js";
|
|
99
|
+
import { BRING_UP_SECTION_LABELS, presentRequiredChecks, } from "./readiness-sections.js";
|
|
100
100
|
import { classifyBringUpFacts } from "./bring-up-facts.js";
|
|
101
101
|
import { classifyMergeExecutionEligibility, MERGE_EXECUTION_INELIGIBLE_REMEDIATION, MERGE_EXECUTION_UNVERIFIABLE_REMEDIATION, } from "./merge-execution-eligibility.js";
|
|
102
102
|
import { createReadinessCheck, createReadinessCheckSafely, summarizeReadinessChecks, validateReadinessChecks, } from "../readiness-check.js";
|
|
103
|
+
import { ADMISSION_EVIDENCE_SCHEMA_VERSION, } from "./bridge-api-client.js";
|
|
103
104
|
/** This report's own schema version. See the module docstring. */
|
|
104
105
|
export const CONDUCTOR_READINESS_GATE_SCHEMA_VERSION = 1;
|
|
105
106
|
/** Fixed statement carried in every report, so no consumer can mistake it for a gate. */
|
|
@@ -140,7 +141,7 @@ export const SERVER_READINESS_DESCRIPTORS = [
|
|
|
140
141
|
{ id: "executor-readiness", label: "Executor server observation" },
|
|
141
142
|
{ id: "review-workflow", label: "Review workflow protocol" },
|
|
142
143
|
{ id: "conductor-ci-workflow", label: "Conductor CI migration guard" },
|
|
143
|
-
// BAPI-1102 — the two
|
|
144
|
+
// BAPI-1102 — the two unattended repository prerequisites plus the aggregate the
|
|
144
145
|
// third unattended refusal reads. Noun phrases, like every row above: these
|
|
145
146
|
// render as list items under a source heading, not as sentences.
|
|
146
147
|
{ id: "unattended-consent", label: "Repository unattended consent" },
|
|
@@ -170,7 +171,6 @@ const SERVER_FAILURE_COPY = {
|
|
|
170
171
|
const SERVER_REMEDIATIONS = {
|
|
171
172
|
supervisorSetup: "run `install conductor` (or `setup-epic`) for this repository to write the project-default supervisor posture.",
|
|
172
173
|
supervisorConfig: "run `install conductor` for this repository to write the project-default supervisor configuration.",
|
|
173
|
-
requiredChecks: "configure a non-empty required-check list in done_gate_config; an empty list makes the done gate pass unconditionally.",
|
|
174
174
|
mergeApproval: "set the merge-approval requirement explicitly in the supervisor configuration rather than leaving it unset.",
|
|
175
175
|
reviewPolicyMissing: "set the repository review-policy default so the merge path and the code_review gate read the same signal.",
|
|
176
176
|
reviewPolicyDivergent: "align the repository review-policy default with the done gate's review_state condition, or confirm the difference is intended.",
|
|
@@ -189,8 +189,10 @@ const SERVER_REMEDIATIONS = {
|
|
|
189
189
|
olderServer: "upgrade the Bridge API deploy so it reports this fact; its state is unknown here, not healthy.",
|
|
190
190
|
// BAPI-1102. Each names the ONE action that clears it, and none of them
|
|
191
191
|
// mentions a webhook URL, a destination, or any configuration value.
|
|
192
|
-
|
|
193
|
-
|
|
192
|
+
// BAPI-1273: consent defaults ON; the repository owner switches it on the
|
|
193
|
+
// admin panel's Setup page. No MCP or CLI surface ever writes it.
|
|
194
|
+
unattendedConsent: "turn on 'Allow unattended Epic Implementer runs' on the repository's Setup page in the Bridge admin panel, or run setup with --attended. If it is already on, contact Bridge support.",
|
|
195
|
+
unattendedNotifyDefault: "supply a --policy-file declaring notify.webhook_url, or add an email address to the account so escalation alerts can be emailed, or run setup with --attended; an unattended run must have a working escalation channel, and notify.local_sink is not one.",
|
|
194
196
|
repositoryWorkflowReadiness: "run `install-bridge conductor` so the review and conductor-CI workflows are confirmed on the default branch, or run setup with --attended.",
|
|
195
197
|
// BAPI-1135. Both are IMPORTED from the shared classifier rather than written
|
|
196
198
|
// out here, so the remedy command has exactly one spelling across the refusal,
|
|
@@ -252,18 +254,20 @@ export function mapServerReadinessToChecks(readiness, failure = "unreachable") {
|
|
|
252
254
|
});
|
|
253
255
|
// Counts and booleans are safe facts and stay in `detail`; they never become
|
|
254
256
|
// checks of their own, because there is nothing separate to remediate.
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
:
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
257
|
+
//
|
|
258
|
+
// BAPI-1254: a nonempty count is not proof. The shared presentation — the same
|
|
259
|
+
// one the install-time section renders — passes ONLY a server `verified`
|
|
260
|
+
// verdict; a confirmed mismatch, could-not-verify, an empty list, and an older
|
|
261
|
+
// server's silence all fail, with distinct fixed copy.
|
|
262
|
+
const requiredChecks = presentRequiredChecks(s);
|
|
263
|
+
outcomes.push({
|
|
264
|
+
id: "supervisor-required-checks",
|
|
265
|
+
status: requiredChecks.satisfied ? "pass" : "fail",
|
|
266
|
+
detail: requiredChecks.detail,
|
|
267
|
+
...(requiredChecks.remediation === undefined
|
|
268
|
+
? {}
|
|
269
|
+
: { remediation: requiredChecks.remediation }),
|
|
270
|
+
});
|
|
267
271
|
// Auto-merge OFF is a posture, not a gap: an operator may deliberately merge
|
|
268
272
|
// by hand. Reported, never remediated.
|
|
269
273
|
outcomes.push({
|
|
@@ -309,7 +313,7 @@ export function mapServerReadinessToChecks(readiness, failure = "unreachable") {
|
|
|
309
313
|
outcomes.push(ciWorkflowOutcome(readiness));
|
|
310
314
|
outcomes.push(...unattendedOutcomes(readiness));
|
|
311
315
|
const byId = new Map(outcomes.map((outcome) => [outcome.id, outcome]));
|
|
312
|
-
|
|
316
|
+
const described = SERVER_READINESS_DESCRIPTORS.map(({ id, label }) => {
|
|
313
317
|
const outcome = byId.get(id);
|
|
314
318
|
if (!outcome) {
|
|
315
319
|
return createReadinessCheck({
|
|
@@ -332,6 +336,97 @@ export function mapServerReadinessToChecks(readiness, failure = "unreachable") {
|
|
|
332
336
|
: { remediation: outcome.remediation ?? SERVER_REMEDIATIONS.olderServer }),
|
|
333
337
|
});
|
|
334
338
|
});
|
|
339
|
+
// BAPI-1260 — appended ONLY when the server reported the block, so every
|
|
340
|
+
// pre-BAPI-1260 report keeps exactly the descriptor set above.
|
|
341
|
+
return [...described, ...admissionEvidenceChecks(readiness)];
|
|
342
|
+
}
|
|
343
|
+
/** Human labels for the known BAPI-1260 evidence predicates. */
|
|
344
|
+
const ADMISSION_EVIDENCE_LABELS = {
|
|
345
|
+
unattended_consent: "Launch evidence: unattended consent",
|
|
346
|
+
notify_webhook: "Launch evidence: notification destination",
|
|
347
|
+
repository_readiness: "Launch evidence: repository workflows",
|
|
348
|
+
required_checks: "Launch evidence: required CI checks",
|
|
349
|
+
index_provenance: "Launch evidence: canonical index provenance",
|
|
350
|
+
durable_actor: "Launch evidence: durable actor handshake",
|
|
351
|
+
migration_head: "Launch evidence: database migration head",
|
|
352
|
+
};
|
|
353
|
+
/** Who acts, in fixed prose. Never a value from the payload. */
|
|
354
|
+
const ADMISSION_OWNER_REMEDIATION = {
|
|
355
|
+
operator: "the operator re-runs guided setup (`install conductor --launch-profile <name>`) and re-observes.",
|
|
356
|
+
repository_admin: "a repository administrator resolves it (unattended consent is the 'Allow unattended Epic Implementer runs' toggle on the repository's Setup page in the Bridge admin panel); setup never grants it.",
|
|
357
|
+
bridge_operations: "escalate to Bridge operations; this is not something an operator command can change.",
|
|
358
|
+
};
|
|
359
|
+
/**
|
|
360
|
+
* Project the server's typed launch evidence (BAPI-1260) into `server.*`
|
|
361
|
+
* checks, read-only.
|
|
362
|
+
*
|
|
363
|
+
* The CLIENT decides nothing here: `confirmed` renders pass, `not_applicable`
|
|
364
|
+
* renders skip, and `denied` and `unable_to_verify` both render fail with
|
|
365
|
+
* DIFFERENT detail — a known denial and an inability to verify call for
|
|
366
|
+
* different actions, and neither authorizes a launch. The server is the only
|
|
367
|
+
* admission authority; this row is what it said, not a verdict of our own.
|
|
368
|
+
*/
|
|
369
|
+
export function admissionEvidenceChecks(readiness) {
|
|
370
|
+
const evidence = readiness.admission_evidence;
|
|
371
|
+
if (evidence === undefined)
|
|
372
|
+
return [];
|
|
373
|
+
if (evidence === null || evidence.schema_version !== ADMISSION_EVIDENCE_SCHEMA_VERSION) {
|
|
374
|
+
return [
|
|
375
|
+
createReadinessCheckSafely({
|
|
376
|
+
id: "server.admission-evidence",
|
|
377
|
+
source: "server",
|
|
378
|
+
label: "Launch evidence",
|
|
379
|
+
status: "fail",
|
|
380
|
+
detail: evidence === null
|
|
381
|
+
? "the server reported no launch evidence, so no mandatory prerequisite is proven"
|
|
382
|
+
: "the server's launch-evidence schema is not one this CLI renders; treat every prerequisite as unverified",
|
|
383
|
+
remediation: "upgrade the CLI or the Bridge API deploy so both speak the same evidence schema.",
|
|
384
|
+
}),
|
|
385
|
+
];
|
|
386
|
+
}
|
|
387
|
+
return evidence.records.map((record) => admissionEvidenceCheck(record));
|
|
388
|
+
}
|
|
389
|
+
function admissionEvidenceCheck(record) {
|
|
390
|
+
const slug = record.predicate.replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "unknown";
|
|
391
|
+
const label = ADMISSION_EVIDENCE_LABELS[record.predicate] ?? "Launch evidence";
|
|
392
|
+
const reason = record.reason ? ` (${record.reason})` : "";
|
|
393
|
+
const remediation = ADMISSION_OWNER_REMEDIATION[record.next_action_owner ?? "operator"] ?? ADMISSION_OWNER_REMEDIATION.operator;
|
|
394
|
+
switch (record.verdict) {
|
|
395
|
+
case "confirmed":
|
|
396
|
+
return createReadinessCheckSafely({
|
|
397
|
+
id: `server.admission-${slug}`,
|
|
398
|
+
source: "server",
|
|
399
|
+
label,
|
|
400
|
+
status: "pass",
|
|
401
|
+
detail: `confirmed by ${record.source}; fresh for ${record.max_age_seconds}s after observation`,
|
|
402
|
+
});
|
|
403
|
+
case "not_applicable":
|
|
404
|
+
return createReadinessCheckSafely({
|
|
405
|
+
id: `server.admission-${slug}`,
|
|
406
|
+
source: "server",
|
|
407
|
+
label,
|
|
408
|
+
status: "skip",
|
|
409
|
+
detail: `not applicable${reason}`,
|
|
410
|
+
});
|
|
411
|
+
case "denied":
|
|
412
|
+
return createReadinessCheckSafely({
|
|
413
|
+
id: `server.admission-${slug}`,
|
|
414
|
+
source: "server",
|
|
415
|
+
label,
|
|
416
|
+
status: "fail",
|
|
417
|
+
detail: `DENIED by ${record.source}${reason} — dependent activation, dispatch, and privileged operations are held`,
|
|
418
|
+
remediation,
|
|
419
|
+
});
|
|
420
|
+
default:
|
|
421
|
+
return createReadinessCheckSafely({
|
|
422
|
+
id: `server.admission-${slug}`,
|
|
423
|
+
source: "server",
|
|
424
|
+
label,
|
|
425
|
+
status: "fail",
|
|
426
|
+
detail: `could not be verified${reason} — unknown is not safe, so dependent work is held`,
|
|
427
|
+
remediation,
|
|
428
|
+
});
|
|
429
|
+
}
|
|
335
430
|
}
|
|
336
431
|
/**
|
|
337
432
|
* The three BAPI-1153 bring-up safety rows.
|
|
@@ -392,7 +487,7 @@ function unattendedOutcomes(readiness) {
|
|
|
392
487
|
: {
|
|
393
488
|
id: "unattended-consent",
|
|
394
489
|
status: "warn",
|
|
395
|
-
detail: "
|
|
490
|
+
detail: "unattended runs are off for this repository — an unattended run will be refused before it is created",
|
|
396
491
|
remediation: SERVER_REMEDIATIONS.unattendedConsent,
|
|
397
492
|
},
|
|
398
493
|
u.notify_webhook_default_declared
|
|
@@ -405,12 +500,21 @@ function unattendedOutcomes(readiness) {
|
|
|
405
500
|
? "a notify webhook default is declared and verified"
|
|
406
501
|
: "a notify webhook default is declared (verified at approval, not here)",
|
|
407
502
|
}
|
|
408
|
-
:
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
503
|
+
: u.escalation_email_available === true
|
|
504
|
+
? {
|
|
505
|
+
// BAPI-1274 — with no webhook declared, escalation alerts go to the
|
|
506
|
+
// account's email address. Presence only: the address is never
|
|
507
|
+
// reported by the server and is never rendered here.
|
|
508
|
+
id: "unattended-notify-default",
|
|
509
|
+
status: "pass",
|
|
510
|
+
detail: "No webhook declared; escalation alerts will be emailed to the account's email address",
|
|
511
|
+
}
|
|
512
|
+
: {
|
|
513
|
+
id: "unattended-notify-default",
|
|
514
|
+
status: "warn",
|
|
515
|
+
detail: "no repository notify webhook default is declared — an unattended run needs an escalation channel, and no column supplies a repository default today, so the run must declare notify.webhook_url itself or the account needs an email address for escalation alerts",
|
|
516
|
+
remediation: SERVER_REMEDIATIONS.unattendedNotifyDefault,
|
|
517
|
+
},
|
|
414
518
|
u.repository_readiness_confirmed
|
|
415
519
|
? {
|
|
416
520
|
id: "repository-workflow-readiness",
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `conductor` operator recovery and repair verbs —
|
|
2
|
+
* `conductor` operator recovery and repair verbs — twelve CLI-only commands.
|
|
3
3
|
*
|
|
4
4
|
* The four BAPI-872 recovery verbs: `stop-run`, `abandon-run`, `unpark`,
|
|
5
5
|
* `adopt-current-head-and-unpark`. The six run-addressed repair verbs: BAPI-1154's
|
|
6
6
|
* `scope-reparse`, `dispatch-remediation`, `merge-child`, and BAPI-1176's
|
|
7
|
-
* `scope-catch-up`, `patch-policy`, `resume-run`.
|
|
7
|
+
* `scope-catch-up`, `patch-policy`, `resume-run`. BAPI-1300's parked-child
|
|
8
|
+
* continuation pair: `continue-child` and the read-only `continuation-receipt`.
|
|
8
9
|
*
|
|
9
10
|
* CLI-only by design — this is an explicit ticket non-goal, not an oversight:
|
|
10
|
-
* none of these
|
|
11
|
+
* none of these twelve command names is ever registered as an MCP tool, tool
|
|
11
12
|
* description, or generated tool-schema input (`index-static.test.ts` guards
|
|
12
13
|
* this). They exist so an operator (or `plane down`, via
|
|
13
14
|
* `recovery-operations.ts`) can recover a stuck run or ticket WITHOUT
|
|
@@ -23,7 +24,7 @@
|
|
|
23
24
|
*/
|
|
24
25
|
import { readFile } from "node:fs/promises";
|
|
25
26
|
import { ConductorValidationError, toConductorErrorEnvelope } from "./errors.js";
|
|
26
|
-
import { DRIVER_CONTEXT_MAX_BYTES, POLICY_PATCH_MAX_BYTES } from "./repair-contract.js";
|
|
27
|
+
import { DRIVER_CONTEXT_MAX_BYTES, POLICY_PATCH_MAX_BYTES, RESUME_RUN_UNCONFIRMED_MESSAGE, } from "./repair-contract.js";
|
|
27
28
|
// ---------------------------------------------------------------------------
|
|
28
29
|
// Flag parsing
|
|
29
30
|
// ---------------------------------------------------------------------------
|
|
@@ -43,6 +44,12 @@ const REJECTED_VERSION_FLAGS = new Set([
|
|
|
43
44
|
"--expected_row_version",
|
|
44
45
|
"--expected-generation",
|
|
45
46
|
"--expected_generation",
|
|
47
|
+
// BAPI-1300: the continuation's park-episode binding is read once and stored
|
|
48
|
+
// by the client itself; an operator never supplies it.
|
|
49
|
+
"--park-generation",
|
|
50
|
+
"--park_generation",
|
|
51
|
+
"--source-park-generation",
|
|
52
|
+
"--source_park_generation",
|
|
46
53
|
]);
|
|
47
54
|
/**
|
|
48
55
|
* Tokenize `--flag value` / `--flag=value` / boolean flags for the recovery
|
|
@@ -266,16 +273,52 @@ const ADOPT_CURRENT_HEAD_USAGE = [
|
|
|
266
273
|
"off its anchored commit: adopt the CURRENT PR head and unpark in one step.",
|
|
267
274
|
"Retries a bounded number of times on its own if the ticket changes",
|
|
268
275
|
"concurrently — never accepts a version counter.",
|
|
276
|
+
"",
|
|
277
|
+
"For a ticket parked as unexplained_review_head_drift this is \"Adopt current",
|
|
278
|
+
"head and resume CI\": the server reads the provider's current head, accepts it,",
|
|
279
|
+
"keeps every repair allowance already spent (no repair attempt is granted), and",
|
|
280
|
+
"resumes the ticket at CI. No worker is started and nothing is marked passed —",
|
|
281
|
+
"CI and review must pass again on the accepted head. A repeat of the same",
|
|
282
|
+
"request reports the recorded result instead of adopting twice.",
|
|
269
283
|
].join("\n");
|
|
270
|
-
|
|
284
|
+
/** BAPI-1310 (E1): the fixed allowance sentence the adoption result carries. */
|
|
285
|
+
const REVIEW_HEAD_ALLOWANCE_LINE = "Allowance effect: repair allowances preserved; acceptance does not grant another repair.";
|
|
286
|
+
/** BAPI-1310 (E1): the warning shown when run control still holds the run. */
|
|
287
|
+
export const REVIEW_HEAD_HELD_WARNING = "Head accepted; run remains held by run control";
|
|
288
|
+
/** Render the E1 adoption result (committed or replayed) to stdout. */
|
|
289
|
+
function renderReviewHeadRecovery(ticketKey, epicRunId, recovery) {
|
|
290
|
+
const outcome = recovery.result === "replayed"
|
|
291
|
+
? "replayed — this operation already committed; nothing changed now"
|
|
292
|
+
: "committed";
|
|
293
|
+
console.log(`Ticket ${ticketKey} (run ${epicRunId}): Adopt current head and resume CI — ${outcome}.`);
|
|
294
|
+
console.log(` Previous expected head: ${recovery.previous_expected_head_sha}`);
|
|
295
|
+
console.log(` Committed accepted head: ${recovery.accepted_head_sha}`);
|
|
296
|
+
console.log(` ${REVIEW_HEAD_ALLOWANCE_LINE}`);
|
|
297
|
+
console.log(` Resumed at: ${recovery.resumed_gate}`);
|
|
298
|
+
console.log(` Current gate: ${recovery.current_gate ?? "unavailable"}`);
|
|
299
|
+
console.log(" CI and review must pass again on the accepted head; no worker was started and nothing was marked passed.");
|
|
300
|
+
if (recovery.held_by_run_control) {
|
|
301
|
+
console.log(` WARNING: ${REVIEW_HEAD_HELD_WARNING}.`);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
export function renderTicketRecoveryResult(result, json) {
|
|
271
305
|
if (json) {
|
|
272
306
|
console.log(JSON.stringify(result));
|
|
273
307
|
return result.ok ? 0 : 1;
|
|
274
308
|
}
|
|
275
309
|
if (result.ok) {
|
|
310
|
+
if (result.reviewHeadRecovery) {
|
|
311
|
+
renderReviewHeadRecovery(result.ticketKey, result.epicRunId, result.reviewHeadRecovery);
|
|
312
|
+
return 0;
|
|
313
|
+
}
|
|
276
314
|
console.log(`Ticket ${result.ticketKey} (run ${result.epicRunId}): unparked — status: ${result.status}.`);
|
|
277
315
|
return 0;
|
|
278
316
|
}
|
|
317
|
+
if (result.kind === "refused") {
|
|
318
|
+
console.error(`Ticket ${result.ticketKey} (run ${result.epicRunId}): refused (${result.errorCode}) — ` +
|
|
319
|
+
`${result.message}. The ticket remains parked; nothing was changed.`);
|
|
320
|
+
return 1;
|
|
321
|
+
}
|
|
279
322
|
if (result.kind === "ticket-not-found") {
|
|
280
323
|
console.error(`Ticket ${result.ticketKey}: not found in run ${result.epicRunId}.`);
|
|
281
324
|
return 1;
|
|
@@ -440,7 +483,7 @@ function renderRepairResult(outcome, input, json, extraLines = () => []) {
|
|
|
440
483
|
}
|
|
441
484
|
console.log(JSON.stringify(outcome.kind === "refused"
|
|
442
485
|
? { ok: false, kind: "refused", code: outcome.code, message: outcome.message }
|
|
443
|
-
: { ok: false, kind: "unavailable", message: outcome.message }));
|
|
486
|
+
: { ok: false, kind: "unavailable", message: input.unavailableMessage ?? outcome.message }));
|
|
444
487
|
return 1;
|
|
445
488
|
}
|
|
446
489
|
if (!outcome.ok) {
|
|
@@ -448,6 +491,10 @@ function renderRepairResult(outcome, input, json, extraLines = () => []) {
|
|
|
448
491
|
console.error(`Error: ${input.verb} refused on run ${input.epicRunId} (${input.targetLabel}) — ` +
|
|
449
492
|
`${outcome.code}: ${outcome.message}`);
|
|
450
493
|
}
|
|
494
|
+
else if (input.unavailableMessage) {
|
|
495
|
+
console.error(`Error: ${input.verb} on run ${input.epicRunId} (${input.targetLabel}) — ` +
|
|
496
|
+
`${input.unavailableMessage}.`);
|
|
497
|
+
}
|
|
451
498
|
else {
|
|
452
499
|
console.error(`Error: ${input.verb} could not complete on run ${input.epicRunId} ` +
|
|
453
500
|
`(${input.targetLabel}) — ${outcome.message}.`);
|
|
@@ -788,11 +835,16 @@ export async function runPatchPolicyCommand(argv, deps = {}) {
|
|
|
788
835
|
const RESUME_RUN_USAGE = [
|
|
789
836
|
"Usage: epic-implementer resume-run --epic-run-id <id> [--clear-quarantine] [--json]",
|
|
790
837
|
"",
|
|
791
|
-
"
|
|
792
|
-
"
|
|
793
|
-
"
|
|
794
|
-
"
|
|
795
|
-
"a
|
|
838
|
+
"Return a held run to active, after the server re-checks the run's activation",
|
|
839
|
+
"requirements. A paused run's control record is cleared. A run blocked by an",
|
|
840
|
+
"eligible integration block has that block lifted (there is no control record",
|
|
841
|
+
"to clear); the server decides eligibility. A quarantined run resumes only with",
|
|
842
|
+
"--clear-quarantine; a stopped run never resumes (start a new run). A run that",
|
|
843
|
+
"is already active and unheld is a successful no-op (already current). The",
|
|
844
|
+
"server retries a concurrent control change itself — never supply a generation.",
|
|
845
|
+
"",
|
|
846
|
+
"If the response is lost or cannot be read, the resume may or may not have",
|
|
847
|
+
"committed: inspect the run read-only before any separately authorized retry.",
|
|
796
848
|
].join("\n");
|
|
797
849
|
export async function runResumeRunCommand(argv) {
|
|
798
850
|
try {
|
|
@@ -812,8 +864,9 @@ export async function runResumeRunCommand(argv) {
|
|
|
812
864
|
return renderRepairResult(outcome, {
|
|
813
865
|
verb: "resume-run",
|
|
814
866
|
epicRunId: parsed.epicRunId,
|
|
815
|
-
targetLabel: parsed.clearQuarantine ? "run
|
|
816
|
-
stateLabel: "run
|
|
867
|
+
targetLabel: parsed.clearQuarantine ? "run (clear quarantine)" : "run",
|
|
868
|
+
stateLabel: "run state",
|
|
869
|
+
unavailableMessage: RESUME_RUN_UNCONFIRMED_MESSAGE,
|
|
817
870
|
}, parsed.json);
|
|
818
871
|
}
|
|
819
872
|
catch (error) {
|
|
@@ -821,3 +874,291 @@ export async function runResumeRunCommand(argv) {
|
|
|
821
874
|
return 1;
|
|
822
875
|
}
|
|
823
876
|
}
|
|
877
|
+
// ---------------------------------------------------------------------------
|
|
878
|
+
// continue-child / continuation-receipt (BAPI-1300)
|
|
879
|
+
// ---------------------------------------------------------------------------
|
|
880
|
+
const CONTINUE_CHILD_VALUE_FLAGS = new Set(["--epic-run-id", "--ticket-key", "--reason"]);
|
|
881
|
+
const CONTINUATION_RECEIPT_VALUE_FLAGS = new Set(["--epic-run-id", "--ticket-key", "--request-id"]);
|
|
882
|
+
const MAX_REASON_CHARS = 500;
|
|
883
|
+
const CONTINUE_CHILD_USAGE = [
|
|
884
|
+
"Usage: epic-implementer continue-child --epic-run-id <id> --ticket-key <key> [--reason <text>] [--json]",
|
|
885
|
+
"",
|
|
886
|
+
"Continue a parked child's CURRENT park episode without resetting it: the same",
|
|
887
|
+
"attempt resumes on its retained work with only the allowance it has left (no",
|
|
888
|
+
"fresh budget, no counter or clock reset — use unpark for that). The server",
|
|
889
|
+
"commits one job and one durable receipt together, at most once per episode.",
|
|
890
|
+
"",
|
|
891
|
+
"If the response is lost, the outcome is reported UNCONFIRMED, never refused or",
|
|
892
|
+
"committed. Re-running continue-child reuses the SAME request (it never",
|
|
893
|
+
"approves a new head or episode); continuation-receipt looks the receipt up.",
|
|
894
|
+
"Never accepts a version counter or a park generation.",
|
|
895
|
+
].join("\n");
|
|
896
|
+
const CONTINUATION_RECEIPT_USAGE = [
|
|
897
|
+
"Usage: epic-implementer continuation-receipt --epic-run-id <id> --ticket-key <key> [--request-id <id>] [--json]",
|
|
898
|
+
"",
|
|
899
|
+
"Read-only. Show the child's current park episode and, for the pending local",
|
|
900
|
+
"continuation request (or --request-id), whether its receipt proves a commit.",
|
|
901
|
+
"A missing receipt is not a refusal. Looking history up authorizes nothing.",
|
|
902
|
+
].join("\n");
|
|
903
|
+
function parseContinuationArgs(argv, valueFlags) {
|
|
904
|
+
const { values, bools } = tokenizeRecoveryFlags(argv, valueFlags, RECOVERY_BOOL_FLAGS);
|
|
905
|
+
if (bools.has("--help")) {
|
|
906
|
+
return { epicRunId: "", ticketKey: "", json: bools.has("--json"), help: true };
|
|
907
|
+
}
|
|
908
|
+
const epicRunId = requireFlag(values, "--epic-run-id");
|
|
909
|
+
const ticketKey = requireFlag(values, "--ticket-key");
|
|
910
|
+
const reason = values.get("--reason")?.trim();
|
|
911
|
+
if (reason !== undefined && reason.length > MAX_REASON_CHARS) {
|
|
912
|
+
throw new ConductorValidationError(`Flag "--reason" must be at most ${MAX_REASON_CHARS} characters.`);
|
|
913
|
+
}
|
|
914
|
+
const requestId = values.get("--request-id")?.trim();
|
|
915
|
+
if (requestId !== undefined && !/^[A-Za-z0-9][A-Za-z0-9._:-]{7,63}$/.test(requestId)) {
|
|
916
|
+
throw new ConductorValidationError('Flag "--request-id" is not a valid request id.');
|
|
917
|
+
}
|
|
918
|
+
return {
|
|
919
|
+
epicRunId,
|
|
920
|
+
ticketKey,
|
|
921
|
+
...(reason ? { reason } : {}),
|
|
922
|
+
...(requestId ? { requestId } : {}),
|
|
923
|
+
json: bools.has("--json"),
|
|
924
|
+
help: false,
|
|
925
|
+
};
|
|
926
|
+
}
|
|
927
|
+
/** Exported for tests: parse `continue-child` arguments. */
|
|
928
|
+
export function parseContinueChildArgs(argv) {
|
|
929
|
+
return parseContinuationArgs(argv, CONTINUE_CHILD_VALUE_FLAGS);
|
|
930
|
+
}
|
|
931
|
+
/** Exported for tests: parse `continuation-receipt` arguments. */
|
|
932
|
+
export function parseContinuationReceiptArgs(argv) {
|
|
933
|
+
return parseContinuationArgs(argv, CONTINUATION_RECEIPT_VALUE_FLAGS);
|
|
934
|
+
}
|
|
935
|
+
function receiptSummary(receipt) {
|
|
936
|
+
return {
|
|
937
|
+
receipt_id: receipt.receipt_id,
|
|
938
|
+
request_id: receipt.request_id,
|
|
939
|
+
gate: receipt.gate,
|
|
940
|
+
job_type: receipt.job_type,
|
|
941
|
+
continuation_job_id: receipt.continuation_job_id,
|
|
942
|
+
original_job_id: receipt.original_job_id,
|
|
943
|
+
source_park_generation: receipt.source_park_generation,
|
|
944
|
+
resulting_park_generation: receipt.resulting_park_generation,
|
|
945
|
+
committed_at: receipt.committed_at,
|
|
946
|
+
runtime_outcome: receipt.runtime_outcome,
|
|
947
|
+
};
|
|
948
|
+
}
|
|
949
|
+
function currentSummary(current) {
|
|
950
|
+
if (current === null)
|
|
951
|
+
return null;
|
|
952
|
+
return {
|
|
953
|
+
status: current.status,
|
|
954
|
+
parked: current.parked,
|
|
955
|
+
parked_gate: current.parked_gate,
|
|
956
|
+
park_generation: current.park_generation,
|
|
957
|
+
parked_age_seconds: current.parked_age_seconds,
|
|
958
|
+
needs_human_reason: current.needs_human_reason,
|
|
959
|
+
unresolved_obligations: current.unresolved_obligations,
|
|
960
|
+
continuations_used: current.continuations_used,
|
|
961
|
+
continuation_limit: current.continuation_limit,
|
|
962
|
+
continuation_available: current.continuation_available,
|
|
963
|
+
continuation_unavailable_reason: current.continuation_unavailable_reason,
|
|
964
|
+
};
|
|
965
|
+
}
|
|
966
|
+
function currentLine(current) {
|
|
967
|
+
if (!current.parked) {
|
|
968
|
+
return `Current: status ${current.status} (not parked).`;
|
|
969
|
+
}
|
|
970
|
+
const age = current.parked_age_seconds === null ? "" : `, parked ${current.parked_age_seconds}s`;
|
|
971
|
+
const available = current.continuation_available
|
|
972
|
+
? "continuation available"
|
|
973
|
+
: `continuation unavailable (${current.continuation_unavailable_reason ?? "unknown"})`;
|
|
974
|
+
return (`Current: parked at gate ${current.parked_gate} (episode ${current.park_generation}${age}); ` +
|
|
975
|
+
`${available}.`);
|
|
976
|
+
}
|
|
977
|
+
/** Exported for tests: render a `continue-child` result. Returns the exit code. */
|
|
978
|
+
export function renderContinueChildResult(result, target, json) {
|
|
979
|
+
const label = `Ticket ${target.ticketKey} (run ${target.epicRunId})`;
|
|
980
|
+
if (result.kind === "decided") {
|
|
981
|
+
const r = result.result;
|
|
982
|
+
const ok = r.outcome === "committed" || r.outcome === "already_continued";
|
|
983
|
+
if (json) {
|
|
984
|
+
console.log(JSON.stringify({
|
|
985
|
+
ok,
|
|
986
|
+
kind: r.outcome,
|
|
987
|
+
reason_code: r.reason_code,
|
|
988
|
+
explanation: r.explanation,
|
|
989
|
+
request_id: result.requestId,
|
|
990
|
+
replayed: r.replayed,
|
|
991
|
+
retryable: r.retryable,
|
|
992
|
+
reference_reused: result.reusedReference,
|
|
993
|
+
receipt: r.receipt === null ? null : receiptSummary(r.receipt),
|
|
994
|
+
current: currentSummary(r.current),
|
|
995
|
+
}));
|
|
996
|
+
return ok ? 0 : 1;
|
|
997
|
+
}
|
|
998
|
+
const receipt = r.receipt;
|
|
999
|
+
switch (r.outcome) {
|
|
1000
|
+
case "committed":
|
|
1001
|
+
console.log(`${label}: continuation COMMITTED${r.replayed ? " (replayed — already committed earlier)" : ""} — ` +
|
|
1002
|
+
`job #${receipt?.continuation_job_id} (${receipt?.job_type}) dispatched for park episode ` +
|
|
1003
|
+
`${receipt?.source_park_generation}; receipt ${receipt?.receipt_id}. This proves the dispatch, ` +
|
|
1004
|
+
"not that the work has finished.");
|
|
1005
|
+
return 0;
|
|
1006
|
+
case "already_continued":
|
|
1007
|
+
console.log(`${label}: this park episode was ALREADY CONTINUED by another request — job ` +
|
|
1008
|
+
`#${receipt?.continuation_job_id}, receipt ${receipt?.receipt_id}. Nothing was dispatched ` +
|
|
1009
|
+
"for this request.");
|
|
1010
|
+
return 0;
|
|
1011
|
+
case "stale_historical_episode":
|
|
1012
|
+
console.error(`${label}: STALE HISTORICAL EPISODE — receipt ${receipt?.receipt_id} (job ` +
|
|
1013
|
+
`#${receipt?.continuation_job_id}) belongs to park episode ${receipt?.source_park_generation}, ` +
|
|
1014
|
+
"which is history. The child has parked again and was NOT resumed by it.");
|
|
1015
|
+
if (r.current)
|
|
1016
|
+
console.error(currentLine(r.current));
|
|
1017
|
+
return 1;
|
|
1018
|
+
case "unsupported":
|
|
1019
|
+
console.error(`${label}: continuation UNSUPPORTED (${r.reason_code}) — ${r.explanation}`);
|
|
1020
|
+
return 1;
|
|
1021
|
+
default:
|
|
1022
|
+
console.error(`${label}: continuation REFUSED (${r.reason_code}) — ${r.explanation}`);
|
|
1023
|
+
if (r.retryable)
|
|
1024
|
+
console.error("This refusal is retryable; nothing was written.");
|
|
1025
|
+
return 1;
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
if (result.kind === "refused_http") {
|
|
1029
|
+
if (json) {
|
|
1030
|
+
console.log(JSON.stringify({
|
|
1031
|
+
ok: false,
|
|
1032
|
+
kind: "refused",
|
|
1033
|
+
status: result.status,
|
|
1034
|
+
code: result.errorCode,
|
|
1035
|
+
request_id: result.requestId,
|
|
1036
|
+
}));
|
|
1037
|
+
return 1;
|
|
1038
|
+
}
|
|
1039
|
+
console.error(`${label}: request REJECTED before anything was written (status ${result.status}` +
|
|
1040
|
+
`${result.errorCode ? `, ${result.errorCode}` : ""}).`);
|
|
1041
|
+
return 1;
|
|
1042
|
+
}
|
|
1043
|
+
if (result.kind === "not_continuable") {
|
|
1044
|
+
if (json) {
|
|
1045
|
+
console.log(JSON.stringify({ ok: false, kind: "not_continuable", current: currentSummary(result.current) }));
|
|
1046
|
+
return 1;
|
|
1047
|
+
}
|
|
1048
|
+
console.error(`${label}: not continuable right now. Nothing was sent.`);
|
|
1049
|
+
console.error(currentLine(result.current));
|
|
1050
|
+
return 1;
|
|
1051
|
+
}
|
|
1052
|
+
if (result.kind === "unconfirmed") {
|
|
1053
|
+
if (json) {
|
|
1054
|
+
console.log(JSON.stringify({
|
|
1055
|
+
ok: false,
|
|
1056
|
+
kind: "unconfirmed",
|
|
1057
|
+
transport: result.transport,
|
|
1058
|
+
request_id: result.requestId,
|
|
1059
|
+
reference_reused: result.reusedReference,
|
|
1060
|
+
}));
|
|
1061
|
+
return 1;
|
|
1062
|
+
}
|
|
1063
|
+
console.error(`${label}: continuation UNCONFIRMED after ${result.transport} — the request may or may not have ` +
|
|
1064
|
+
`committed, and no receipt proves it yet (request ${result.requestId}).`);
|
|
1065
|
+
console.error(`Look it up: epic-implementer continuation-receipt --epic-run-id ${target.epicRunId} ` +
|
|
1066
|
+
`--ticket-key ${target.ticketKey}. Re-running continue-child reuses this same request; it ` +
|
|
1067
|
+
"never starts a new one.");
|
|
1068
|
+
return 1;
|
|
1069
|
+
}
|
|
1070
|
+
if (json) {
|
|
1071
|
+
console.log(JSON.stringify({ ok: false, kind: "unavailable", message: result.message }));
|
|
1072
|
+
return 1;
|
|
1073
|
+
}
|
|
1074
|
+
console.error(`${label}: could not continue — ${result.message}. Nothing was sent.`);
|
|
1075
|
+
return 1;
|
|
1076
|
+
}
|
|
1077
|
+
/** Exported for tests: render a `continuation-receipt` lookup. */
|
|
1078
|
+
export function renderContinuationReceiptResult(result, target, json) {
|
|
1079
|
+
const label = `Ticket ${target.ticketKey} (run ${target.epicRunId})`;
|
|
1080
|
+
if (result.kind === "unavailable") {
|
|
1081
|
+
if (json) {
|
|
1082
|
+
console.log(JSON.stringify({ ok: false, kind: "unavailable", message: result.message }));
|
|
1083
|
+
}
|
|
1084
|
+
else {
|
|
1085
|
+
console.error(`${label}: could not read — ${result.message}.`);
|
|
1086
|
+
}
|
|
1087
|
+
return 1;
|
|
1088
|
+
}
|
|
1089
|
+
const status = result.status;
|
|
1090
|
+
if (json) {
|
|
1091
|
+
console.log(JSON.stringify({
|
|
1092
|
+
ok: true,
|
|
1093
|
+
kind: result.kind,
|
|
1094
|
+
request_id: result.requestId,
|
|
1095
|
+
outcome: status.outcome,
|
|
1096
|
+
receipt: status.receipt === null ? null : receiptSummary(status.receipt),
|
|
1097
|
+
current: currentSummary(status.current),
|
|
1098
|
+
}));
|
|
1099
|
+
return 0;
|
|
1100
|
+
}
|
|
1101
|
+
if (result.kind === "found" && status.receipt !== null) {
|
|
1102
|
+
const r = status.receipt;
|
|
1103
|
+
console.log(`${label}: receipt ${r.receipt_id} — continuation COMMITTED at ${r.committed_at}: job ` +
|
|
1104
|
+
`#${r.continuation_job_id} (${r.job_type}, runtime ${r.runtime_outcome ?? "unknown"}) for park ` +
|
|
1105
|
+
`episode ${r.source_park_generation}.`);
|
|
1106
|
+
if (status.outcome === "stale_historical_episode") {
|
|
1107
|
+
console.log("That episode is history: the child has parked again since.");
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
else if (result.requestId !== null) {
|
|
1111
|
+
console.log(`${label}: no receipt for request ${result.requestId} — its commit is NOT proven (this is not a refusal).`);
|
|
1112
|
+
}
|
|
1113
|
+
else {
|
|
1114
|
+
console.log(`${label}: no pending local continuation request.`);
|
|
1115
|
+
}
|
|
1116
|
+
console.log(currentLine(status.current));
|
|
1117
|
+
return 0;
|
|
1118
|
+
}
|
|
1119
|
+
export async function runContinueChildCommand(argv) {
|
|
1120
|
+
try {
|
|
1121
|
+
const parsed = parseContinueChildArgs(argv);
|
|
1122
|
+
if (parsed.help) {
|
|
1123
|
+
console.log(CONTINUE_CHILD_USAGE);
|
|
1124
|
+
return 0;
|
|
1125
|
+
}
|
|
1126
|
+
const access = await resolveRecoveryAccess(parsed.json);
|
|
1127
|
+
if (!access)
|
|
1128
|
+
return 1;
|
|
1129
|
+
const { continueChildOnce } = await import("./recovery-operations.js");
|
|
1130
|
+
const result = await continueChildOnce(access, {
|
|
1131
|
+
epicRunId: parsed.epicRunId,
|
|
1132
|
+
ticketKey: parsed.ticketKey,
|
|
1133
|
+
...(parsed.reason ? { reason: parsed.reason } : {}),
|
|
1134
|
+
});
|
|
1135
|
+
return renderContinueChildResult(result, parsed, parsed.json);
|
|
1136
|
+
}
|
|
1137
|
+
catch (error) {
|
|
1138
|
+
console.error(`Error: ${toConductorErrorEnvelope(error).message}`);
|
|
1139
|
+
return 1;
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
export async function runContinuationReceiptCommand(argv) {
|
|
1143
|
+
try {
|
|
1144
|
+
const parsed = parseContinuationReceiptArgs(argv);
|
|
1145
|
+
if (parsed.help) {
|
|
1146
|
+
console.log(CONTINUATION_RECEIPT_USAGE);
|
|
1147
|
+
return 0;
|
|
1148
|
+
}
|
|
1149
|
+
const access = await resolveRecoveryAccess(parsed.json);
|
|
1150
|
+
if (!access)
|
|
1151
|
+
return 1;
|
|
1152
|
+
const { lookupContinuationReceipt } = await import("./recovery-operations.js");
|
|
1153
|
+
const result = await lookupContinuationReceipt(access, {
|
|
1154
|
+
epicRunId: parsed.epicRunId,
|
|
1155
|
+
ticketKey: parsed.ticketKey,
|
|
1156
|
+
...(parsed.requestId ? { requestId: parsed.requestId } : {}),
|
|
1157
|
+
});
|
|
1158
|
+
return renderContinuationReceiptResult(result, parsed, parsed.json);
|
|
1159
|
+
}
|
|
1160
|
+
catch (error) {
|
|
1161
|
+
console.error(`Error: ${toConductorErrorEnvelope(error).message}`);
|
|
1162
|
+
return 1;
|
|
1163
|
+
}
|
|
1164
|
+
}
|