@itpay/cli 2.1.18 → 2.1.22

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 (75) hide show
  1. package/dist/src/commands/catalog.js +1 -0
  2. package/dist/src/commands/rail_catalog.js +160 -11
  3. package/dist/src/commands/services.js +289 -71
  4. package/dist/src/commands/skill.js +1 -1
  5. package/dist/src/main.js +54 -19
  6. package/dist/src/render/markdown.js +0 -3
  7. package/dist/src/state/account_auth.js +6 -2
  8. package/dist/src/state/cart_session.js +4 -1
  9. package/dist/src/state/config.js +1 -1
  10. package/dist/src/state/device_authority.js +69 -13
  11. package/dist/src/state/task_journal.js +2 -0
  12. package/docs/agent/buyer/cart-checkout.json +5 -4
  13. package/docs/agent/buyer/catalog-list.json +24 -6
  14. package/docs/agent/buyer/identity-and-sessions.json +15 -10
  15. package/docs/agent/buyer/install-and-setup.json +7 -6
  16. package/docs/agent/buyer/orders-refunds.json +4 -4
  17. package/docs/agent/buyer/payment-flow.json +3 -3
  18. package/docs/agent/buyer/purchased-content.json +17 -3
  19. package/docs/agent/buyer/quickstart.json +18 -11
  20. package/docs/agent/buyer/rail-booking.json +79 -167
  21. package/docs/agent/buyer/rail-fast-checkout.json +21 -43
  22. package/docs/agent/buyer/render-hosts.json +8 -5
  23. package/docs/agent/buyer/service-feedback.json +2 -1
  24. package/docs/cli-reference/agent-types.md +1 -1
  25. package/docs/cli-reference/commands/auth.md +2 -0
  26. package/docs/cli-reference/commands/buy.md +1 -1
  27. package/docs/cli-reference/commands/cart/add.md +1 -1
  28. package/docs/cli-reference/commands/cart/clear.md +1 -1
  29. package/docs/cli-reference/commands/cart/index.md +1 -1
  30. package/docs/cli-reference/commands/cart/next.md +1 -1
  31. package/docs/cli-reference/commands/cart/remove.md +1 -1
  32. package/docs/cli-reference/commands/cart/show.md +1 -1
  33. package/docs/cli-reference/commands/catalog/index.md +1 -1
  34. package/docs/cli-reference/commands/catalog/list.md +1 -1
  35. package/docs/cli-reference/commands/checkout.md +1 -1
  36. package/docs/cli-reference/commands/device.md +4 -2
  37. package/docs/cli-reference/commands/docs/index.md +1 -1
  38. package/docs/cli-reference/commands/docs/list.md +1 -1
  39. package/docs/cli-reference/commands/docs/search.md +1 -1
  40. package/docs/cli-reference/commands/docs/show.md +1 -1
  41. package/docs/cli-reference/commands/install.md +2 -2
  42. package/docs/cli-reference/commands/next.md +1 -1
  43. package/docs/cli-reference/commands/order.md +1 -1
  44. package/docs/cli-reference/commands/pay.md +1 -1
  45. package/docs/cli-reference/commands/readyz.md +1 -1
  46. package/docs/cli-reference/commands/refund/cancel.md +1 -1
  47. package/docs/cli-reference/commands/refund/create.md +1 -1
  48. package/docs/cli-reference/commands/refund/get.md +2 -2
  49. package/docs/cli-reference/commands/refund/index.md +1 -1
  50. package/docs/cli-reference/commands/refund/list.md +1 -1
  51. package/docs/cli-reference/commands/refund/watch.md +1 -1
  52. package/docs/cli-reference/commands/services/action.md +3 -3
  53. package/docs/cli-reference/commands/services/checkout.md +1 -1
  54. package/docs/cli-reference/commands/services/events.md +1 -1
  55. package/docs/cli-reference/commands/services/get.md +1 -1
  56. package/docs/cli-reference/commands/services/index.md +1 -1
  57. package/docs/cli-reference/commands/services/invoke.md +3 -11
  58. package/docs/cli-reference/commands/services/list.md +1 -1
  59. package/docs/cli-reference/commands/services/next.md +12 -10
  60. package/docs/cli-reference/commands/services/page.md +9 -7
  61. package/docs/cli-reference/commands/services/quote.md +1 -1
  62. package/docs/cli-reference/commands/services/read-result.md +8 -8
  63. package/docs/cli-reference/commands/services/run.md +2 -2
  64. package/docs/cli-reference/commands/services/start.md +1 -1
  65. package/docs/cli-reference/conventions.md +2 -2
  66. package/package.json +7 -5
  67. package/skills/itpay/SKILL.md +70 -186
  68. package/docs/reviews/agent-ux-handoff-2026-09-23/IMPLEMENTATION-EVIDENCE.md +0 -174
  69. package/docs/skill-bundle-rollout/01-mcp-authentication.md +0 -134
  70. package/docs/skill-bundle-rollout/02-platform-bundle-repositories.md +0 -210
  71. package/docs/skill-bundle-rollout/03-platform-publishing.md +0 -161
  72. package/docs/skill-bundle-rollout/04-first-wave-platforms.md +0 -70
  73. package/docs/skill-bundle-rollout/05-sync-operations.md +0 -62
  74. package/docs/skill-bundle-rollout/README.md +0 -151
  75. /package/bin/{itp → itp.mjs} +0 -0
@@ -24,7 +24,7 @@ export function runSkillShow(name, options = {}) {
24
24
  result: { skill: ITPAY_SKILL, content },
25
25
  instruction: agentType
26
26
  ? agentType === "workbuddy"
27
- ? "完整读取并遵守 Skill;把自己视为服务用户的代理,先理解用户要购买新服务、查看已购内容、查询订单还是处理退款,并先解释用户权益,再选择对应的第一条命令。保持 workbuddy 和同一 Node/CLI launcher。运行会持久化 ~/.itpay-v3 的 ItPay Bash 命令前,把 Shell 工具参数 dangerouslyDisableSandbox 设为 true;用户拒绝权限时停止,不要切换 Node、删除 identity 或处理 lock。"
27
+ ? "完整读取并遵守 Skill;按用户意图选择第一条命令。保持 workbuddy 和同一 CLI launcher,并为 ~/.itpay-v3 申请宿主允许的持久化权限;权限被拒时报告受阻操作,不删除 identity。"
28
28
  : `完整读取并遵守 Skill;把自己视为服务用户的代理,先理解用户意图和解释用户权益,再选择购买、已购内容、订单或退款对应的第一条命令。当前 Agent Type 是 ${agentType},后续命令保持不变。`
29
29
  : "完整读取并遵守 Skill;先如实选择当前运行环境对应的 Agent Type。",
30
30
  next: agentType
package/dist/src/main.js CHANGED
@@ -7,7 +7,7 @@ import { runServicesRun } from "./commands/services.js";
7
7
  // orchestrate; HTTP and rendering live in src/client and src/render.
8
8
  import { Command } from "commander";
9
9
  import { BackendOverrideError, CLI_VERSION, cliDistribution, loadConfig, cartSessionPath, newBackendClient } from "./state/config.js";
10
- import { DeviceAuthority, DeviceAuthorizationError, DeviceStateError } from "./state/device_authority.js";
10
+ import { DeviceAuthority, DeviceAuthorizationError, DeviceLockBusyError, DeviceStateError } from "./state/device_authority.js";
11
11
  import { CartSession } from "./state/cart_session.js";
12
12
  import { defaultHostForAgentType, normalizeHost, validateContext } from "./state/client_context.js";
13
13
  import { HttpError } from "./client/http.js";
@@ -116,6 +116,7 @@ function reportCLIError(error, contract) {
116
116
  const backendOverrideError = error instanceof BackendOverrideError ? error : undefined;
117
117
  const deviceError = error instanceof DeviceAuthorizationError ? error : undefined;
118
118
  const stateError = error instanceof DeviceStateError ? error : undefined;
119
+ const lockError = error instanceof DeviceLockBusyError ? error : undefined;
119
120
  const transportError = error instanceof HttpTransportError ? error : undefined;
120
121
  const httpRecovery = errorRecoveryActions(error).map((action) => ({
121
122
  command: action.command,
@@ -154,24 +155,30 @@ function reportCLIError(error, contract) {
154
155
  command: "itpay skill show itpay --json",
155
156
  reason: "读取 Device 状态边界;修复当前 Host 的持久写权限后重试原命令",
156
157
  }] : [];
158
+ const lockRecovery = lockError ? [{
159
+ command: "itpay device repair-lock --json",
160
+ reason: "检查锁持有者;只回收已退出进程留下的锁,不改变设备身份",
161
+ }] : [];
157
162
  const authorizationInstruction = stateError
158
163
  ? "当前运行环境无法写入 owner-only Device 状态;请保持同一 Node、CLI 和 Agent Type,在允许持久写入 ~/.itpay-v3 的执行环境中重试。不要手工创建 lock、删除 identity 或换运行时碰运气。"
159
- : error instanceof HttpError && error.code === "agent_device_session_required"
160
- ? "CLI 已自动续期并重试同一请求一次,仍被拒绝;停止重试,不要切换 Agent Type 或旋转身份。"
161
- : deviceError?.code === "agent_device_revoked"
162
- ? "Backend 已撤销当前 Device 登记;CLI 没有自动创建替代身份。停止重试并请用户或运营恢复登记。"
163
- : deviceKeyResettable && deviceError?.status === 500
164
- ? "服务端未能完成设备登记。先原样重试一次原命令:服务端会把已登记的同一公钥幂等挂回原设备。若仍返回 internal_error(Backend 未含该修复),执行 itpay device reset-key --confirm-key-reset 生成全新密钥后重试。"
165
- : deviceKeyResettable
166
- ? "服务端记录显示当前设备私钥已不再有效(已轮换或与既有登记冲突)。执行 itpay device reset-key --confirm-key-reset 生成全新密钥并重新登记。"
167
- : deviceError
168
- ? "Device 身份验证失败;停止重试,不要切换 Agent Type、删除状态或旋转私钥。"
169
- : undefined;
170
- if (contract || commandError || backendOverrideError) {
164
+ : lockError
165
+ ? "另一个进程仍在更新本地身份。执行 recovery 检查;若持有者仍活跃,等待后继续原任务,不要删除锁或重建身份。"
166
+ : error instanceof HttpError && error.code === "agent_device_session_required"
167
+ ? "CLI 已自动续期并重试同一请求一次,仍被拒绝;停止重试,不要切换 Agent Type 或旋转身份。"
168
+ : deviceError?.code === "agent_device_revoked"
169
+ ? "Backend 已撤销当前 Device 登记;CLI 没有自动创建替代身份。停止重试并请用户或运营恢复登记。"
170
+ : deviceKeyResettable && deviceError?.status === 500
171
+ ? "服务端未能完成设备登记。先原样重试一次原命令:服务端会把已登记的同一公钥幂等挂回原设备。若仍返回 internal_error(Backend 未含该修复),执行 itpay device reset-key --confirm-key-reset 生成全新密钥后重试。"
172
+ : deviceKeyResettable
173
+ ? "服务端记录显示当前设备私钥已不再有效(已轮换或与既有登记冲突)。执行 itpay device reset-key --confirm-key-reset 生成全新密钥并重新登记。"
174
+ : deviceError
175
+ ? "Device 身份验证失败;停止重试,不要切换 Agent Type、删除状态或旋转私钥。"
176
+ : undefined;
177
+ if (contract || commandError || backendOverrideError || lockError || stateError) {
171
178
  writeCommandEnvelope({
172
179
  status: "error",
173
180
  error: {
174
- code: incompatible ? "backend_contract_incompatible" : backendOverrideError?.code ?? commandError?.code ?? (error instanceof HttpError ? error.code : transportError?.code ?? stateError?.code ?? deviceError?.code ?? contract?.code ?? "command_failed"),
181
+ code: incompatible ? "backend_contract_incompatible" : backendOverrideError?.code ?? commandError?.code ?? (error instanceof HttpError ? error.code : transportError?.code ?? stateError?.code ?? lockError?.code ?? deviceError?.code ?? contract?.code ?? "command_failed"),
175
182
  message: error instanceof Error ? error.message : String(error),
176
183
  },
177
184
  ...(requiredCLIVersion ? {
@@ -219,7 +226,7 @@ function reportCLIError(error, contract) {
219
226
  ? "临时网络故障;CLI 已仅对可安全重放的操作完成有限自动重试,但仍未获得完整响应。按 recovery 查询同一资源的权威状态;不要创建替代 Checkout、Execution、Payment 或 Refund。"
220
227
  : "网络在完整响应前中断;当前写操作没有安全重放合同,因此 CLI 未自动重试。按 recovery 查询权威状态;不要原样重放或创建替代 Checkout、Execution、Payment 或 Refund。"
221
228
  : backendOverrideError
222
- ? "移除 ITPAY_BACKEND_URL 使用正式环境,或准确设置为 https://sandbox.itpay.ai。"
229
+ ? "移除 ITPAY_BACKEND_URL 使用正式环境,或按当前测试目标准确设置为 https://sandbox.itpay.ai 或 https://dev.itpay.ai。不要通过切换环境规避当前错误。"
223
230
  : commandError?.instruction ?? authorizationInstruction ?? contract?.instruction ?? "检查命令参数后重试。",
224
231
  next: null,
225
232
  recovery: incompatible
@@ -228,9 +235,9 @@ function reportCLIError(error, contract) {
228
235
  : []
229
236
  : backendInternal || providerConnectionUnavailable || providerTemporary || providerInputRejected || providerContractMismatch || providerRejected || capabilityInputInvalid
230
237
  ? []
231
- : backendOverrideError ? [] : commandError?.recovery ?? (stateError ? stateRecovery : deviceError ? deviceRecovery : identityRecovery ? httpRecovery : contract?.recovery ?? []),
238
+ : backendOverrideError ? [] : commandError?.recovery ?? (stateError ? stateRecovery : lockError ? lockRecovery : deviceError ? deviceRecovery : identityRecovery ? httpRecovery : contract?.recovery ?? []),
232
239
  }, {
233
- ...(contract?.jsonOutput !== undefined ? { jsonOutput: contract.jsonOutput } : backendOverrideError ? { jsonOutput: process.argv.includes("--json") } : {}),
240
+ ...(contract?.jsonOutput !== undefined ? { jsonOutput: contract.jsonOutput } : backendOverrideError || lockError || stateError ? { jsonOutput: process.argv.includes("--json") } : {}),
234
241
  output: (text) => { process.stderr.write(text); },
235
242
  });
236
243
  process.exitCode = 1;
@@ -331,6 +338,30 @@ for (const action of ["login", "status"]) {
331
338
  }
332
339
  // --- device ---------------------------------------------------------------
333
340
  const deviceCmd = program.command("device").description("Recover the current official Backend registration after an operator-confirmed reset");
341
+ deviceCmd
342
+ .command("repair-lock")
343
+ .description("Safely inspect and recover a local Device identity lock left by an exited process")
344
+ .option("--json", "output JSON instead of terminal text")
345
+ .action((options) => {
346
+ try {
347
+ const config = loadConfig();
348
+ const status = new DeviceAuthority({
349
+ baseURL: config.baseURL,
350
+ ...(config.agentType ? { requestedAgentType: config.agentType } : {}),
351
+ compatibilityHeaders: {},
352
+ }).repairLock();
353
+ writeCommandEnvelope({
354
+ status: status === "recovered" ? "device_lock_recovered" : status === "active" ? "device_lock_active" : "device_lock_absent",
355
+ result: { lock_status: status, device_identity_preserved: true },
356
+ instruction: status === "active" ? "本地身份正由存活进程更新;稍后继续原任务。" : "本地锁无需进一步处理;继续原任务。",
357
+ next: null,
358
+ recovery: [],
359
+ }, { jsonOutput: Boolean(options.json), plainResult: [`lock: ${status}`, "device_identity: preserved"] });
360
+ }
361
+ catch (error) {
362
+ reportCLIError(error, { jsonOutput: Boolean(options.json), code: "device_lock_repair_failed", instruction: "本地锁无法安全检查;保留设备身份并联系维护者。" });
363
+ }
364
+ });
334
365
  deviceCmd
335
366
  .command("recover")
336
367
  .description("Forget only the current official Backend registration while preserving the local private key")
@@ -1382,7 +1413,7 @@ services
1382
1413
  .argument("<result_item_id>")
1383
1414
  .option("--offset <offset>", "zero-based candidate offset", Number, 0)
1384
1415
  .option("--cursor <cursor>", "opaque v2 page cursor (rcur_<offset>); takes precedence over --offset")
1385
- .option("--limit <limit>", "page size (1-20)", Number, 5)
1416
+ .option("--limit <limit>", "page size (1-20)", Number, 20)
1386
1417
  .option("--json", "output JSON")
1387
1418
  .action(async (serviceExecutionID, resultItemID, options) => {
1388
1419
  const config = loadConfig();
@@ -1634,12 +1665,16 @@ services
1634
1665
  .description("Show the next recommended agent action for a Service Execution")
1635
1666
  .argument("<service_execution_id>")
1636
1667
  .option("--since-snapshot <snapshot_id>", "delta read: suppress the journey payload when unchanged")
1668
+ .option("--timeout <seconds>", "wait up to 120 seconds for a useful result on this execution", (value) => Number(value))
1637
1669
  .option("--json", "output JSON instead of terminal text")
1638
1670
  .action(async (serviceExecutionID, options) => {
1639
1671
  const config = loadConfig();
1640
1672
  const backend = newBackendClient(config);
1641
1673
  try {
1642
- await runServicesNext(backend, serviceExecutionID, { jsonOutput: Boolean(options.json), sinceSnapshot: options.sinceSnapshot });
1674
+ if (options.timeout !== undefined && (!Number.isInteger(options.timeout) || options.timeout < 0 || options.timeout > 120)) {
1675
+ throw new Error("--timeout must be an integer from 0 to 120");
1676
+ }
1677
+ await runServicesNext(backend, serviceExecutionID, { jsonOutput: Boolean(options.json), sinceSnapshot: options.sinceSnapshot, timeoutSeconds: options.timeout });
1643
1678
  }
1644
1679
  catch (error) {
1645
1680
  reportCLIError(error, {
@@ -24,9 +24,6 @@ export function buildAgentChatHandoff(plan) {
24
24
  if (plan.orderItems?.length) {
25
25
  lines.push(`项目:${plan.orderItems.map((item) => `${item.title} x${item.quantity}`).join("、")}`);
26
26
  }
27
- if (plan.afterActionCommand) {
28
- lines.push("", `付款后查询:\`${plan.afterActionCommand}\``);
29
- }
30
27
  return {
31
28
  type: "send_payment_handoff",
32
29
  must_send_to_user: true,
@@ -200,9 +200,13 @@ async function accountAuth(action, baseURL, env, fetcher, backend) {
200
200
  }
201
201
  catch (error) {
202
202
  if (error instanceof AuthRequestError && (error.httpStatus === 404 || error.httpStatus === 410)) {
203
- return sessionGone(env, purpose, baseURL, open.sessionID);
203
+ rmSync(sellerAuthPath(baseURL, env, purpose), { force: true });
204
+ // The explicit login call may open one fresh session now. Status
205
+ // remains read-only and still reports the missing old session.
206
+ }
207
+ else {
208
+ return authStatusUnknown(open.sessionID);
204
209
  }
205
- return authStatusUnknown(open.sessionID);
206
210
  }
207
211
  }
208
212
  }
@@ -55,9 +55,12 @@ export class CartSession {
55
55
  ...(this.state.lastDisplayToken ? { lastDisplayToken: this.state.lastDisplayToken } : {}),
56
56
  ...(this.state.lastCheckoutURL ? { lastCheckoutURL: this.state.lastCheckoutURL } : {}),
57
57
  };
58
+ const contents = JSON.stringify(toSave, null, 2);
59
+ if (existsSync(path) && readFileSync(path, "utf-8") === contents)
60
+ return;
58
61
  mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
59
62
  const temporary = `${path}.${process.pid}.${randomUUID()}.tmp`;
60
- writeFileSync(temporary, JSON.stringify(toSave, null, 2), { encoding: "utf-8", mode: 0o600 });
63
+ writeFileSync(temporary, contents, { encoding: "utf-8", mode: 0o600 });
61
64
  chmodSync(temporary, 0o600);
62
65
  renameSync(temporary, path);
63
66
  chmodSync(path, 0o600);
@@ -15,7 +15,7 @@ import { OperationJournal } from "./operation_journal.js";
15
15
  export const DEFAULT_BASE_URL = "https://app.itpay.ai";
16
16
  export const DEV_BASE_URL = "https://dev.itpay.ai";
17
17
  export const SANDBOX_BASE_URL = "https://sandbox.itpay.ai";
18
- export const CLI_VERSION = "2.1.18";
18
+ export const CLI_VERSION = "2.1.22";
19
19
  export const API_CONTRACT_REVISION = "sha256:2f9fbcdc5cd0630910abb52671304cc019e348e9a778017ec92ddc3513b3639f";
20
20
  const CART_SESSION_DEFAULT_DIR = ".itpay-v3";
21
21
  const CART_SESSION_FILENAME = "cart.json";
@@ -43,12 +43,25 @@ export class DeviceAuthority {
43
43
  }
44
44
  async ensureAuthorization() {
45
45
  if (!this.pending) {
46
- this.pending = withFileLock(`${this.statePath}.lock`, () => this.prepareAuthorization()).finally(() => {
46
+ this.pending = this.cachedAuthorization() ?? withFileLock(`${this.statePath}.lock`, () => this.prepareAuthorization());
47
+ this.pending = this.pending.finally(() => {
47
48
  this.pending = undefined;
48
49
  });
49
50
  }
50
51
  return this.pending;
51
52
  }
53
+ cachedAuthorization() {
54
+ const agentType = this.requestedAgentType;
55
+ if (!agentType)
56
+ return undefined;
57
+ const state = this.readState();
58
+ const registration = state?.registrations[this.backendKey];
59
+ const session = registration?.sessions[agentType];
60
+ if (!registration?.agentInstances[agentType] || !session || Date.parse(session.expiresAt) <= Date.now() + 60_000)
61
+ return undefined;
62
+ const privateKey = this.readPrivateKey();
63
+ return privateKey ? Promise.resolve({ state: registration, agentType, session, privateKey }) : undefined;
64
+ }
52
65
  async recoverAuthorization() {
53
66
  await withFileLock(`${this.statePath}.lock`, async () => {
54
67
  const state = this.readState();
@@ -61,6 +74,9 @@ export class DeviceAuthority {
61
74
  this.writeState(state);
62
75
  });
63
76
  }
77
+ repairLock() {
78
+ return inspectAndRecoverLock(`${this.statePath}.lock`);
79
+ }
64
80
  async recoverBackendReset() {
65
81
  return withFileLock(`${this.statePath}.lock`, async () => {
66
82
  const state = this.readState();
@@ -236,6 +252,7 @@ export class DeviceAuthority {
236
252
  method: "POST",
237
253
  headers: { "Content-Type": "application/json", Accept: "application/json", ...this.compatibilityHeaders, ...extraHeaders },
238
254
  body,
255
+ signal: AbortSignal.timeout(15_000),
239
256
  });
240
257
  const payload = await response.json().catch(() => ({}));
241
258
  if (!response.ok)
@@ -300,6 +317,13 @@ export class DeviceStateError extends Error {
300
317
  this.name = "DeviceStateError";
301
318
  }
302
319
  }
320
+ export class DeviceLockBusyError extends Error {
321
+ code = "device_lock_busy";
322
+ constructor() {
323
+ super("ItPay device identity is being updated by another process");
324
+ this.name = "DeviceLockBusyError";
325
+ }
326
+ }
303
327
  function emptyDeviceState() {
304
328
  return { schemaVersion: "itpay.device.v2", registrations: {} };
305
329
  }
@@ -344,9 +368,9 @@ async function withFileLock(path, run) {
344
368
  catch (error) {
345
369
  throw asDeviceStatePathError(error, "prepare_lock") ?? error;
346
370
  }
347
- const ownerToken = randomUUID();
371
+ const ownerToken = `${process.pid}:${randomUUID()}`;
348
372
  let acquired = false;
349
- for (let attempt = 0; attempt < 200; attempt += 1) {
373
+ for (let attempt = 0; attempt < 800; attempt += 1) {
350
374
  try {
351
375
  writeFileSync(path, ownerToken, { encoding: "utf8", flag: "wx", mode: 0o600 });
352
376
  chmodSync(path, 0o600);
@@ -357,20 +381,12 @@ async function withFileLock(path, run) {
357
381
  const code = error.code;
358
382
  if (code !== "EEXIST")
359
383
  throw asDeviceStateError(error, "acquire_lock") ?? error;
360
- try {
361
- if (Date.now() - statSync(path).mtimeMs > 30_000)
362
- moveLockAside(path, "stale", "remove_stale_lock");
363
- }
364
- catch (statError) {
365
- if (statError.code !== "ENOENT") {
366
- throw asDeviceStateError(statError, "inspect_lock") ?? statError;
367
- }
368
- }
384
+ inspectAndRecoverLock(path);
369
385
  await new Promise((resolve) => setTimeout(resolve, 25));
370
386
  }
371
387
  }
372
388
  if (!acquired)
373
- throw new Error("timed out waiting for ItPay device identity lock");
389
+ throw new DeviceLockBusyError();
374
390
  try {
375
391
  return await run();
376
392
  }
@@ -378,6 +394,46 @@ async function withFileLock(path, run) {
378
394
  releaseLock(path, ownerToken);
379
395
  }
380
396
  }
397
+ function inspectAndRecoverLock(path) {
398
+ let token;
399
+ let age;
400
+ try {
401
+ const lockStat = statSync(path);
402
+ age = Date.now() - lockStat.mtimeMs;
403
+ token = lockStat.isDirectory() ? "" : readFileSync(path, "utf8");
404
+ }
405
+ catch (error) {
406
+ if (error.code === "ENOENT")
407
+ return "absent";
408
+ throw asDeviceStateError(error, "inspect_lock") ?? error;
409
+ }
410
+ const ownerPID = /^(\d+):[0-9a-f-]+$/.exec(token)?.[1];
411
+ if (ownerPID) {
412
+ try {
413
+ process.kill(Number(ownerPID), 0);
414
+ return "active";
415
+ }
416
+ catch (error) {
417
+ if (error.code !== "ESRCH")
418
+ return "active";
419
+ }
420
+ }
421
+ else if (age <= 30_000) {
422
+ return "active";
423
+ }
424
+ // Recheck the exact owner before moving the lock; another process may have renewed it.
425
+ try {
426
+ if (token ? readFileSync(path, "utf8") !== token : !statSync(path).isDirectory())
427
+ return "active";
428
+ }
429
+ catch (error) {
430
+ if (error.code === "ENOENT")
431
+ return "absent";
432
+ throw asDeviceStateError(error, "inspect_lock") ?? error;
433
+ }
434
+ moveLockAside(path, "stale", "remove_stale_lock");
435
+ return "recovered";
436
+ }
381
437
  function releaseLock(path, ownerToken) {
382
438
  try {
383
439
  if (readFileSync(path, "utf8") !== ownerToken)
@@ -45,6 +45,8 @@ export class TaskJournal {
45
45
  observe(serviceExecutionID, stage, resumeCommand) {
46
46
  const file = this.readFile();
47
47
  const existing = file.tasks[serviceExecutionID];
48
+ if (existing && existing.stage === stage && (resumeCommand === undefined || existing.resume_command === resumeCommand))
49
+ return;
48
50
  file.tasks[serviceExecutionID] = {
49
51
  service_execution_id: serviceExecutionID,
50
52
  ...(existing?.service_id ? { service_id: existing.service_id } : {}),
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "schema_version": "itp.agent_doc.v1",
3
3
  "role": "buyer",
4
- "product_scope": "itpay is the single public CLI entry point, and $itpay is its user-facing Skill invocation. Under that one product entry point, the two top-level commerce actions are buy and sell: Buyer workflows are available now; Seller workflows will use the same entry point and are not implemented yet.",
4
+ "product_scope": "itpay is the single public CLI entry point for Buyer services and the existing itpay sell Seller workflow; $itpay is its user-facing Skill invocation.",
5
5
  "topic": "cart-checkout",
6
6
  "title": "Canonical Cart And Checkout Routing",
7
7
  "purpose": "Use the canonical server Cart and route service-backed lines through Service Execution without hardcoded service logic.",
@@ -32,11 +32,12 @@
32
32
  }
33
33
  ],
34
34
  "agent_rules": [
35
- "The Backend Cart is business truth; ~/.itpay-v3/cart.json only stores local recovery handles and explicit drafts.",
35
+ "The Backend Cart is business truth; the current backend-isolated local cart state file only stores local recovery handles and explicit drafts.",
36
36
  "Run the exact next.command returned by the CLI. Do not extract or invent a capability from a raw list.",
37
- "One service intent uses one Service Execution. Start another execution for another independent intent.",
37
+ "Railway query and booking use distinct Service Executions for one purchase journey. Start another execution for an independent intent.",
38
38
  "Ask for email only when the selected capability requires protected delivery; never invent contact data.",
39
- "A lost Checkout response is retried with the same Cart and persisted idempotency operation."
39
+ "A lost Checkout response is retried with the same Cart and persisted idempotency operation.",
40
+ "A single railway service checkout normally follows its booking execution; Cart is used only when the returned flow calls for it."
40
41
  ],
41
42
  "forbidden": [
42
43
  "Do not call ordinary buy for a service-backed Cart line.",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "schema_version": "itp.agent_doc.v1",
3
3
  "role": "buyer",
4
- "product_scope": "itpay is the single public CLI entry point, and $itpay is its user-facing Skill invocation. Under that one product entry point, the two top-level commerce actions are buy and sell: Buyer workflows are available now; Seller workflows will use the same entry point and are not implemented yet.",
4
+ "product_scope": "itpay is the single public CLI entry point for Buyer services and the existing itpay sell Seller workflow; $itpay is its user-facing Skill invocation.",
5
5
  "topic": "catalog-list",
6
6
  "title": "Browse ItPay V3 Service Catalog",
7
7
  "purpose": "Explain available services, outcomes, conditions, and prices in human language without exposing catalog identifiers.",
@@ -20,7 +20,8 @@
20
20
  "item_format": "Explain the title, what the human receives, any auxiliary discovery step, and the published price.",
21
21
  "service_flow": "Explain discovery as an auxiliary identity-confirmation step, including free quota and paid continuation. Present primary_service as the actual paid outcome. Read all values from the manifest.",
22
22
  "variant_selection": "Internal IDs are for the Agent's next command only and are never shown or requested from the human.",
23
- "price_note": "amount_minor is in minor units (e.g., 10000 = 100.00 CNY). The CLI formats prices as XX.YY currency automatically."
23
+ "price_note": "amount_minor is in minor units (e.g., 10000 = 100.00 CNY). The CLI formats prices as XX.YY currency automatically.",
24
+ "railway": "Railway entries point to itpay docs show rail-booking --json; use its query and booking contracts instead of a generic start→invoke→cart sequence."
24
25
  },
25
26
  "agent_rules": [
26
27
  "Use catalog list only when the human asks for a new service, not when they want an earlier purchase.",
@@ -28,15 +29,32 @@
28
29
  "Explain the customer journey from service_flow instead of presenting every variant as an unrelated service.",
29
30
  "State free quota, paid continuation price, email requirement, and claim purpose exactly as published; do not exaggerate or omit conditions.",
30
31
  "Ask the human to choose only when multiple services could satisfy the goal; never make them choose an internal variant or offer ID.",
31
- "After services start, submit service keywords only through the returned capability command's --input key=value options; --target is never a search keyword."
32
+ "Use each service’s returned input contract; railway planning and booking follow rail-booking, not a universal Cart route."
32
33
  ],
33
34
  "human_language": {
34
- "prefer": ["服务", "查询", "报告", "免费次数", "价格", "你会得到什么"],
35
- "avoid": ["service_id", "variant_id", "offer_id", "capability", "manifest"]
35
+ "prefer": [
36
+ "服务",
37
+ "查询",
38
+ "报告",
39
+ "免费次数",
40
+ "价格",
41
+ "你会得到什么"
42
+ ],
43
+ "avoid": [
44
+ "service_id",
45
+ "variant_id",
46
+ "offer_id",
47
+ "capability",
48
+ "manifest"
49
+ ]
36
50
  },
37
51
  "next_docs": [
38
52
  {
39
- "condition": "Buyer selected a service and wants to purchase",
53
+ "condition": "Railway service selected",
54
+ "topic": "rail-booking"
55
+ },
56
+ {
57
+ "condition": "Non-rail Buyer selected a service and wants to purchase",
40
58
  "topic": "cart-checkout",
41
59
  "file": "docs/agent/buyer/cart-checkout.json"
42
60
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "schema_version": "itp.agent_doc.v1",
3
3
  "role": "buyer",
4
- "product_scope": "itpay is the single public CLI entry point, and $itpay is its user-facing Skill invocation. Under that one product entry point, the two top-level commerce actions are buy and sell: Buyer workflows are available now; Seller workflows will use the same entry point and are not implemented yet.",
4
+ "product_scope": "itpay is the single public CLI entry point for Buyer services and the existing itpay sell Seller workflow; $itpay is its user-facing Skill invocation.",
5
5
  "topic": "identity-and-sessions",
6
6
  "title": "Device Identity And Session Recovery",
7
7
  "purpose": "Explain stable local identity, official Backend-scoped registrations, Agent Type instances, and bounded automatic session recovery.",
@@ -12,7 +12,7 @@
12
12
  ],
13
13
  "identity_model": {
14
14
  "local_key": "One owner-only Ed25519 private key for the local installation.",
15
- "backend_registration": "One registration per official Backend: app.itpay.ai by default and sandbox.itpay.ai only with the exact test override.",
15
+ "backend_registration": "One registration per configured official Backend. Follow the current CLI backend allowlist; do not switch to evade failures.",
16
16
  "agent_instance": "One instance per agent_type under each Backend registration; same-type windows and chats reuse it.",
17
17
  "presentation": "Host and target route output only and never identify the Agent or provide business input."
18
18
  },
@@ -26,10 +26,10 @@
26
26
  "Re-enrolling a public key the Backend already holds re-attaches to the existing device and quota lineage after proof-of-possession; it never creates a duplicate device. If enrollment verification keeps failing with internal_error, or returns agent_device_key_rotated or agent_device_key_conflict, `itpay device reset-key --confirm-key-reset` is the supported way to start over with a fresh key."
27
27
  ],
28
28
  "local_state_recovery": [
29
- "Device state updates use an atomic directory lock and owner-only atomic file replacement; no lock file is opened with fs.open or fs.openSync.",
29
+ "Device state uses an owner-only atomic file replacement and a lock that the CLI can inspect. On a stale lock, run the returned device repair-lock command; it only reclaims a proven stale owner.",
30
30
  "Operation idempotency uses one immutable owner-only record per operation key. Different commands and Agent processes do not share a global operation-journal lock; legacy operations.json is read only.",
31
31
  "On device_state_unwritable, use the reported operation and errno to diagnose the failing stage without exposing local paths; keep the same Agent Type and CLI/Node launcher, restore Host-approved persistent access to ~/.itpay-v3, then retry the original command.",
32
- "Do not manually create or delete state locks, switch Node runtimes, delete identity, or rotate Agent Type as a retry strategy."
32
+ "Do not delete a live lock, switch Node runtimes, delete identity, or rotate Agent Type as a retry strategy."
33
33
  ],
34
34
  "commands": [
35
35
  {
@@ -51,19 +51,24 @@
51
51
  "intent": "abandon the local device key after the Backend refuses to complete enrollment with it (persistent internal_error, agent_device_key_rotated, or agent_device_key_conflict)",
52
52
  "command": "itpay device reset-key --confirm-key-reset --json",
53
53
  "success_signal": "the local private key is discarded and the next command enrolls as a brand-new device"
54
+ },
55
+ {
56
+ "intent": "repair only a proven stale local device lock",
57
+ "command": "itpay device repair-lock --json",
58
+ "success_signal": "the CLI reports whether the lock was stale and reclaimed; an active owner stays protected"
54
59
  }
55
60
  ],
56
61
  "agent_rules": [
57
62
  "Use the real runtime type and keep it stable across every command in the flow.",
58
63
  "Keep the same CLI/Node launcher and Host-approved permission context for the whole flow.",
59
64
  "Treat a new window, chat, task, or process of the same runtime as the same Agent Type, not a new identity.",
60
- "Expect production traffic to use https://app.itpay.ai. Use https://sandbox.itpay.ai only for an explicit test and keep the complete returned Backend-prefixed command for that entire flow.",
65
+ "Production defaults to https://app.itpay.ai; explicit sandbox or dev testing uses https://sandbox.itpay.ai or https://dev.itpay.ai. Keep the complete returned Backend-prefixed command for that flow.",
61
66
  "After automatic recovery fails, stop and report the exact code, selected official Backend, Agent Type, and command family without exposing private keys or tokens."
62
67
  ],
63
68
  "forbidden": [
64
69
  "Do not delete ~/.itpay-v3, rotate the key, switch Agent Type, or repeatedly retry to obtain fresh quota.",
65
70
  "Do not use device recover for session expiry, revocation, quota recovery, or an unconfirmed Backend failure.",
66
- "Do not redirect the CLI to dev, local, an IP, or any custom Backend; only the exact official sandbox.itpay.ai test override is allowed.",
71
+ "Do not switch Backend to evade a failure. Allowed public URLs are app.itpay.ai, sandbox.itpay.ai and dev.itpay.ai; local loopback requires the explicit CLI development flag.",
67
72
  "Do not use --target as identity or service input."
68
73
  ],
69
74
  "next_docs": [
@@ -93,10 +98,10 @@
93
98
  ],
94
99
  "auth_session_states": {
95
100
  "stages": "The CLI now reports the auth session stage exactly as the server returns it: waiting_provider (show the handoff and wait), email_verification_required (the human must verify the emailed code on the auth page), merge_confirmation_required (an existing account matches — the human must confirm the merge on the auth page), provider_pending/pending (generic wait), completed (session done — bind it, do not restart).",
96
- "terminal_states": "auth_expired/auth_denied/auth_cancelled/auth_failed mean this session is over; say so plainly and start a NEW session only if the human still wants in — never keep polling a dead one.",
97
- "transport_unknown": "auth_session_unknown means a network error while polling — the outcome is unknown. Keep waiting on the SAME session (it may still complete); do NOT restart auth, and do not assume denial.",
98
- "missing": "auth_session_not_found means the cached session is gone server-side; start a new login.",
99
- "completed_resume": "When login completes, itpay auth login resumes the remembered pending service execution automatically when one exists — you do not need to find it yourself.",
101
+ "terminal_states": "auth_expired/auth_denied/auth_cancelled/auth_failed mean this session is over; say so plainly and respect denial or cancellation. Start a new session only on an explicit login request; never keep polling a dead one.",
102
+ "transport_unknown": "auth_status_unknown means a network error while polling — the outcome is unknown. Keep waiting on the SAME session (it may still complete); do NOT restart auth, and do not assume denial.",
103
+ "missing": "auth_session_missing means the cached session is gone. An explicit auth login opens one fresh official session in that call; auth status stays read-only.",
104
+ "completed_resume": "After login completes, execute the returned command to resume the same paused service execution. Login does not silently run that command.",
100
105
  "path_b": "A buyer who reaches checkout without a login completes identity inside the protected checkout page; never force a separate itpay auth login first."
101
106
  }
102
107
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "schema_version": "itp.agent_doc.v1",
3
3
  "role": "buyer",
4
- "product_scope": "itpay is the single public CLI entry point, and $itpay is its user-facing Skill invocation. Under that one product entry point, the two top-level commerce actions are buy and sell: Buyer workflows are available now; Seller workflows will use the same entry point and are not implemented yet.",
4
+ "product_scope": "itpay is the public CLI entry point for Buyer services and existing itpay sell Seller workflows.",
5
5
  "topic": "install-and-setup",
6
6
  "title": "Install And Identify The ItPay Agent Runtime",
7
7
  "purpose": "Select the real Agent Type, verify one official Backend, and load the packaged Skill through one linear setup path.",
@@ -32,23 +32,24 @@
32
32
  }
33
33
  ],
34
34
  "agent_rules": [
35
- "Start with install, identify the current real runtime from result.agent_types, run itpay install <agent_type> --json yourself, and then execute each returned next.command unchanged until the Skill is loaded.",
35
+ "For first setup or a runtime change, identify the real Agent Type once, run itpay install <agent_type> --json yourself, and follow its setup continuation until the packaged Skill is loaded. Ordinary service requests start from the Skill's service command, without repeating install or readyz.",
36
36
  "After setup, tell the human only that ItPay is ready; never ask them to run setup commands or explain technical setup steps.",
37
37
  "Use the CLI distribution already provided by the current npm package, Skill, or plugin; update only through that distribution's own instructions.",
38
- "Production defaults to https://app.itpay.ai. Testing may set ITPAY_BACKEND_URL to the exact official URL https://sandbox.itpay.ai; no other Backend is allowed.",
38
+ "Production defaults to https://app.itpay.ai. Testing may set ITPAY_BACKEND_URL to the exact official URL the configured official test Backend only when explicitly requested; follow the CLI allowlist.",
39
39
  "If backend_contract_incompatible includes result.required_cli_version, stop all ItPay business commands and use only the exact distribution-specific update recovery returned by the CLI; never replace its version with latest.",
40
40
  "After upgrading, require itpay --version to equal result.required_cli_version before running readyz again; never change Agent Type or Device identity to recover compatibility.",
41
41
  "Use one exact type: codex-desktop, codex-cli, claude-code-desktop, claude-code-cli, workbuddy, zcode, doubao-work, kimi-code, or openclaw.",
42
42
  "For compatibility, a global --agent-type codex declaration is normalized immediately to codex-desktop; all registration, output, and returned commands use codex-desktop. Never generate the alias, and do not use codex as an install target.",
43
- "One local private key is reused, while Device registrations and quota lineage remain separate for app.itpay.ai and sandbox.itpay.ai.",
43
+ "One local private key is reused, while Device registrations and quota lineage remain separate for app.itpay.ai, sandbox.itpay.ai and dev.itpay.ai.",
44
44
  "Within each official Backend registration, each Agent Type has one Agent Instance; all windows and chats of the same type reuse it.",
45
45
  "Agent Type identifies the runtime. Host and target only select a presentation destination; load render-hosts only when a handoff cannot be shown.",
46
46
  "Keep the exact Agent Type in every next or recovery command; do not fall back to a type previously used by another runtime.",
47
- "Do not change Agent Type or rotate local identity to reset quota or recover a failed command."
47
+ "Do not change Agent Type or rotate local identity to reset quota or recover a failed command.",
48
+ "Skillhub and local host bundles are separate installed artifacts; verify their version and packaged Skill/docs instead of assuming an npm update changed them."
48
49
  ],
49
50
  "forbidden": [
50
51
  "Do not use terminal, claude-code, or plain-chat as Agent Types. Do not generate codex as an Agent Type; it is accepted only as a legacy global declaration and becomes codex-desktop before any request.",
51
- "Do not set or suggest any Backend except the default https://app.itpay.ai or the exact test override https://sandbox.itpay.ai.",
52
+ "Do not set or suggest a Backend outside the CLI allowlist: https://app.itpay.ai, https://sandbox.itpay.ai, https://dev.itpay.ai; local loopback requires the CLI development flag. Do not switch Backend to evade a failure.",
52
53
  "Do not claim that install writes host configuration or registers a device; it only prints instructions.",
53
54
  "Do not create a new identity for a different window, task, chat, or process of the same Agent Type."
54
55
  ],
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "schema_version": "itp.agent_doc.v1",
3
3
  "role": "buyer",
4
- "product_scope": "itpay is the single public CLI entry point, and $itpay is its user-facing Skill invocation. Under that one product entry point, the two top-level commerce actions are buy and sell: Buyer workflows are available now; Seller workflows will use the same entry point and are not implemented yet.",
4
+ "product_scope": "itpay is the single public CLI entry point for Buyer services and the existing itpay sell Seller workflow; $itpay is its user-facing Skill invocation.",
5
5
  "topic": "orders-refunds",
6
6
  "title": "Order, Delivery, Customer Care And Refund Recovery",
7
7
  "purpose": "Explain paid-order and delivery problems to the human, then read or manage the same owned Order without guessing payment, consumption, or refund state.",
@@ -34,16 +34,16 @@
34
34
  {
35
35
  "intent": "cancel before provider submission",
36
36
  "command": "itpay --agent-type <agent_type> refund cancel <refund_request_id> --json",
37
- "success_signal": "the Refund Owner cancels the request and requires a new delivery authorization"
37
+ "success_signal": "the Refund Owner returns the current cancellation and access state; old grants do not revive"
38
38
  }
39
39
  ],
40
40
  "agent_rules": [
41
41
  "Act as the human's ItPay service representative: explain payment, delivery, access, and refund facts in plain language before giving the next action.",
42
42
  "After verified payment, say the Order is recorded and the human must not pay again. Recover that same Order or Service Execution if delivery is delayed or fails.",
43
43
  "ItPay derives payment, amount, currency, account ownership, and refund policy from the owned order; explain only their user-facing meaning.",
44
- "Unconsumed delivery normally follows the automatic policy; consumed delivery normally requires manual review. These are policy routes, not promises of a successful or instant refund.",
44
+ "Read this order’s current refund policy. Railway return depends on supplier ticket facts and may require manual review; platform refund status alone does not prove a train ticket was returned.",
45
45
  "A refund request freezes all delivery paths and revokes existing grants immediately.",
46
- "Cancellation or rejection restores eligibility but never reactivates an old grant.",
46
+ "Cancellation or rejection follows the returned order policy and never reactivates an old grant automatically.",
47
47
  "Use get or watch after interruption; do not infer success from elapsed time.",
48
48
  "Never switch CLI/MCP lanes or identities to recover an order or refund error.",
49
49
  "A read-only MCP Agent may explain policy and order facts, but directs refund actions to the ItPay Dashboard or a full Local CLI Agent."