@rover-studio/answer-me 0.1.0-rc.1
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/bin/answerme-toolkit.mjs +6 -0
- package/distribution/npm/migrations.json +605 -0
- package/distribution/npm/package-manifest.json +192 -0
- package/distribution/npm/skills/answerme/SKILL.md +94 -0
- package/distribution/npm/skills/answerme/agents/openai.yaml +4 -0
- package/distribution/npm/skills/answerme/references/api.md +135 -0
- package/distribution/npm/skills/answerme/references/creator-credential-deployment.md +19 -0
- package/distribution/npm/skills/answerme/references/creator-credential-recovery.md +24 -0
- package/distribution/npm/skills/answerme/references/errors.md +44 -0
- package/distribution/npm/skills/answerme/references/handoff.md +46 -0
- package/distribution/npm/skills/answerme/references/install-self-test.md +28 -0
- package/distribution/npm/skills/answerme/references/result-token-store.md +50 -0
- package/distribution/npm/skills/answerme/references/templates.md +139 -0
- package/distribution/npm/skills/answerme/scripts/answerme-api-base-url.ps1 +40 -0
- package/distribution/npm/skills/answerme/scripts/create-answerme.ps1 +1892 -0
- package/distribution/npm/skills/answerme/scripts/creator-credential-store.windows.ps1 +503 -0
- package/distribution/npm/skills/answerme/scripts/deploy-answerme-creator-credential.ps1 +447 -0
- package/distribution/npm/skills/answerme/scripts/enroll-answerme-creator.ps1 +764 -0
- package/distribution/npm/skills/answerme/scripts/open-answerme-page.windows.ps1 +272 -0
- package/distribution/npm/skills/answerme/scripts/remove-answerme-result-token.ps1 +63 -0
- package/distribution/npm/skills/answerme/scripts/result-token-store.windows.ps1 +261 -0
- package/distribution/npm/skills/answerme/scripts/test-answerme-installation.ps1 +498 -0
- package/distribution/npm/skills/answerme/scripts/wait-answerme-result.ps1 +908 -0
- package/distribution/npm/skills/answerme/scripts/windows-crypto.ps1 +57 -0
- package/distribution/npm/skills/answerme/scripts/windows-http.ps1 +45 -0
- package/distribution/npm/skills/answerme/scripts/windows-process-start-info.ps1 +76 -0
- package/distribution/npm/skills/ask-when-needed/SKILL.md +164 -0
- package/distribution/npm/skills/ask-when-needed/agents/openai.yaml +4 -0
- package/distribution/npm/skills/ask-when-needed/references/interview-strategies.md +43 -0
- package/lib/npm-cli/commands.mjs +247 -0
- package/lib/npm-cli/constants.mjs +51 -0
- package/lib/npm-cli/errors.mjs +15 -0
- package/lib/npm-cli/filesystem.mjs +193 -0
- package/lib/npm-cli/host-discovery.mjs +404 -0
- package/lib/npm-cli/main.mjs +42 -0
- package/lib/npm-cli/package-integrity.mjs +212 -0
- package/lib/npm-cli/transaction.mjs +375 -0
- package/lib/npm-cli/usage-validation.mjs +349 -0
- package/package.json +17 -0
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
$ErrorActionPreference = 'Stop'
|
|
2
|
+
|
|
3
|
+
function Clear-AnswerMeSensitiveByteArray {
|
|
4
|
+
param([AllowNull()][byte[]]$Bytes)
|
|
5
|
+
|
|
6
|
+
if ($null -ne $Bytes -and $Bytes.Length -gt 0) {
|
|
7
|
+
[Array]::Clear($Bytes, 0, $Bytes.Length)
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function Get-AnswerMeSha256Bytes {
|
|
12
|
+
param([Parameter(Mandatory = $true)][byte[]]$Bytes)
|
|
13
|
+
|
|
14
|
+
$algorithm = [Security.Cryptography.SHA256]::Create()
|
|
15
|
+
try {
|
|
16
|
+
return ,$algorithm.ComputeHash($Bytes)
|
|
17
|
+
}
|
|
18
|
+
finally {
|
|
19
|
+
$algorithm.Dispose()
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function ConvertTo-AnswerMeLowerHex {
|
|
24
|
+
param([Parameter(Mandatory = $true)][byte[]]$Bytes)
|
|
25
|
+
|
|
26
|
+
return [BitConverter]::ToString($Bytes).Replace('-', '').ToLowerInvariant()
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function New-AnswerMeCryptographicRandomBytes {
|
|
30
|
+
param([Parameter(Mandatory = $true)][ValidateRange(1, 4096)][int]$Length)
|
|
31
|
+
|
|
32
|
+
$bytes = New-Object byte[] $Length
|
|
33
|
+
$generator = [Security.Cryptography.RandomNumberGenerator]::Create()
|
|
34
|
+
try {
|
|
35
|
+
$generator.GetBytes($bytes)
|
|
36
|
+
return ,$bytes
|
|
37
|
+
}
|
|
38
|
+
finally {
|
|
39
|
+
$generator.Dispose()
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function Test-AnswerMeFixedTimeBytesEqual {
|
|
44
|
+
param(
|
|
45
|
+
[Parameter(Mandatory = $true)][byte[]]$Left,
|
|
46
|
+
[Parameter(Mandatory = $true)][byte[]]$Right
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
[int]$difference = $Left.Length -bxor $Right.Length
|
|
50
|
+
[int]$comparisonLength = [Math]::Max($Left.Length, $Right.Length)
|
|
51
|
+
for ($index = 0; $index -lt $comparisonLength; $index += 1) {
|
|
52
|
+
[int]$leftByte = if ($index -lt $Left.Length) { $Left[$index] } else { 0 }
|
|
53
|
+
[int]$rightByte = if ($index -lt $Right.Length) { $Right[$index] } else { 0 }
|
|
54
|
+
$difference = $difference -bor ($leftByte -bxor $rightByte)
|
|
55
|
+
}
|
|
56
|
+
return $difference -eq 0
|
|
57
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
$ErrorActionPreference = 'Stop'
|
|
2
|
+
|
|
3
|
+
function Invoke-AnswerMeWebRequest {
|
|
4
|
+
[CmdletBinding()]
|
|
5
|
+
param(
|
|
6
|
+
[Parameter(Mandatory = $true)][Uri]$Uri,
|
|
7
|
+
[ValidateSet('Get', 'Post')][string]$Method = 'Get',
|
|
8
|
+
[AllowNull()][System.Collections.IDictionary]$Headers,
|
|
9
|
+
[AllowNull()][string]$ContentType,
|
|
10
|
+
[AllowNull()][object]$Body,
|
|
11
|
+
[int]$TimeoutSec = 0
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
$parameters = @{ Uri = $Uri; Method = $Method; ErrorAction = 'Stop' }
|
|
15
|
+
$command = Get-Command -Name 'Invoke-WebRequest' -CommandType Cmdlet -ErrorAction Stop
|
|
16
|
+
if ($command.Parameters.ContainsKey('UseBasicParsing')) { $parameters.UseBasicParsing = $true }
|
|
17
|
+
if ($command.Parameters.ContainsKey('SkipHttpErrorCheck')) { $parameters.SkipHttpErrorCheck = $true }
|
|
18
|
+
if ($null -ne $Headers) { $parameters.Headers = $Headers }
|
|
19
|
+
if (-not [string]::IsNullOrWhiteSpace($ContentType)) { $parameters.ContentType = $ContentType }
|
|
20
|
+
if ($null -ne $Body) { $parameters.Body = $Body }
|
|
21
|
+
if ($TimeoutSec -gt 0) { $parameters.TimeoutSec = $TimeoutSec }
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
return Invoke-WebRequest @parameters
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
$response = $_.Exception.Response
|
|
28
|
+
if ($null -eq $response -or $response -isnot [Net.HttpWebResponse]) { throw }
|
|
29
|
+
$stream = $response.GetResponseStream()
|
|
30
|
+
$reader = $null
|
|
31
|
+
try {
|
|
32
|
+
$reader = New-Object IO.StreamReader($stream, [Text.Encoding]::UTF8, $true)
|
|
33
|
+
$content = $reader.ReadToEnd()
|
|
34
|
+
}
|
|
35
|
+
finally {
|
|
36
|
+
if ($null -ne $reader) { $reader.Dispose() }
|
|
37
|
+
elseif ($null -ne $stream) { $stream.Dispose() }
|
|
38
|
+
}
|
|
39
|
+
return [PSCustomObject]@{
|
|
40
|
+
StatusCode = [int]$response.StatusCode
|
|
41
|
+
Headers = $response.Headers
|
|
42
|
+
Content = $content
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
$ErrorActionPreference = 'Stop'
|
|
2
|
+
|
|
3
|
+
function ConvertTo-AnswerMeWindowsProcessArgument {
|
|
4
|
+
param(
|
|
5
|
+
[Parameter(Mandatory = $true)]
|
|
6
|
+
[AllowEmptyString()]
|
|
7
|
+
[string]$Value
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
if ($Value.IndexOf([char]0) -ge 0) {
|
|
11
|
+
throw 'Process arguments cannot contain a null character.'
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
$builder = New-Object System.Text.StringBuilder
|
|
15
|
+
[void]$builder.Append('"')
|
|
16
|
+
$backslashCount = 0
|
|
17
|
+
foreach ($character in $Value.ToCharArray()) {
|
|
18
|
+
if ($character -ceq [char]'\') {
|
|
19
|
+
$backslashCount += 1
|
|
20
|
+
continue
|
|
21
|
+
}
|
|
22
|
+
if ($character -ceq [char]'"') {
|
|
23
|
+
if ($backslashCount -gt 0) {
|
|
24
|
+
[void]$builder.Append((('\' * ($backslashCount * 2)) -join ''))
|
|
25
|
+
}
|
|
26
|
+
[void]$builder.Append('\"')
|
|
27
|
+
$backslashCount = 0
|
|
28
|
+
continue
|
|
29
|
+
}
|
|
30
|
+
if ($backslashCount -gt 0) {
|
|
31
|
+
[void]$builder.Append((('\' * $backslashCount) -join ''))
|
|
32
|
+
$backslashCount = 0
|
|
33
|
+
}
|
|
34
|
+
[void]$builder.Append($character)
|
|
35
|
+
}
|
|
36
|
+
if ($backslashCount -gt 0) {
|
|
37
|
+
[void]$builder.Append((('\' * ($backslashCount * 2)) -join ''))
|
|
38
|
+
}
|
|
39
|
+
[void]$builder.Append('"')
|
|
40
|
+
return $builder.ToString()
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function Set-AnswerMeProcessArguments {
|
|
44
|
+
param(
|
|
45
|
+
[Parameter(Mandatory = $true)]
|
|
46
|
+
[System.Diagnostics.ProcessStartInfo]$StartInfo,
|
|
47
|
+
[Parameter(Mandatory = $true)]
|
|
48
|
+
[AllowEmptyCollection()]
|
|
49
|
+
[object[]]$Arguments
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
$encoded = @($Arguments | ForEach-Object {
|
|
53
|
+
ConvertTo-AnswerMeWindowsProcessArgument -Value ([string]$_)
|
|
54
|
+
})
|
|
55
|
+
$StartInfo.Arguments = [string]::Join(' ', [string[]]$encoded)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function Set-AnswerMeProcessEnvironmentVariable {
|
|
59
|
+
param(
|
|
60
|
+
[Parameter(Mandatory = $true)]
|
|
61
|
+
[System.Diagnostics.ProcessStartInfo]$StartInfo,
|
|
62
|
+
[Parameter(Mandatory = $true)]
|
|
63
|
+
[string]$Name,
|
|
64
|
+
[Parameter(Mandatory = $true)]
|
|
65
|
+
[AllowEmptyString()]
|
|
66
|
+
[string]$Value
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
if ([string]::IsNullOrWhiteSpace($Name) -or $Name.IndexOf('=') -ge 0 -or $Name.IndexOf([char]0) -ge 0) {
|
|
70
|
+
throw 'Process environment variable name is invalid.'
|
|
71
|
+
}
|
|
72
|
+
if ($Value.IndexOf([char]0) -ge 0) {
|
|
73
|
+
throw 'Process environment variable value cannot contain a null character.'
|
|
74
|
+
}
|
|
75
|
+
$StartInfo.EnvironmentVariables[$Name] = $Value
|
|
76
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ask-when-needed
|
|
3
|
+
description: 每个任务开始实质工作前必须加载,检查工作目标和必要前置条件是否齐全;任务中目标、约束、证据或用户要求发生实质变化时重新检查。判断默认处理、调查或提问,组织开工核对与访谈,选择渠道并恢复原任务。加载不意味着必须提问,也不把每次普通回复当成新任务。
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# AskWhenNeeded
|
|
7
|
+
|
|
8
|
+
## 目标
|
|
9
|
+
|
|
10
|
+
在工作开始前检查需求理解,在执行中根据新信息更新。区分必要澄清与可选复核,避免遗漏和过度打断;根据问题形态选择访谈策略与渠道,获得答复后恢复原任务。
|
|
11
|
+
|
|
12
|
+
本 Skill 是用户输入的临时路由器,不是原任务的流程控制器。需求、设计、实现、调试、审批或其他专项工作仍由调用它的任务或 Skill 控制。
|
|
13
|
+
|
|
14
|
+
## 遵守边界
|
|
15
|
+
|
|
16
|
+
- 遵守当前环境的指令优先级、授权、安全、文件保护与持久化规则。本 Skill 不扩大权限。
|
|
17
|
+
- 不用提问代替合理调查,也不为避免一次低成本确认而无限调查。
|
|
18
|
+
- 不把任何具体 Skill、工具、MCP、网页服务或 Agent 产品设为默认渠道,也不要求为了提问而安装特定能力。
|
|
19
|
+
- 访谈策略只定义如何询问,不接管原任务,也不自行决定具体交互工具。
|
|
20
|
+
- 未回答、取消或超时均不表示同意。
|
|
21
|
+
|
|
22
|
+
## 识别检查节点
|
|
23
|
+
|
|
24
|
+
- 每个任务开始实质工作前,围绕当前交付物检查目标、验收标准、必要输入、约束和授权;不等发现缺口才加载。
|
|
25
|
+
- 只整理已有信息的缺口、冲突,以及下一步实际依赖的条件;不穷举假想分歧,不为凑题重复已确认内容。
|
|
26
|
+
- 目标、约束、证据或用户要求发生实质变化时重新检查;进入重要决策、外部操作或最终结论前,核对所依赖的条件是否仍有效。
|
|
27
|
+
|
|
28
|
+
检查节点不单独构成提问理由。没有具体缺口、有价值的可选核对问题或明确访谈请求时,继续原任务;普通回复不重新开启一次开工检查。
|
|
29
|
+
|
|
30
|
+
## 判断是否值得提问
|
|
31
|
+
|
|
32
|
+
先查当前任务明确指向的代码、规格、配置、日志、文档和其他可用证据,再查最接近问题的权威、直接、当前版本来源。来源冲突时,不把找到单个来源视为已经查明。
|
|
33
|
+
|
|
34
|
+
下一次调查不能明显提高结论可靠度,或调查成本已经高于一次用户确认时,停止追加调查。
|
|
35
|
+
|
|
36
|
+
用户明确要求 Brainstorm 或 Grill 时,该请求构成采用相应访谈策略的理由,不要求额外证明存在既定未决事实;但访谈中的事实性问题仍必须先调查,范围扩展和策略切换仍需确认。
|
|
37
|
+
|
|
38
|
+
- **已有有效答案**:当前指示、已确认信息或适用约定已决定做法,且没有反证时直接沿用。
|
|
39
|
+
- **能够可靠查明**:在当前授权和合理成本内先调查或验证;来源冲突未解决不算查明。
|
|
40
|
+
- **不同答案不会实质改变结果**:按适用惯例选择成本较低的默认做法,必要时说明假设。
|
|
41
|
+
- **有实质影响且无法合理查明**:进入时点判断,向用户收集答案。
|
|
42
|
+
|
|
43
|
+
实质影响指不同答案会改变目标、验收标准、范围、授权边界,或造成显著返工;架构、数据、安全和用户专属选择按这些后果判断。可逆性只用于评估执行风险,不能单独作为默认处理的理由。
|
|
44
|
+
|
|
45
|
+
## 开工前核对
|
|
46
|
+
|
|
47
|
+
- **可选复核**:必要条件齐全且存在有价值的核对问题时,说明核对原因和预计题数,邀请用户核对或直接开始;复杂题组的申请按“访谈申请与轮次变化”执行,不再叠加一次申请。
|
|
48
|
+
- **无需重复申请**:已有明确开工指示、讨论已经充分或没有具体问题时,直接开始。用户拒绝可选复核后,沿用已确认信息推进。
|
|
49
|
+
- **必要澄清**:说明具体缺口及其影响,单个简单问题直接问,复杂题组先申请。拒绝访谈不表示条件已补齐;只继续不受缺口影响的工作。
|
|
50
|
+
|
|
51
|
+
## 选择提问时点
|
|
52
|
+
|
|
53
|
+
“决策锁定点”是第一个依赖该答案,并会锁定重要方向、产生显著返工、造成外部影响或变得难以恢复的步骤。
|
|
54
|
+
|
|
55
|
+
- **已到决策锁定点**:现在问;高影响或不可逆操作在执行前取得明确同意。
|
|
56
|
+
- **尚未到决策锁定点**:记录待决问题,继续不受影响的工作,在锁定点前提问。
|
|
57
|
+
- **低成本证据即将获得**:先完成验证,再决定是否提问。
|
|
58
|
+
- **新证据推翻旧答案或假设**:重新判断,不机械沿用旧结论。
|
|
59
|
+
|
|
60
|
+
不要先执行依赖答案的工作,再用提问追认本可提前确认的假设。
|
|
61
|
+
|
|
62
|
+
## 选择访谈策略
|
|
63
|
+
|
|
64
|
+
策略与渠道是两个独立维度。先选择如何访谈,再选择通过什么能力交付。
|
|
65
|
+
|
|
66
|
+
- **直接提问**:单个明确缺口、简单确认、单选或简短开放回答。保持轻量,不加载访谈参考。
|
|
67
|
+
- **Brainstorm**:问题开放复杂、候选空间尚未形成,或需要先发散再归类、比较和收拢。选择后读取 [references/interview-strategies.md](references/interview-strategies.md) 的 Brainstorm 部分。
|
|
68
|
+
- **Grill**:已有计划、方案或决策框架,需要暴露关键矛盾、验证依赖并锁定落地细节。选择后读取同一参考的 Grill 部分。
|
|
69
|
+
|
|
70
|
+
用户明确指定策略时,在不违反更高优先级约束的前提下保持该选择。策略不再适合时,说明新策略、切换原因和收益;获得用户确认后切换。Brainstorm 收拢出候选方案后可以转入 Grill,Grill 发现尚无可行解空间时可以转回 Brainstorm。
|
|
71
|
+
|
|
72
|
+
旧的显式兼容入口只能提供策略提示;本 Skill 仍负责最终路由。不得反向加载兼容入口。
|
|
73
|
+
|
|
74
|
+
## 设计问题
|
|
75
|
+
|
|
76
|
+
- 每题只问一个核心判断;用最少背景说明问题及其具体影响。
|
|
77
|
+
- 需要选项时,使其简短、可执行并保留自由输入;单选项应互斥,多选项可组合。
|
|
78
|
+
- 协助用户理清思路或作方案取舍时,默认先结合已知目标、事实和约束形成推荐,说明关键理由、主要代价和适用条件,再请用户确认或修正;保留可行替代与自由表达,不把可以完成的分析转嫁给用户,也不以开放题代替应有的建议。
|
|
79
|
+
- 依据不足时先作合理调查;仍缺关键条件时给出条件性建议,并只追问会改变建议的缺口。无法形成有依据的推荐时说明缺口,提供判断框架,不捏造事实、确定性或优劣。
|
|
80
|
+
- 按每道题判断是否需要中立:核实实际情况、采集真实经历或未经引导的偏好,以及调查、测量等会被建议污染的回答,不预设推荐答案,可提供中立的说明或示例。不能仅因用户尚未形成偏好就免除决策辅助;推荐不等于用户答案或操作授权。
|
|
81
|
+
- 每轮最多 3 题,优先解决最重要的上游问题;相互依赖的问题分轮,独立问题可以同轮。不把多个判断藏进一题以规避上限。
|
|
82
|
+
- 每轮获得答案后更新已知信息,删除已解决或失去价值的问题,重新组织后续高价值问题;不问满配额。
|
|
83
|
+
- 简单确认保持简单,不为使用丰富界面而增加步骤。
|
|
84
|
+
- 选择 Brainstorm 或 Grill 时,问题内容与顺序服从访谈参考;是否组成题组仍由本 Skill 结合依赖关系和渠道能力决定。
|
|
85
|
+
|
|
86
|
+
## 按能力选择渠道
|
|
87
|
+
|
|
88
|
+
**简单问题**:当前待问内容只有一道单选题,且题干不超过 20 字。独立背景与选项不计入题干;不得把实际问题移进背景、删减必要信息或硬改成单选来规避分流。其他均为**非简单(复杂)问题**,包括多选、多道题及开放式问题。按当前已知的整组待问内容判断,不因分轮投递降为简单问题;访谈管理确认按下一节单独处理。此分类只用于渠道分流,不表示认知难度、业务重要性或风险。
|
|
89
|
+
|
|
90
|
+
| 输入需要 | 必要能力 |
|
|
91
|
+
|---|---|
|
|
92
|
+
| 简单问题 | 单选与必要背景展示 |
|
|
93
|
+
| 开放式问题 | 自由文本 |
|
|
94
|
+
| 多项同时选择 | 真正多选 |
|
|
95
|
+
| 多个相关问题或丰富说明 | 结构化题组和上下文展示 |
|
|
96
|
+
| 用户可能稍后回答 | 异步等待和任务恢复 |
|
|
97
|
+
| 用户可能更换设备 | 跨设备访问 |
|
|
98
|
+
| 高影响操作 | 明确确认和所需的可追溯性 |
|
|
99
|
+
|
|
100
|
+
1. 检查当前真实可用的能力,不假定平台一定支持多选、异步或跨设备。
|
|
101
|
+
2. 用户明确指定渠道时,在能力、安全和授权允许的前提下保持该选择;访谈策略不得改写渠道决定。
|
|
102
|
+
3. 当前环境存在范围匹配且已获授权的首选渠道策略时,在满足必要能力、能够安全承载当前内容且授权充分的候选中优先采用;不能满足时明确说明降级或阻塞,不得假装已经使用成功。
|
|
103
|
+
4. 不存在适用的首选渠道策略时,在满足必要能力的渠道中选择启动步骤、等待时间和用户操作最少者。
|
|
104
|
+
5. 缺少的能力只是便利时,允许降级并说明损失。
|
|
105
|
+
6. 能力是用户明确要求或正确收集答案的必要条件时,不得声称普通对话已经满足;说明阻塞,并请求更换渠道或输入形式。
|
|
106
|
+
|
|
107
|
+
首选渠道只改变渠道排序,不扩大授权,也不替代高影响操作所需的独立确认。策略范围不匹配、内容不适合该渠道或渠道不可用时,继续按必要能力选择其他低成本渠道。
|
|
108
|
+
|
|
109
|
+
## 访谈申请与轮次变化
|
|
110
|
+
|
|
111
|
+
- 复杂提问开始前,说明为什么提问、预计总题数和最多轮数,再用一道简单单选申请开始;必要背景独立展示。按渠道规则交付该申请,不再为交付申请本身递归申请。
|
|
112
|
+
- 用户已明确要求开始访谈或逐项审批且范围明确时,该请求视为启动同意;说明题数和轮数后开始,不重复确认。访谈同意不授权业务执行或高风险操作。
|
|
113
|
+
- 保留获准话题范围、题数和轮数上限,以及已用题数和轮数。原范围和上限内可以动态重排问题;超出范围、题数或轮数前,说明新增缺口、影响及新增上限,重新申请。
|
|
114
|
+
- 信息提前充分时,说明必要条件已澄清,请用户确认是否提前结束访谈并开始工作;未收到同意前,不擅自提前结束,也不为补足轮数继续凑题。用户明确停止时立即停止,无需追问是否停止。
|
|
115
|
+
- 按约定完成时恢复原任务;拒绝追加、取消或未答均不补齐缺口,只推进不依赖该答案的工作。启动与轮次变更的管理确认不占已获准的内容题数和轮数,避免确认本身触发追加申请。
|
|
116
|
+
|
|
117
|
+
## 等待与恢复
|
|
118
|
+
|
|
119
|
+
提问前保留四项最小状态:待决问题、第一个受影响步骤、已完成且无需重做的工作、等待期间仍可安全推进的工作。
|
|
120
|
+
|
|
121
|
+
当结构化提问因超时、取消或返回空答案而未收到用户有效回复,导致本轮等待中断时,最终回复必须完整复述决策背景、待决问题、全部候选项及各自关键后果;不得假定用户已经看过或仍能看到问卷,不得省略上述信息而直接要求用户“批准”“确认”或“继续”。
|
|
122
|
+
|
|
123
|
+
获得答复后:
|
|
124
|
+
|
|
125
|
+
1. 将答复映射到待决问题;存在歧义时只澄清该歧义。
|
|
126
|
+
2. 更新相关假设和计划,不重复询问已经回答的事项。
|
|
127
|
+
3. 访谈尚未结束时按轮次规则继续;结束条件及所需确认满足后,从第一个受影响步骤恢复,不无故重做已完成工作。
|
|
128
|
+
4. 取消、超时或未回答时,只继续不依赖答案且安全的工作;否则说明阻塞点。
|
|
129
|
+
|
|
130
|
+
## 从纠正中提出候选偏好
|
|
131
|
+
|
|
132
|
+
以下信号可以触发偏好建议:用户明确说“以后”“每次”“不要再”;同类默认行为被重复否定;纠正反映稳定工作方式、授权边界或风险偏好;不记录会持续产生显著返工或风险。
|
|
133
|
+
|
|
134
|
+
一次特殊情况、普通方案选择、临时环境偏好、无法写清条件的纠正,以及用户已拒绝且没有新证据的同类规则,只用于当前任务。
|
|
135
|
+
|
|
136
|
+
候选规则写清:触发条件、行为、范围、例外、建议载体。可以这样询问:
|
|
137
|
+
|
|
138
|
+
```text
|
|
139
|
+
你刚才否定了我在“[触发条件]”时默认“[原行为]”。
|
|
140
|
+
以后遇到这类情况,要不要先向你确认“[需要确认的信息]”?
|
|
141
|
+
建议范围:[范围];例外:[例外]。
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## 经同意后持久化
|
|
145
|
+
|
|
146
|
+
- 未经明确同意,只在当前会话应用纠正,不写入文件或外部记忆。
|
|
147
|
+
- 按当前环境的全局持久化规则选择最小权威载体:项目偏好写项目规则,跨项目个人偏好写用户级规则;个人偏好不得写入核心 `SKILL.md`。
|
|
148
|
+
- 只有方法本身的通用变化,才可在用户明确授权修改该 Skill 后写入核心 `SKILL.md`。
|
|
149
|
+
- 写入前说明具体规则、范围和目标路径并取得所需授权;写入后回读。失败时明确说明不具备跨会话约束力。
|
|
150
|
+
- 不持久化密钥、身份信息、私人答案或其他敏感内容。撤销或收窄规则时遵循相同授权流程。
|
|
151
|
+
|
|
152
|
+
## 应用偏好
|
|
153
|
+
|
|
154
|
+
- 只应用范围与当前情境匹配的规则;当前任务的明确指示优先,但不能覆盖更高优先级指令或安全边界。
|
|
155
|
+
- 项目规则不得外推为跨项目偏好;规则冲突或范围不清时询问用户。
|
|
156
|
+
- 不因存在偏好而机械提问,触发条件不成立时继续正常工作。
|
|
157
|
+
|
|
158
|
+
## 自检
|
|
159
|
+
|
|
160
|
+
提问前核对:必要澄清、可选复核或显式访谈的依据成立;合理调查已到停止线;提问时点、访谈同意与轮次范围符合上述规则;已提供有依据的决策建议,或有明确的中立需要或证据缺口;问题单义,渠道符合适用策略并满足必要能力。
|
|
161
|
+
|
|
162
|
+
必要能力缺失而阻塞时,回答同时说明缺失能力、受阻步骤,并明确请求更换渠道、恢复能力或放宽输入要求。
|
|
163
|
+
|
|
164
|
+
建议持久化前确认:这是稳定模式而非一次例外;规则、范围和例外具体;用户明确同意;载体符合当前环境的全局规则。
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# 访谈策略
|
|
2
|
+
|
|
3
|
+
仅当 `ask-when-needed` 已选择 Brainstorm 或 Grill 时读取。本参考只定义访谈方法和切换信号;是否提问、题组组织、渠道、等待与恢复仍由 `ask-when-needed` 决定。
|
|
4
|
+
|
|
5
|
+
## 共享状态
|
|
6
|
+
|
|
7
|
+
维护:当前策略、主题与范围、已确认信息、未确定信息、问题路径、已解决与未解决分支、获准与已用题数和轮数、停止条件、待恢复位置。启动、追加和提前结束的确认统一遵循 `ask-when-needed` 的“访谈申请与轮次变化”,本参考不另设结束权限。
|
|
8
|
+
|
|
9
|
+
- 证据、自洽性、可行性和实际约束优先于迎合。
|
|
10
|
+
- 发现跑题、理解偏差、无法继续求真或问题质量下降时暂停,说明原因并交回路由器判断下一步。
|
|
11
|
+
- 用户停止时立即停止。强调深挖或拷问不表示无限循环。
|
|
12
|
+
|
|
13
|
+
## Brainstorm
|
|
14
|
+
|
|
15
|
+
目标:先扩大候选空间,再把想法归类、比较、筛选并收拢为可判断的结论或下一步。
|
|
16
|
+
|
|
17
|
+
1. 开始前确定主题与重点方向,按路由器规则取得启动同意并维护题数和轮数上限。
|
|
18
|
+
2. 题组组织遵守路由器的每轮上限与依赖顺序,不将上限当成必须问满的配额。
|
|
19
|
+
3. 发散阶段探索彼此不同的方向、约束和反例,不因首个可行想法过早收敛。
|
|
20
|
+
4. 根据最新答案动态生成下一问题;不预先倾倒完整清单,不重复用户已回答的内容。
|
|
21
|
+
5. 新的深度分支超出当前范围或需要追加题数、轮数时,交回路由器申请后再扩展。
|
|
22
|
+
6. 收拢阶段合并重复想法,按目标、证据、约束、成本和风险比较候选;淘汰不满足资格或缺少执行路径的候选。
|
|
23
|
+
7. 信息足够时交回路由器处理结束或提前结束确认;达到上限不得自行追加。结束时总结已确认、未确定、被淘汰方向、风险和下一步。
|
|
24
|
+
|
|
25
|
+
### 转向 Grill 的信号
|
|
26
|
+
|
|
27
|
+
候选方案或计划已经形成,继续发散的收益低于验证关键矛盾、依赖和落地细节的收益。将信号交回 `ask-when-needed`,不得自行切换。
|
|
28
|
+
|
|
29
|
+
## Grill
|
|
30
|
+
|
|
31
|
+
目标:从上游决策与关键矛盾开始,逐分支验证计划,直到结论自洽且落地细节足够明确。
|
|
32
|
+
|
|
33
|
+
1. 按决策依赖建立问题树,维护已解决分支、未解决分支和下一个上游问题。
|
|
34
|
+
2. 问题形式与推荐条件遵循 [问题设计规则](../SKILL.md#设计问题)。
|
|
35
|
+
3. 能从现有证据可靠查明的事实交回 `ask-when-needed` 先调查,不向用户转嫁。
|
|
36
|
+
4. 沿依赖顺序推进;新答案改变上游前提时,重排未解决分支。
|
|
37
|
+
5. 长轮次设置检查点,简述已解决、未解决、当前关键矛盾和下一问题。
|
|
38
|
+
6. 用户停止时立即停止;达到共同理解、所有实质分支已解决、问题质量下降或继续提问收益不足时,交回路由器判断结束或申请提前结束;需要扩展时同样由路由器申请。
|
|
39
|
+
7. 结束时总结共同结论、仍有分歧的分支、关键矛盾、风险和下一步。
|
|
40
|
+
|
|
41
|
+
### 转向 Brainstorm 的信号
|
|
42
|
+
|
|
43
|
+
当前阻塞不是某项决策未明确,而是缺少可行候选或问题空间尚未展开。将信号交回 `ask-when-needed`,不得自行切换。
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import {
|
|
3
|
+
MACHINE_SCHEMA_VERSION,
|
|
4
|
+
MANAGED_SKILLS,
|
|
5
|
+
PACKAGE_VERSION,
|
|
6
|
+
publicCode,
|
|
7
|
+
} from './constants.mjs';
|
|
8
|
+
import { InstallerError, errorCode } from './errors.mjs';
|
|
9
|
+
import {
|
|
10
|
+
discoverCodexHost,
|
|
11
|
+
evaluateManagedSkillDiscovery,
|
|
12
|
+
} from './host-discovery.mjs';
|
|
13
|
+
import {
|
|
14
|
+
classifyInstallation,
|
|
15
|
+
verifyPackageIntegrity,
|
|
16
|
+
} from './package-integrity.mjs';
|
|
17
|
+
import {
|
|
18
|
+
abortInstallTransaction,
|
|
19
|
+
executeInstallTransaction,
|
|
20
|
+
finalizeInstallTransaction,
|
|
21
|
+
recoverPendingTransaction,
|
|
22
|
+
} from './transaction.mjs';
|
|
23
|
+
import { runUsageValidation } from './usage-validation.mjs';
|
|
24
|
+
|
|
25
|
+
export function checkRuntime({ platform = process.platform, arch = process.arch, nodeVersion = process.versions.node } = {}) {
|
|
26
|
+
if (platform !== 'win32' || arch !== 'x64') throw new InstallerError('unsupported-platform');
|
|
27
|
+
const match = String(nodeVersion).match(/^(\d+)\.\d+\.\d+(?:[-+].+)?$/);
|
|
28
|
+
if (!match || Number(match[1]) < 20) throw new InstallerError('node-version-unsupported');
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function baseResult(command) {
|
|
33
|
+
return {
|
|
34
|
+
schemaVersion: MACHINE_SCHEMA_VERSION,
|
|
35
|
+
command,
|
|
36
|
+
version: PACKAGE_VERSION,
|
|
37
|
+
installation: { status: 'blocked', code: 'transaction-failed', directory: null, migratedFrom: null },
|
|
38
|
+
usageValidation: { status: 'not-run', code: null, cleanupWarning: null },
|
|
39
|
+
rollback: { status: 'not-needed', code: null },
|
|
40
|
+
host: null,
|
|
41
|
+
limitations: [],
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function setHost(result, host) {
|
|
46
|
+
result.host = { id: 'codex', version: host.hostVersion };
|
|
47
|
+
result.installation.directory = host.skillsRoot;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function dependency(name, fallback, dependencies) {
|
|
51
|
+
return dependencies[name] ?? fallback;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function discover(input, dependencies) {
|
|
55
|
+
const discoverHost = dependency('discoverHost', discoverCodexHost, dependencies);
|
|
56
|
+
return discoverHost(input, dependencies.host ?? {});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function prepare(command, options, dependencies) {
|
|
60
|
+
checkRuntime(dependencies.runtime);
|
|
61
|
+
const packageVerifier = dependency('verifyPackage', verifyPackageIntegrity, dependencies);
|
|
62
|
+
const packageInfo = await packageVerifier(dependencies.packageOptions);
|
|
63
|
+
let host = await discover({ cwd: options.cwd, env: options.env, context: options.executionContext }, dependencies);
|
|
64
|
+
const recover = dependency('recoverTransaction', recoverPendingTransaction, dependencies);
|
|
65
|
+
const recovered = await recover(host, dependencies.transaction ?? {});
|
|
66
|
+
if (recovered.status !== 'none') {
|
|
67
|
+
host = await discover({ cwd: options.cwd, env: options.env, context: options.executionContext }, dependencies);
|
|
68
|
+
}
|
|
69
|
+
evaluateManagedSkillDiscovery(host, { requireInstalled: false });
|
|
70
|
+
const classify = dependency('classifyInstallation', classifyInstallation, dependencies);
|
|
71
|
+
const classification = await classify(host.skillsRoot, packageInfo);
|
|
72
|
+
return { command, packageInfo, host, classification, recovered };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function verifyAfterMutation(context, dependencies, { absent = false } = {}) {
|
|
76
|
+
const observed = await discover({
|
|
77
|
+
cwd: context.options.cwd,
|
|
78
|
+
env: context.options.env,
|
|
79
|
+
context: context.options.executionContext,
|
|
80
|
+
}, dependencies);
|
|
81
|
+
if (path.resolve(observed.codexHome) !== path.resolve(context.host.codexHome)
|
|
82
|
+
|| path.resolve(observed.skillsRoot) !== path.resolve(context.host.skillsRoot)) {
|
|
83
|
+
throw new InstallerError('codex-root-untrusted');
|
|
84
|
+
}
|
|
85
|
+
evaluateManagedSkillDiscovery(observed, absent ? { requireAbsent: true } : { requireInstalled: true });
|
|
86
|
+
return observed;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function invokeUsageValidation(skillsRoot, dependencies) {
|
|
90
|
+
const validateUsage = dependency('validateUsage', runUsageValidation, dependencies);
|
|
91
|
+
return validateUsage({ skillsRoot, timeoutSeconds: 300 }, dependencies.usage ?? {});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function installOrUpdate(command, options, dependencies) {
|
|
95
|
+
const result = baseResult(command);
|
|
96
|
+
const prepared = await prepare(command, options, dependencies);
|
|
97
|
+
prepared.options = options;
|
|
98
|
+
setHost(result, prepared.host);
|
|
99
|
+
result.installation.migratedFrom = prepared.classification.legacyVersion ?? null;
|
|
100
|
+
|
|
101
|
+
if (command === 'update' && prepared.classification.kind === 'absent') {
|
|
102
|
+
throw Object.assign(new InstallerError('not-installed'), { partialResult: result });
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
let transactionHandle = null;
|
|
106
|
+
if (prepared.classification.kind === 'current') {
|
|
107
|
+
await verifyAfterMutation(prepared, dependencies);
|
|
108
|
+
result.installation.status = 'completed';
|
|
109
|
+
result.installation.code = 'no-change';
|
|
110
|
+
} else {
|
|
111
|
+
const transact = dependency('executeTransaction', executeInstallTransaction, dependencies);
|
|
112
|
+
const transaction = await transact({
|
|
113
|
+
operation: command,
|
|
114
|
+
codexHome: prepared.host.codexHome,
|
|
115
|
+
skillsRoot: prepared.host.skillsRoot,
|
|
116
|
+
packageInfo: prepared.packageInfo,
|
|
117
|
+
}, { ...(dependencies.transaction ?? {}), deferFinalize: true });
|
|
118
|
+
if (transaction.status === 'failed') {
|
|
119
|
+
result.installation.status = 'failed';
|
|
120
|
+
result.installation.code = publicCode(transaction.code);
|
|
121
|
+
result.rollback = { status: 'completed', code: 'rollback-completed' };
|
|
122
|
+
return { exitCode: 1, result };
|
|
123
|
+
}
|
|
124
|
+
transactionHandle = transaction.transactionHandle;
|
|
125
|
+
try {
|
|
126
|
+
await verifyAfterMutation(prepared, dependencies);
|
|
127
|
+
const finalize = dependency('finalizeTransaction', finalizeInstallTransaction, dependencies);
|
|
128
|
+
const finalized = await finalize(transactionHandle);
|
|
129
|
+
result.installation.status = 'completed';
|
|
130
|
+
result.installation.code = 'ok';
|
|
131
|
+
if (finalized.cleanupWarning) result.limitations.push('cleanup-warning');
|
|
132
|
+
} catch (error) {
|
|
133
|
+
const abort = dependency('abortTransaction', abortInstallTransaction, dependencies);
|
|
134
|
+
try {
|
|
135
|
+
await abort(transactionHandle, dependencies.transaction ?? {});
|
|
136
|
+
result.installation.status = 'failed';
|
|
137
|
+
result.installation.code = errorCode(error);
|
|
138
|
+
result.rollback = { status: 'completed', code: 'rollback-completed' };
|
|
139
|
+
return { exitCode: 1, result };
|
|
140
|
+
} catch {
|
|
141
|
+
result.installation.status = 'recovery-required';
|
|
142
|
+
result.installation.code = 'recovery-required';
|
|
143
|
+
result.rollback = { status: 'recovery-required', code: 'recovery-required' };
|
|
144
|
+
return { exitCode: 2, result };
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const usage = await invokeUsageValidation(prepared.host.skillsRoot, dependencies);
|
|
150
|
+
result.usageValidation = usage;
|
|
151
|
+
if (usage.cleanupWarning) result.limitations.push('cleanup-warning');
|
|
152
|
+
return { exitCode: 0, result };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async function uninstall(options, dependencies) {
|
|
156
|
+
const command = 'uninstall';
|
|
157
|
+
const result = baseResult(command);
|
|
158
|
+
const prepared = await prepare(command, options, dependencies);
|
|
159
|
+
prepared.options = options;
|
|
160
|
+
setHost(result, prepared.host);
|
|
161
|
+
if (prepared.classification.kind === 'absent') {
|
|
162
|
+
result.installation.status = 'completed';
|
|
163
|
+
result.installation.code = 'no-change';
|
|
164
|
+
return { exitCode: 0, result };
|
|
165
|
+
}
|
|
166
|
+
if (prepared.classification.kind === 'current-unowned') {
|
|
167
|
+
throw Object.assign(new InstallerError('not-installed'), { partialResult: result });
|
|
168
|
+
}
|
|
169
|
+
const transact = dependency('executeTransaction', executeInstallTransaction, dependencies);
|
|
170
|
+
const transaction = await transact({
|
|
171
|
+
operation: command,
|
|
172
|
+
codexHome: prepared.host.codexHome,
|
|
173
|
+
skillsRoot: prepared.host.skillsRoot,
|
|
174
|
+
packageInfo: prepared.packageInfo,
|
|
175
|
+
}, { ...(dependencies.transaction ?? {}), deferFinalize: true });
|
|
176
|
+
if (transaction.status === 'failed') {
|
|
177
|
+
result.installation.status = 'failed';
|
|
178
|
+
result.installation.code = publicCode(transaction.code);
|
|
179
|
+
result.rollback = { status: 'completed', code: 'rollback-completed' };
|
|
180
|
+
return { exitCode: 1, result };
|
|
181
|
+
}
|
|
182
|
+
try {
|
|
183
|
+
await verifyAfterMutation(prepared, dependencies, { absent: true });
|
|
184
|
+
const finalize = dependency('finalizeTransaction', finalizeInstallTransaction, dependencies);
|
|
185
|
+
const finalized = await finalize(transaction.transactionHandle);
|
|
186
|
+
result.installation.status = 'removed';
|
|
187
|
+
result.installation.code = 'removed';
|
|
188
|
+
if (finalized.cleanupWarning) result.limitations.push('cleanup-warning');
|
|
189
|
+
return { exitCode: 0, result };
|
|
190
|
+
} catch (error) {
|
|
191
|
+
const abort = dependency('abortTransaction', abortInstallTransaction, dependencies);
|
|
192
|
+
try {
|
|
193
|
+
await abort(transaction.transactionHandle, dependencies.transaction ?? {});
|
|
194
|
+
result.installation.status = 'failed';
|
|
195
|
+
result.installation.code = errorCode(error);
|
|
196
|
+
result.rollback = { status: 'completed', code: 'rollback-completed' };
|
|
197
|
+
return { exitCode: 1, result };
|
|
198
|
+
} catch {
|
|
199
|
+
result.installation.status = 'recovery-required';
|
|
200
|
+
result.installation.code = 'recovery-required';
|
|
201
|
+
result.rollback = { status: 'recovery-required', code: 'recovery-required' };
|
|
202
|
+
return { exitCode: 2, result };
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async function doctor(options, dependencies) {
|
|
208
|
+
const command = 'doctor';
|
|
209
|
+
const result = baseResult(command);
|
|
210
|
+
const prepared = await prepare(command, options, dependencies);
|
|
211
|
+
setHost(result, prepared.host);
|
|
212
|
+
if (prepared.classification.kind !== 'current') {
|
|
213
|
+
result.installation.status = 'checked';
|
|
214
|
+
result.installation.code = 'doctor-issue';
|
|
215
|
+
result.limitations.push(prepared.classification.kind === 'legacy' ? 'untrusted-legacy-installation' : 'not-installed');
|
|
216
|
+
return { exitCode: 1, result };
|
|
217
|
+
}
|
|
218
|
+
evaluateManagedSkillDiscovery(prepared.host, { requireInstalled: true });
|
|
219
|
+
result.installation.status = 'checked';
|
|
220
|
+
result.installation.code = 'doctor-healthy';
|
|
221
|
+
if (options.interactive) result.usageValidation = await invokeUsageValidation(prepared.host.skillsRoot, dependencies);
|
|
222
|
+
return { exitCode: 0, result };
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export async function runCommand(command, options = {}, dependencies = {}) {
|
|
226
|
+
const normalized = {
|
|
227
|
+
cwd: path.resolve(options.cwd ?? process.cwd()),
|
|
228
|
+
env: options.env ?? process.env,
|
|
229
|
+
executionContext: options.executionContext ?? {},
|
|
230
|
+
interactive: options.interactive === true,
|
|
231
|
+
};
|
|
232
|
+
try {
|
|
233
|
+
if (command === 'install' || command === 'update') return await installOrUpdate(command, normalized, dependencies);
|
|
234
|
+
if (command === 'uninstall') return await uninstall(normalized, dependencies);
|
|
235
|
+
if (command === 'doctor') return await doctor(normalized, dependencies);
|
|
236
|
+
throw new InstallerError('invalid-command');
|
|
237
|
+
} catch (error) {
|
|
238
|
+
const result = error.partialResult ?? baseResult(command);
|
|
239
|
+
const code = errorCode(error, 'transaction-failed');
|
|
240
|
+
result.installation.status = error?.recoveryRequired ? 'recovery-required' : 'blocked';
|
|
241
|
+
result.installation.code = code;
|
|
242
|
+
if (error?.recoveryRequired) result.rollback = { status: 'recovery-required', code: 'recovery-required' };
|
|
243
|
+
return { exitCode: error?.recoveryRequired ? 2 : 1, result };
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export const __test = Object.freeze({ baseResult, prepare, verifyAfterMutation });
|