@numa-tech/numa 1.13.2 → 1.13.4

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.
Files changed (46) hide show
  1. package/README.md +75 -10
  2. package/dist/application-onboarding/client.d.ts +6 -1
  3. package/dist/application-onboarding/client.js +11 -1
  4. package/dist/application-onboarding/client.js.map +1 -1
  5. package/dist/application-onboarding/commands.d.ts +23 -1
  6. package/dist/application-onboarding/commands.js +278 -30
  7. package/dist/application-onboarding/commands.js.map +1 -1
  8. package/dist/application-onboarding/schemas.d.ts +109 -0
  9. package/dist/application-onboarding/schemas.js +17 -4
  10. package/dist/application-onboarding/schemas.js.map +1 -1
  11. package/dist/command-catalog.js +134 -13
  12. package/dist/command-catalog.js.map +1 -1
  13. package/dist/gitops/client.d.ts +41 -0
  14. package/dist/gitops/client.js +13 -1
  15. package/dist/gitops/client.js.map +1 -1
  16. package/dist/gitops/commands.d.ts +24 -1
  17. package/dist/gitops/commands.js +64 -4
  18. package/dist/gitops/commands.js.map +1 -1
  19. package/dist/gitops/schemas.d.ts +50 -0
  20. package/dist/gitops/schemas.js +37 -0
  21. package/dist/gitops/schemas.js.map +1 -1
  22. package/dist/jenkins-gitops-rollouts/client.d.ts +197 -0
  23. package/dist/jenkins-gitops-rollouts/client.js +126 -0
  24. package/dist/jenkins-gitops-rollouts/client.js.map +1 -0
  25. package/dist/jenkins-gitops-rollouts/commands.d.ts +50 -0
  26. package/dist/jenkins-gitops-rollouts/commands.js +358 -0
  27. package/dist/jenkins-gitops-rollouts/commands.js.map +1 -0
  28. package/dist/jenkins-gitops-rollouts/schemas.d.ts +217 -0
  29. package/dist/jenkins-gitops-rollouts/schemas.js +131 -0
  30. package/dist/jenkins-gitops-rollouts/schemas.js.map +1 -0
  31. package/dist/jenkins-jobs/commands.d.ts +2 -0
  32. package/dist/jenkins-jobs/commands.js +33 -1
  33. package/dist/jenkins-jobs/commands.js.map +1 -1
  34. package/dist/publications/commands.js +24 -1
  35. package/dist/publications/commands.js.map +1 -1
  36. package/dist/repositories/commands.js +23 -1
  37. package/dist/repositories/commands.js.map +1 -1
  38. package/package.json +4 -4
  39. package/skills/numa-create-application/SKILL.md +163 -0
  40. package/skills/numa-create-application/agents/openai.yaml +4 -0
  41. package/skills/numa-create-application/evals/evals.json +75 -0
  42. package/skills/numa-create-application/references/checklist.md +130 -0
  43. package/skills/numa-create-application/references/inference-rules.md +69 -0
  44. package/skills/numa-create-application/references/numa-cli.md +232 -0
  45. package/skills/numa-create-application/references/promote-workflows.md +134 -0
  46. package/skills/numa-create-application/scripts/inspect-project.mjs +243 -0
@@ -4,7 +4,7 @@ import { loadAppConfig } from "../app-config.js";
4
4
  import { loadConfig } from "../config.js";
5
5
  import { registerApplicationCandidateCommands } from "../application-candidates/commands.js";
6
6
  import { SourceRepositoryClient } from "../repositories/client.js";
7
- import { publicationScanSummary, scanProjectPublication } from "../publications/scanner.js";
7
+ import { scanProjectPublication } from "../publications/scanner.js";
8
8
  import { OnboardingClient } from "./client.js";
9
9
  import { OnboardingError, sanitizeOnboardingOutput } from "./errors.js";
10
10
  import { OnboardingModeSchema, OnboardingEventOutputSchema, OnboardingFirstBuildActionRequestSchema, OnboardingJenkinsBindingActionRequestSchema, OnboardingObserverScopeActionRequestSchema, OnboardingPublicationPlanActionRequestSchema, OnboardingSessionStateSchema, OnboardingSpecSchema, ScmProviderSchema } from "./schemas.js";
@@ -189,7 +189,12 @@ export function scmCredentialUpdate(version, authType, credential) {
189
189
  }
190
190
  function scmConnectionLine(connection) {
191
191
  const enabled = Object.entries(connection.capabilities).filter(([, value]) => value).map(([key]) => key);
192
- return `${connection.code}\t${connection.provider}\t${connection.health}\t${connection.organizationKey}\t${enabled.join(",") || "no-capabilities"}`;
192
+ const codeupCloneReadiness = connection.provider === "CODEUP"
193
+ ? `\tcodeup-clone:${connection.codeupCloneCredentialConfigured == null
194
+ ? "unknown"
195
+ : connection.codeupCloneCredentialConfigured ? "ready" : "missing"}`
196
+ : "";
197
+ return `${connection.code}\t${connection.provider}\t${connection.health}\t${connection.organizationKey}\t${enabled.join(",") || "no-capabilities"}${codeupCloneReadiness}`;
193
198
  }
194
199
  function repositoryLine(repository) {
195
200
  return `${repository.externalId}\t${repository.fullName ?? repository.name}\t${repository.defaultBranch ?? "-"}\t${repository.headRevision ?? "-"}${repository.archived ? "\tARCHIVED" : ""}`;
@@ -290,6 +295,9 @@ export async function executeOnboardingSpec(client, spec, options = {}) {
290
295
  if (!options.apply && options.idempotencyKey) {
291
296
  throw new OnboardingError("--idempotency-key is only valid with --apply.", "onboarding_input_invalid");
292
297
  }
298
+ if (spec.schemaVersion === 3 && spec.projectStrategy === "PUBLISH_PROJECT" && !options.sessionNo) {
299
+ throw new OnboardingError("schemaVersion=3 PUBLISH_PROJECT must reuse the session created by `numa app begin`; provide --session <session-no>.", "onboarding_session_required");
300
+ }
293
301
  const key = options.apply ? idempotencyKey(options.idempotencyKey) : undefined;
294
302
  const draftSession = options.sessionNo
295
303
  ? await client.getSession(options.sessionNo)
@@ -315,9 +323,17 @@ export async function executeOnboardingSpec(client, spec, options = {}) {
315
323
  if (!planHash) {
316
324
  throw new OnboardingError("The server did not return a plan hash; apply was not submitted.", "onboarding_plan_missing");
317
325
  }
318
- const task = await client.apply(session.sessionNo, { planHash, idempotencyKey: key });
326
+ const attempt = await executeOnboardingSessionMutation(client, session.sessionNo, () => client.apply(session.sessionNo, { planHash, idempotencyKey: key }), (event) => hasOnboardingApplyRecoveryEvidence(event, planHash));
327
+ if (!attempt.result) {
328
+ return {
329
+ session: attempt.inspection.session,
330
+ preflight,
331
+ idempotencyKey: key,
332
+ recoveredFromStatus: true
333
+ };
334
+ }
319
335
  session = await client.getSession(session.sessionNo);
320
- return { session, preflight, task, idempotencyKey: key };
336
+ return { session, preflight, task: attempt.result, idempotencyKey: key, recoveredFromStatus: false };
321
337
  }
322
338
  function defaultSleep(milliseconds, signal) {
323
339
  return new Promise((resolve, reject) => {
@@ -367,7 +383,7 @@ export async function inspectOnboardingSession(client, sessionNo, afterSequence
367
383
  .filter((event) => event.sequenceNo > afterSequence)
368
384
  .map(publicOnboardingEvent)
369
385
  .sort((left, right) => left.sequenceNo - right.sequenceNo);
370
- const lastSequence = events.reduce((maximum, event) => Math.max(maximum, event.sequenceNo), afterSequence);
386
+ const lastSequence = events.reduce((maximum, event) => Math.max(maximum, event.sequenceNo), Math.max(afterSequence, eventPage.nextSequence ?? afterSequence));
371
387
  return {
372
388
  session,
373
389
  events,
@@ -375,6 +391,192 @@ export async function inspectOnboardingSession(client, sessionNo, afterSequence
375
391
  execution: { ...onboardingExecutionView(session, events), lastSequence }
376
392
  };
377
393
  }
394
+ function mutationResultMayBeUnknown(error) {
395
+ if (error instanceof OnboardingError) {
396
+ return error.retryable
397
+ || (error.status != null && error.status >= 500)
398
+ || (error.code === "onboarding_invalid_response" && error.status != null
399
+ && error.status >= 200 && error.status < 300);
400
+ }
401
+ return error instanceof TypeError;
402
+ }
403
+ export function hasOnboardingApplyRecoveryEvidence(event, planHash) {
404
+ return event.output?.taskNo != null && event.output.planHash === planHash;
405
+ }
406
+ export function hasJenkinsBindingRecoveryEvidence(_event, _bindingId) {
407
+ // jenkinsBindingId alone does not prove CONFIRMED status or the requested
408
+ // optimistic-lock version; unknown confirmations therefore fail closed.
409
+ return false;
410
+ }
411
+ export function hasObserverScopeRecoveryEvidence(_event) {
412
+ // The reviewed event projection currently lacks serviceBindingId and exact
413
+ // observer coordinates, so no event can prove this request's full intent.
414
+ return false;
415
+ }
416
+ export function hasFirstBuildRecoveryEvidence(_event, _input) {
417
+ // Aggregated task output can repeat an older decision on an unrelated new
418
+ // event and does not carry the action idempotency key or full build intent.
419
+ return false;
420
+ }
421
+ export async function executeOnboardingSessionMutation(client, sessionNo, mutate, hasNewEvidence) {
422
+ // A pre-write cursor is required: without it, a matching field or broad
423
+ // session state that predates this request could be misreported as recovery.
424
+ const baseline = await inspectOnboardingSession(client, sessionNo);
425
+ try {
426
+ return { result: await mutate(), recoveredFromStatus: false };
427
+ }
428
+ catch (error) {
429
+ if (!mutationResultMayBeUnknown(error))
430
+ throw error;
431
+ let inspection;
432
+ try {
433
+ inspection = await inspectOnboardingSession(client, sessionNo, baseline.lastSequence);
434
+ }
435
+ catch {
436
+ // Preserve the mutation error below; a failed read must never trigger a write replay.
437
+ }
438
+ if (inspection && inspection.events.some((event) => event.sequenceNo > baseline.lastSequence && hasNewEvidence(event, baseline, inspection))) {
439
+ return { inspection, recoveredFromStatus: true };
440
+ }
441
+ const original = error instanceof OnboardingError ? error : undefined;
442
+ throw new OnboardingError("The mutation result is unknown. Status recovery was attempted; inspect the same session before reusing the original idempotency key.", "onboarding_result_unknown", original?.status, original?.requestId, true, {
443
+ sessionNo,
444
+ recoveryCommand: `numa app inspect ${sessionNo} --json`,
445
+ originalCode: original?.code ?? "network_error"
446
+ }, original?.retryAfterSeconds);
447
+ }
448
+ }
449
+ function assertPublicationSessionBinding(result, sessionNo, resolved) {
450
+ if (result.session.sessionNo !== sessionNo
451
+ || result.publicationPlan.consumerType !== "APPLICATION_ONBOARDING"
452
+ || result.publicationPlan.consumerRef !== sessionNo
453
+ || (resolved && !result.replayed)) {
454
+ throw new OnboardingError("The publication response is not a replay bound to the same APPLICATION_ONBOARDING session.", "onboarding_invalid_response", 200);
455
+ }
456
+ }
457
+ function publicationResolutionPending(error) {
458
+ return error instanceof OnboardingError
459
+ && error.status === 404
460
+ && error.code === "ONBOARDING_PUBLICATION_PLAN_NOT_RESOLVED"
461
+ && error.retryable;
462
+ }
463
+ export async function executePublicationAttachmentWithRecovery(client, sessionNo, clientRequestId, mutate, options = {}) {
464
+ try {
465
+ const result = await mutate();
466
+ assertPublicationSessionBinding(result, sessionNo, false);
467
+ return { result, recoveredFromStatus: false };
468
+ }
469
+ catch (error) {
470
+ if (!mutationResultMayBeUnknown(error))
471
+ throw error;
472
+ const delaysMs = options.delaysMs ?? [0, 200, 500, 1_000];
473
+ const sleep = options.sleep ?? ((milliseconds) => defaultSleep(milliseconds));
474
+ let resolutionCode;
475
+ for (const delayMs of delaysMs) {
476
+ if (delayMs > 0)
477
+ await sleep(delayMs);
478
+ try {
479
+ const result = await client.resolvePublicationPlan(sessionNo, clientRequestId);
480
+ assertPublicationSessionBinding(result, sessionNo, true);
481
+ return { result, recoveredFromStatus: true };
482
+ }
483
+ catch (resolutionError) {
484
+ resolutionCode = resolutionError instanceof OnboardingError ? resolutionError.code : "network_error";
485
+ if (publicationResolutionPending(resolutionError) || mutationResultMayBeUnknown(resolutionError))
486
+ continue;
487
+ break;
488
+ }
489
+ }
490
+ const original = error instanceof OnboardingError ? error : undefined;
491
+ throw new OnboardingError("The publication attachment result is unknown. Continue read-only resolution with the original session and client-request-id; do not replay the write based on this error.", "onboarding_publication_result_unknown", original?.status, original?.requestId, true, {
492
+ sessionNo,
493
+ clientRequestId,
494
+ recoveryCommand: `numa app publication resolve ${sessionNo} --client-request-id ${clientRequestId} --json`,
495
+ originalCode: original?.code ?? "network_error",
496
+ resolutionCode
497
+ }, original?.retryAfterSeconds);
498
+ }
499
+ }
500
+ function sessionSummary(session) {
501
+ return {
502
+ sessionNo: session.sessionNo,
503
+ mode: session.mode,
504
+ state: session.state,
505
+ currentStep: session.currentStep,
506
+ revision: session.revision,
507
+ planHash: session.planHash
508
+ };
509
+ }
510
+ function recoveredMutationPayload(inspection, idempotencyKeyValue) {
511
+ return {
512
+ ok: true,
513
+ session: sessionSummary(inspection.session),
514
+ execution: inspection.execution,
515
+ recoveredFromStatus: true,
516
+ mutationResponseReceived: false,
517
+ idempotencyKey: idempotencyKeyValue,
518
+ recovery: { inspectCommand: `numa app inspect ${inspection.session.sessionNo} --json` }
519
+ };
520
+ }
521
+ export function onboardingPublicationAttachmentView(result, source, idempotencyKeyValue, recoveredFromStatus = false) {
522
+ const plan = result.publicationPlan;
523
+ return {
524
+ ok: true,
525
+ session: sessionSummary(result.session),
526
+ publicationPlan: {
527
+ planNo: plan.planNo,
528
+ state: plan.state,
529
+ planHash: plan.planHash,
530
+ repositoryKey: plan.repositoryKey,
531
+ consumerType: plan.consumerType,
532
+ consumerRef: plan.consumerRef,
533
+ publicationMode: plan.publicationMode,
534
+ branch: plan.branch,
535
+ expectedHead: plan.expectedHead,
536
+ fileCount: plan.manifest.length,
537
+ warningCount: plan.warnings.length,
538
+ blockerCount: plan.blockers.length,
539
+ expiresAt: plan.expiresAt
540
+ },
541
+ scan: {
542
+ schemaVersion: source.schemaVersion,
543
+ sourceRevision: source.sourceRevision,
544
+ sourceTreeDigest: source.sourceTreeDigest,
545
+ clean: source.clean,
546
+ fileCount: source.fileCount,
547
+ totalBytes: source.totalBytes,
548
+ executableFileCount: source.executableFileCount
549
+ },
550
+ replayed: result.replayed,
551
+ recoveredFromStatus,
552
+ idempotencyKey: idempotencyKeyValue
553
+ };
554
+ }
555
+ export function onboardingPublicationResolutionView(result, clientRequestId) {
556
+ const plan = result.publicationPlan;
557
+ return {
558
+ ok: true,
559
+ session: sessionSummary(result.session),
560
+ publicationPlan: {
561
+ planNo: plan.planNo,
562
+ state: plan.state,
563
+ planHash: plan.planHash,
564
+ repositoryKey: plan.repositoryKey,
565
+ consumerType: plan.consumerType,
566
+ consumerRef: plan.consumerRef,
567
+ publicationMode: plan.publicationMode,
568
+ branch: plan.branch,
569
+ expectedHead: plan.expectedHead,
570
+ fileCount: plan.manifest.length,
571
+ warningCount: plan.warnings.length,
572
+ blockerCount: plan.blockers.length,
573
+ expiresAt: plan.expiresAt
574
+ },
575
+ replayed: result.replayed,
576
+ recoveredFromStatus: true,
577
+ clientRequestId
578
+ };
579
+ }
378
580
  export async function executeOnboardingAction(client, sessionNo, action) {
379
581
  await client[action](sessionNo);
380
582
  return inspectOnboardingSession(client, sessionNo);
@@ -412,9 +614,18 @@ export function registerApplicationOnboardingCommands(program, dependencies = {}
412
614
  核心约束:
413
615
  所有客户端共享服务端 sessionNo、revision、planHash 和事件 sequence。
414
616
  --plan 只预检;--apply 才提交副作用,且服务端决定真实状态。
415
- 结果不明时用相同 idempotency-key 重试或 inspect/resume,禁止新建重复会话。
617
+ 结果不明时先用 inspect/status/resolve 只读恢复;只有具体命令的服务端明确支持 replay 才可以原 key 重试,禁止新建重复会话。
416
618
  `);
417
619
  registerApplicationCandidateCommands(app);
620
+ addExamples(app.command("capabilities")
621
+ .description("读取服务端 Application Onboarding 能力自述;命令存在不代表端到端能力可用")
622
+ .action(async (_options, command) => {
623
+ const capabilities = await client().getCapabilities();
624
+ await output(command, { ok: true, capabilities }, JSON.stringify(stableOutput(capabilities), null, 2));
625
+ }), ["numa app capabilities --json"], [
626
+ "与 `numa commands --json`、Repository capability 及各 child control plane 的只读能力共同形成运行时能力矩阵。",
627
+ "HTTP 501 表示服务端能力未发布;不得因为本地命令存在就声称可执行。"
628
+ ]);
418
629
  addExamples(app.command("options <type>")
419
630
  .description("按名称查询应用所需的团队、系统、业务域或环境 ID")
420
631
  .option("--search <text>", "按 code 或名称搜索")
@@ -701,11 +912,13 @@ export function registerApplicationOnboardingCommands(program, dependencies = {}
701
912
  idempotencyKey: options.idempotencyKey
702
913
  });
703
914
  const execution = onboardingExecutionView(result.session);
704
- const text = result.task
705
- ? `入驻执行已提交:\n${executionText(result.session)}\nidempotency-key: ${result.idempotencyKey}`
706
- : result.preflight
707
- ? `预检完成: ${sessionLine(result.session)}\nplan: ${result.preflight.planHash ?? "-"}\nblocking: ${result.preflight.blockingErrors.length}`
708
- : `草稿已保存: ${sessionLine(result.session)}`;
915
+ const text = result.recoveredFromStatus
916
+ ? `写响应未知,已从同一会话状态恢复:\n${executionText(result.session)}\nidempotency-key: ${result.idempotencyKey}`
917
+ : result.task
918
+ ? `入驻执行已提交:\n${executionText(result.session)}\nidempotency-key: ${result.idempotencyKey}`
919
+ : result.preflight
920
+ ? `预检完成: ${sessionLine(result.session)}\nplan: ${result.preflight.planHash ?? "-"}\nblocking: ${result.preflight.blockingErrors.length}`
921
+ : `草稿已保存: ${sessionLine(result.session)}`;
709
922
  await output(command, { ok: true, ...result, execution }, text);
710
923
  }), [
711
924
  "numa app onboard",
@@ -719,6 +932,20 @@ export function registerApplicationOnboardingCommands(program, dependencies = {}
719
932
  ]);
720
933
  const publicationAction = app.command("publication")
721
934
  .description("为 schemaVersion=3 会话附加受控本地源码 Publication 计划");
935
+ addExamples(publicationAction.command("resolve <session-no>")
936
+ .description("按原 clientRequestId 只读恢复 publication attach 的确定结果")
937
+ .requiredOption("--client-request-id <id>", "原 attach 使用的稳定 child request ID")
938
+ .action(async (sessionNo, options, command) => {
939
+ const clientRequestId = stableActionText(options.clientRequestId, "client-request-id", 160);
940
+ const result = await client().resolvePublicationPlan(sessionNo, clientRequestId);
941
+ assertPublicationSessionBinding(result, sessionNo, true);
942
+ await output(command, onboardingPublicationResolutionView(result, clientRequestId), `${result.publicationPlan.planNo}\t${result.publicationPlan.state}\t${result.publicationPlan.planHash}\trecovered-from-status`);
943
+ }), [
944
+ "numa app publication resolve <session-no> --client-request-id onboarding-code-index-publication --json"
945
+ ], [
946
+ "固定 GET resolve,不上传 bundle、不重放 attach;404 ONBOARDING_PUBLICATION_PLAN_NOT_RESOLVED 可安全稍后重查。",
947
+ "compact 输出不含 manifest path、文件内容、完整 session draft/plan 或 SCM credential。"
948
+ ]);
722
949
  addExamples(publicationAction.command("attach <session-no>")
723
950
  .description("扫描 clean Git HEAD 并创建绑定当前 onboarding planHash 的 child plan;不写远端 Git")
724
951
  .requiredOption("--source <directory>", "本地 Git worktree 根目录")
@@ -729,12 +956,15 @@ export function registerApplicationOnboardingCommands(program, dependencies = {}
729
956
  .action(async (sessionNo, options, command) => {
730
957
  requireConfirmation(options.yes, "Attaching a project publication plan");
731
958
  const key = idempotencyKey(options.idempotencyKey, "publication attach");
732
- const clientRequestId = stableActionText(options.clientRequestId, "client-request-id", 200);
959
+ const clientRequestId = stableActionText(options.clientRequestId, "client-request-id", 160);
733
960
  if (key !== clientRequestId) {
734
961
  throw new OnboardingError("idempotency-key must exactly match client-request-id for publication attachment.", "onboarding_input_invalid");
735
962
  }
736
963
  const onboarding = client();
737
964
  const session = await onboarding.getSession(sessionNo);
965
+ if (session.state !== "WAITING_EXTERNAL" || session.currentStep !== "ATTACH_PUBLICATION_PLAN") {
966
+ throw new OnboardingError("Publication attachment requires the same parent session to be WAITING_EXTERNAL at ATTACH_PUBLICATION_PLAN.", "onboarding_publication_session_invalid", undefined, undefined, false, { sessionNo: session.sessionNo, state: session.state, currentStep: session.currentStep ?? null });
967
+ }
738
968
  const source = await scanPublication(options.source);
739
969
  const input = parsedActionInput(OnboardingPublicationPlanActionRequestSchema, {
740
970
  parentPlanHash: sha256(session.planHash, "session planHash"),
@@ -745,19 +975,14 @@ export function registerApplicationOnboardingCommands(program, dependencies = {}
745
975
  commitMessage: options.commitMessage,
746
976
  files: source.requestFiles
747
977
  });
748
- const result = await onboarding.attachPublicationPlan(sessionNo, input, key);
749
- await output(command, {
750
- ok: true,
751
- session: result.session,
752
- publicationPlan: result.publicationPlan,
753
- replayed: result.replayed,
754
- scan: publicationScanSummary(source),
755
- idempotencyKey: key
756
- }, `${result.publicationPlan.planNo}\t${result.publicationPlan.state}\t${result.publicationPlan.planHash}\treplayed:${result.replayed}`);
978
+ const attempt = await executePublicationAttachmentWithRecovery(onboarding, session.sessionNo, clientRequestId, () => onboarding.attachPublicationPlan(session.sessionNo, input, key));
979
+ const result = attempt.result;
980
+ await output(command, onboardingPublicationAttachmentView(result, source, key, attempt.recoveredFromStatus), `${result.publicationPlan.planNo}\t${result.publicationPlan.state}\t${result.publicationPlan.planHash}\t${attempt.recoveredFromStatus ? "recovered-from-status" : `replayed:${result.replayed}`}`);
757
981
  }), [
758
982
  "numa app publication attach <session-no> --source ./code-index --commit-message 'feat: publish code-index source' --client-request-id onboarding-code-index-publication --idempotency-key onboarding-code-index-publication --yes --json"
759
983
  ], [
760
- "只读取 clean Git HEAD;输出不含 sourceRoot、contentBase64 或 SCM credential。",
984
+ "只读取 clean Git HEAD;compact 输出不含 sourceRoot、manifest path、contentBase64、完整 session draft/plan 或 SCM credential。",
985
+ "POST 500/504、连接中断或成功响应无法解析时,CLI 以原 sessionNo/clientRequestId 轮询 GET resolve;绝不自动重放 attach。",
761
986
  "该动作只创建 child plan;onboarding worker 仍会等待显式执行和验证。"
762
987
  ]);
763
988
  const jenkinsBindingAction = app.command("jenkins-binding")
@@ -765,7 +990,7 @@ export function registerApplicationOnboardingCommands(program, dependencies = {}
765
990
  addExamples(jenkinsBindingAction.command("confirm <session-no>")
766
991
  .requiredOption("--binding-id <id>", "会话事件返回的候选 binding ID")
767
992
  .requiredOption("--expected-version <number>", "候选 binding 当前乐观锁版本")
768
- .requiredOption("--idempotency-key <key>", "结果未知时复用同一 key")
993
+ .requiredOption("--idempotency-key <key>", "稳定 key;写响应未知时仅用于取证,不自动重发")
769
994
  .option("--yes", "确认该 Jenkins job 与应用/tier 的绑定关系")
770
995
  .action(async (sessionNo, options, command) => {
771
996
  requireConfirmation(options.yes, "Confirming a Jenkins binding");
@@ -774,7 +999,13 @@ export function registerApplicationOnboardingCommands(program, dependencies = {}
774
999
  bindingId: positiveInteger(options.bindingId, "binding-id"),
775
1000
  expectedVersion: nonNegativeInteger(options.expectedVersion, "expected-version", 0)
776
1001
  });
777
- const result = await client().confirmJenkinsBinding(sessionNo, input, key);
1002
+ const onboarding = client();
1003
+ const attempt = await executeOnboardingSessionMutation(onboarding, sessionNo, () => onboarding.confirmJenkinsBinding(sessionNo, input, key), (event) => hasJenkinsBindingRecoveryEvidence(event, input.bindingId));
1004
+ if (!attempt.result) {
1005
+ await output(command, recoveredMutationPayload(attempt.inspection, key), `${input.bindingId}\trecovered-from-status\t${attempt.inspection.session.state}`);
1006
+ return;
1007
+ }
1008
+ const result = attempt.result;
778
1009
  await output(command, { ok: true, ...result, idempotencyKey: key }, `${result.binding.id}\t${result.binding.confirmationStatus}\tv${result.binding.version}\treplayed:${result.replayed}`);
779
1010
  }), [
780
1011
  "numa app jenkins-binding confirm <session-no> --binding-id 42 --expected-version 0 --idempotency-key confirm-code-index-binding --yes --json"
@@ -791,7 +1022,7 @@ export function registerApplicationOnboardingCommands(program, dependencies = {}
791
1022
  .requiredOption("--workload-kind <kind>", "最终 workload kind")
792
1023
  .requiredOption("--workload-name <name>", "最终 workload name")
793
1024
  .requiredOption("--expected-version <number>", "scope 当前版本;首次创建使用服务端事件给出的版本")
794
- .requiredOption("--idempotency-key <key>", "结果未知时复用同一 key")
1025
+ .requiredOption("--idempotency-key <key>", "稳定 key;写响应未知时仅用于取证,不自动重发")
795
1026
  .option("--yes", "确认精确运行态坐标")
796
1027
  .action(async (sessionNo, options, command) => {
797
1028
  requireConfirmation(options.yes, "Configuring an observer scope");
@@ -807,7 +1038,13 @@ export function registerApplicationOnboardingCommands(program, dependencies = {}
807
1038
  workloadName: stableActionText(options.workloadName, "workload-name"),
808
1039
  expectedVersion: nonNegativeInteger(options.expectedVersion, "expected-version", 0)
809
1040
  });
810
- const result = await client().configureObserverScope(sessionNo, input, key);
1041
+ const onboarding = client();
1042
+ const attempt = await executeOnboardingSessionMutation(onboarding, sessionNo, () => onboarding.configureObserverScope(sessionNo, input, key), hasObserverScopeRecoveryEvidence);
1043
+ if (!attempt.result) {
1044
+ await output(command, recoveredMutationPayload(attempt.inspection, key), `${String(attempt.inspection.execution.eventOutput.observerScopeNo)}\trecovered-from-status\t${attempt.inspection.session.state}`);
1045
+ return;
1046
+ }
1047
+ const result = attempt.result;
811
1048
  await output(command, { ok: true, ...result, idempotencyKey: key }, `${result.scope.scopeNo}\t${result.scope.status}\t${result.scope.evidenceFreshness}\t${result.scope.lastDriftState ?? "-"}`);
812
1049
  }), [
813
1050
  "numa app observer-scope configure <session-no> --service-binding-id 17 --kustomization-namespace flux-system --kustomization-name prd --helm-release-namespace prd --helm-release-name code-index --workload-namespace prd --workload-kind Deployment --workload-name code-index --expected-version 0 --idempotency-key scope-code-index-prd --yes --json"
@@ -820,7 +1057,7 @@ export function registerApplicationOnboardingCommands(program, dependencies = {}
820
1057
  .option("--parameter <KEY=VALUE>", "非敏感构建参数,可重复", collectParameter, [])
821
1058
  .option("--approve-id <id>", "生产 TRIGGER 的审批记录 ID")
822
1059
  .option("--production-reason <reason>", "生产 TRIGGER 的受审原因(至少10字符)")
823
- .requiredOption("--idempotency-key <key>", "结果未知时复用同一 key")
1060
+ .requiredOption("--idempotency-key <key>", "稳定 key;写响应未知时仅用于取证,不自动重发")
824
1061
  .option("--yes", "确认记录决定;TRIGGER 会真正提交 Jenkins build")
825
1062
  .action(async (sessionNo, options, command) => {
826
1063
  requireConfirmation(options.yes, "Recording the first-build decision");
@@ -833,7 +1070,13 @@ export function registerApplicationOnboardingCommands(program, dependencies = {}
833
1070
  productionReason: options.productionReason?.trim() || undefined
834
1071
  });
835
1072
  const key = idempotencyKey(options.idempotencyKey, "first-build decision");
836
- const result = await client().decideFirstBuild(sessionNo, input, key);
1073
+ const onboarding = client();
1074
+ const attempt = await executeOnboardingSessionMutation(onboarding, sessionNo, () => onboarding.decideFirstBuild(sessionNo, input, key), (event) => hasFirstBuildRecoveryEvidence(event, input));
1075
+ if (!attempt.result) {
1076
+ await output(command, recoveredMutationPayload(attempt.inspection, key), `${decision}\trecovered-from-status\t${attempt.inspection.session.state}`);
1077
+ return;
1078
+ }
1079
+ const result = attempt.result;
837
1080
  await output(command, { ok: true, ...result, idempotencyKey: key }, result.build
838
1081
  ? `${result.decision}\t${result.build.requestId}\t${result.build.state}\treplayed:${result.replayed}`
839
1082
  : `${result.decision}\tno-build\treplayed:${result.replayed}`);
@@ -860,11 +1103,16 @@ export function registerApplicationOnboardingCommands(program, dependencies = {}
860
1103
  throw new OnboardingError("Session has no server plan hash; run onboarding preflight first.", "onboarding_plan_missing");
861
1104
  }
862
1105
  const key = idempotencyKey(options.idempotencyKey);
863
- const task = await onboarding.apply(sessionNo, { planHash: before.planHash, idempotencyKey: key });
1106
+ const attempt = await executeOnboardingSessionMutation(onboarding, sessionNo, () => onboarding.apply(sessionNo, { planHash: before.planHash, idempotencyKey: key }), (event) => hasOnboardingApplyRecoveryEvidence(event, before.planHash));
1107
+ if (!attempt.result) {
1108
+ await output(command, recoveredMutationPayload(attempt.inspection, key), `写响应未知,已从同一会话状态恢复:\n${executionText(attempt.inspection.session, attempt.inspection.events)}\nidempotency-key: ${key}`);
1109
+ return;
1110
+ }
1111
+ const task = attempt.result;
864
1112
  const session = await onboarding.getSession(sessionNo);
865
1113
  await output(command, { ok: true, session, task, idempotencyKey: key, execution: onboardingExecutionView(session) }, `入驻执行已提交:\n${executionText(session)}\nidempotency-key: ${key}`);
866
1114
  }), ["numa app apply 8d15ee89-43f0-4378-a7aa-51f09c5b42fc --idempotency-key app-order-20260819 --json"], [
867
- "用于先 --plan apply 的安全自动化,也用于响应不明时以同一 key 重放。"
1115
+ "用于先 --plan 后的首次显式 apply;响应不明时只读 inspect/status,证据不足返回 onboarding_result_unknown,不要重发 apply。"
868
1116
  ]);
869
1117
  addExamples(app.command("inspect <session-no>")
870
1118
  .description("查询会话;watch 从最后 event sequence 持续恢复")