@alphafox/cli 0.1.5 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/auth/browser-login.d.ts +30 -0
- package/dist/auth/browser-login.js +193 -0
- package/dist/auth/loopback-callback.d.ts +32 -0
- package/dist/auth/loopback-callback.js +175 -0
- package/dist/auth/open-browser.d.ts +15 -0
- package/dist/auth/open-browser.js +54 -0
- package/dist/auth/refresh.d.ts +27 -2
- package/dist/auth/refresh.js +52 -15
- package/dist/catalog/allowlist.d.ts +20 -4
- package/dist/catalog/allowlist.js +126 -25
- package/dist/catalog/command-tree.d.ts +34 -0
- package/dist/catalog/command-tree.js +117 -0
- package/dist/catalog/compatibility.d.ts +23 -0
- package/dist/catalog/compatibility.js +58 -0
- package/dist/catalog/generated/registry.json +6137 -0
- package/dist/catalog/generated/schemas.json +31036 -0
- package/dist/catalog/operations.d.ts +76 -3
- package/dist/catalog/operations.js +87 -213
- package/dist/commands/run.js +171 -153
- package/dist/config/profiles.js +3 -3
- package/dist/envelope.d.ts +3 -0
- package/dist/envelope.js +43 -3
- package/dist/http/client.js +18 -15
- package/dist/index.d.ts +12 -5
- package/dist/index.js +32 -1
- package/dist/keychain/linux-secret-service.d.ts +11 -0
- package/dist/keychain/linux-secret-service.js +93 -0
- package/dist/keychain/store.d.ts +20 -1
- package/dist/keychain/store.js +94 -6
- package/dist/keychain/windows-credential.d.ts +13 -0
- package/dist/keychain/windows-credential.js +176 -0
- package/dist/safety/confirmation.d.ts +10 -3
- package/dist/safety/confirmation.js +27 -4
- package/dist/version.d.ts +2 -2
- package/dist/version.js +3 -2
- package/docs/agents/domain.md +51 -0
- package/docs/agents/issue-tracker.md +156 -0
- package/docs/agents/triage-labels.md +18 -0
- package/docs/e2e-staging.md +76 -6
- package/docs/release-supply-chain.md +99 -26
- package/package.json +4 -2
- package/skills/account/SKILL.md +8 -6
- package/skills/admin/SKILL.md +9 -4
- package/skills/alphafox-shared/SKILL.md +23 -14
- package/skills/auth/SKILL.md +21 -6
- package/skills/exchange/SKILL.md +10 -3
- package/skills/market/SKILL.md +9 -4
- package/skills/notification/SKILL.md +8 -3
- package/skills/strategy/SKILL.md +15 -9
- package/skills/trading/SKILL.md +15 -5
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Windows Credential Manager via advapi32 CredWrite/CredRead/CredDelete.
|
|
4
|
+
* Payload is passed on stdin to PowerShell — never as argv.
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.WINDOWS_CRED_PS1 = exports.WINDOWS_CRED_MAX_BYTES = void 0;
|
|
8
|
+
exports.windowsCredentialTarget = windowsCredentialTarget;
|
|
9
|
+
exports.windowsPowershellBin = windowsPowershellBin;
|
|
10
|
+
exports.windowsCredentialWrite = windowsCredentialWrite;
|
|
11
|
+
exports.windowsCredentialRead = windowsCredentialRead;
|
|
12
|
+
exports.windowsCredentialDelete = windowsCredentialDelete;
|
|
13
|
+
exports.windowsCredentialAvailable = windowsCredentialAvailable;
|
|
14
|
+
const node_child_process_1 = require("node:child_process");
|
|
15
|
+
const node_fs_1 = require("node:fs");
|
|
16
|
+
const node_os_1 = require("node:os");
|
|
17
|
+
const node_path_1 = require("node:path");
|
|
18
|
+
/** CRED_MAX_CREDENTIAL_BLOB_SIZE is 5*512 = 2560. */
|
|
19
|
+
exports.WINDOWS_CRED_MAX_BYTES = 2560;
|
|
20
|
+
exports.WINDOWS_CRED_PS1 = `# Alphafox CLI — Windows Credential Manager helper (t101360)
|
|
21
|
+
param(
|
|
22
|
+
[Parameter(Mandatory = $true)][ValidateSet('write','read','delete')][string]$Action,
|
|
23
|
+
[Parameter(Mandatory = $true)][string]$Target
|
|
24
|
+
)
|
|
25
|
+
$ErrorActionPreference = 'Stop'
|
|
26
|
+
Add-Type -TypeDefinition @"
|
|
27
|
+
using System;
|
|
28
|
+
using System.Runtime.InteropServices;
|
|
29
|
+
namespace AlphafoxCred {
|
|
30
|
+
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
|
|
31
|
+
public struct CREDENTIAL {
|
|
32
|
+
public uint Flags;
|
|
33
|
+
public uint Type;
|
|
34
|
+
public string TargetName;
|
|
35
|
+
public string Comment;
|
|
36
|
+
public System.Runtime.InteropServices.ComTypes.FILETIME LastWritten;
|
|
37
|
+
public uint CredentialBlobSize;
|
|
38
|
+
public IntPtr CredentialBlob;
|
|
39
|
+
public uint Persist;
|
|
40
|
+
public uint AttributeCount;
|
|
41
|
+
public IntPtr Attributes;
|
|
42
|
+
public string TargetAlias;
|
|
43
|
+
public string UserName;
|
|
44
|
+
}
|
|
45
|
+
public static class Native {
|
|
46
|
+
[DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
|
|
47
|
+
public static extern bool CredWrite(ref CREDENTIAL credential, uint flags);
|
|
48
|
+
[DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
|
|
49
|
+
public static extern bool CredRead(string target, uint type, uint flags, out IntPtr credentialPtr);
|
|
50
|
+
[DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
|
|
51
|
+
public static extern bool CredDelete(string target, uint type, uint flags);
|
|
52
|
+
[DllImport("advapi32.dll", SetLastError = true)]
|
|
53
|
+
public static extern void CredFree(IntPtr credential);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
"@
|
|
57
|
+
$CredTypeGeneric = 1
|
|
58
|
+
$PersistLocalMachine = 2
|
|
59
|
+
switch ($Action) {
|
|
60
|
+
'write' {
|
|
61
|
+
$payload = [Console]::In.ReadToEnd()
|
|
62
|
+
$bytes = [Text.Encoding]::UTF8.GetBytes($payload)
|
|
63
|
+
if ($bytes.Length -gt ${exports.WINDOWS_CRED_MAX_BYTES}) { throw "credential blob too large" }
|
|
64
|
+
$blob = [Runtime.InteropServices.Marshal]::AllocHGlobal($bytes.Length)
|
|
65
|
+
try {
|
|
66
|
+
[Runtime.InteropServices.Marshal]::Copy($bytes, 0, $blob, $bytes.Length)
|
|
67
|
+
$cred = New-Object AlphafoxCred.CREDENTIAL
|
|
68
|
+
$cred.Type = $CredTypeGeneric
|
|
69
|
+
$cred.TargetName = $Target
|
|
70
|
+
$cred.UserName = "alphafox-cli"
|
|
71
|
+
$cred.CredentialBlobSize = [uint32]$bytes.Length
|
|
72
|
+
$cred.CredentialBlob = $blob
|
|
73
|
+
$cred.Persist = $PersistLocalMachine
|
|
74
|
+
$ok = [AlphafoxCred.Native]::CredWrite([ref]$cred, 0)
|
|
75
|
+
if (-not $ok) {
|
|
76
|
+
throw "CredWrite failed Win32=$([Runtime.InteropServices.Marshal]::GetLastWin32Error())"
|
|
77
|
+
}
|
|
78
|
+
} finally {
|
|
79
|
+
[Runtime.InteropServices.Marshal]::FreeHGlobal($blob)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
'read' {
|
|
83
|
+
$ptr = [IntPtr]::Zero
|
|
84
|
+
$ok = [AlphafoxCred.Native]::CredRead($Target, $CredTypeGeneric, 0, [ref]$ptr)
|
|
85
|
+
if (-not $ok) { exit 2 }
|
|
86
|
+
try {
|
|
87
|
+
$cred = [Runtime.InteropServices.Marshal]::PtrToStructure($ptr, [type][AlphafoxCred.CREDENTIAL])
|
|
88
|
+
$size = [int]$cred.CredentialBlobSize
|
|
89
|
+
$bytes = New-Object byte[] $size
|
|
90
|
+
[Runtime.InteropServices.Marshal]::Copy($cred.CredentialBlob, $bytes, 0, $size)
|
|
91
|
+
[Console]::Out.Write([Text.Encoding]::UTF8.GetString($bytes))
|
|
92
|
+
} finally {
|
|
93
|
+
[AlphafoxCred.Native]::CredFree($ptr)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
'delete' {
|
|
97
|
+
[void][AlphafoxCred.Native]::CredDelete($Target, $CredTypeGeneric, 0)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
`;
|
|
101
|
+
function windowsCredentialTarget(profile) {
|
|
102
|
+
return `alphafox-cli/${profile}/oauth-tokens`;
|
|
103
|
+
}
|
|
104
|
+
function windowsPowershellBin(env = process.env) {
|
|
105
|
+
return env.ALPHAFOX_POWERSHELL?.trim() || "powershell.exe";
|
|
106
|
+
}
|
|
107
|
+
function scriptPath() {
|
|
108
|
+
const dir = (0, node_path_1.join)((0, node_os_1.tmpdir)(), "alphafox-cli");
|
|
109
|
+
(0, node_fs_1.mkdirSync)(dir, { recursive: true });
|
|
110
|
+
const path = (0, node_path_1.join)(dir, "windows-cred.ps1");
|
|
111
|
+
(0, node_fs_1.writeFileSync)(path, exports.WINDOWS_CRED_PS1, { encoding: "utf8" });
|
|
112
|
+
return path;
|
|
113
|
+
}
|
|
114
|
+
function runCred(action, target, env, input) {
|
|
115
|
+
const result = (0, node_child_process_1.execFileSync)(windowsPowershellBin(env), [
|
|
116
|
+
"-NoProfile",
|
|
117
|
+
"-NonInteractive",
|
|
118
|
+
"-ExecutionPolicy",
|
|
119
|
+
"Bypass",
|
|
120
|
+
"-File",
|
|
121
|
+
scriptPath(),
|
|
122
|
+
action,
|
|
123
|
+
target,
|
|
124
|
+
], {
|
|
125
|
+
input: input ?? "",
|
|
126
|
+
encoding: "utf8",
|
|
127
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
128
|
+
timeout: 15_000,
|
|
129
|
+
windowsHide: true,
|
|
130
|
+
env: { ...process.env, ...env },
|
|
131
|
+
});
|
|
132
|
+
return typeof result === "string" ? result : String(result);
|
|
133
|
+
}
|
|
134
|
+
function windowsCredentialWrite(profile, payload, env = process.env) {
|
|
135
|
+
if (Buffer.byteLength(payload, "utf8") > exports.WINDOWS_CRED_MAX_BYTES) {
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
try {
|
|
139
|
+
runCred("write", windowsCredentialTarget(profile), env, payload);
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
function windowsCredentialRead(profile, env = process.env) {
|
|
147
|
+
try {
|
|
148
|
+
const out = runCred("read", windowsCredentialTarget(profile), env);
|
|
149
|
+
return out.length > 0 ? out : null;
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
function windowsCredentialDelete(profile, env = process.env) {
|
|
156
|
+
try {
|
|
157
|
+
runCred("delete", windowsCredentialTarget(profile), env);
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
// none
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
function windowsCredentialAvailable(env = process.env) {
|
|
164
|
+
try {
|
|
165
|
+
(0, node_child_process_1.execFileSync)(windowsPowershellBin(env), ["-NoProfile", "-Command", "exit 0"], {
|
|
166
|
+
stdio: "ignore",
|
|
167
|
+
timeout: 5_000,
|
|
168
|
+
windowsHide: true,
|
|
169
|
+
env: { ...process.env, ...env },
|
|
170
|
+
});
|
|
171
|
+
return true;
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type RiskLevel = "read" | "write" | "high-risk-write";
|
|
1
|
+
export type RiskLevel = "read" | "write" | "high-risk-write" | "unknown";
|
|
2
2
|
export interface ConfirmationGateResult {
|
|
3
3
|
readonly allowed: boolean;
|
|
4
4
|
readonly error?: {
|
|
@@ -6,12 +6,19 @@ export interface ConfirmationGateResult {
|
|
|
6
6
|
readonly subtype: "confirmation_required";
|
|
7
7
|
readonly message: string;
|
|
8
8
|
readonly hint: string;
|
|
9
|
-
readonly risk: "high-risk-write";
|
|
9
|
+
readonly risk: "high-risk-write" | "unknown";
|
|
10
10
|
readonly action: string;
|
|
11
11
|
};
|
|
12
12
|
}
|
|
13
13
|
/**
|
|
14
|
-
*
|
|
14
|
+
* Infer risk for raw `api METHOD PATH` calls.
|
|
15
|
+
* Catalog-matched risks win; uncataloged mutations are treated as high-risk
|
|
16
|
+
* so they cannot skip the `--yes` gate.
|
|
17
|
+
*/
|
|
18
|
+
export declare function inferRawApiRisk(method: string, catalogRisk: string | undefined): RiskLevel | string;
|
|
19
|
+
export declare function requiresHighRiskConfirmation(risk: RiskLevel | string): boolean;
|
|
20
|
+
/**
|
|
21
|
+
* High-risk writes (and uncataloged/unknown mutations) require explicit --yes.
|
|
15
22
|
* Server still enforces scopes/roles; this is CLI UX only.
|
|
16
23
|
*/
|
|
17
24
|
export declare function assertHighRiskConfirmation(input: {
|
|
@@ -1,12 +1,32 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.inferRawApiRisk = inferRawApiRisk;
|
|
4
|
+
exports.requiresHighRiskConfirmation = requiresHighRiskConfirmation;
|
|
3
5
|
exports.assertHighRiskConfirmation = assertHighRiskConfirmation;
|
|
6
|
+
const MUTATING_METHODS = new Set(["POST", "PUT", "PATCH", "DELETE"]);
|
|
4
7
|
/**
|
|
5
|
-
*
|
|
8
|
+
* Infer risk for raw `api METHOD PATH` calls.
|
|
9
|
+
* Catalog-matched risks win; uncataloged mutations are treated as high-risk
|
|
10
|
+
* so they cannot skip the `--yes` gate.
|
|
11
|
+
*/
|
|
12
|
+
function inferRawApiRisk(method, catalogRisk) {
|
|
13
|
+
if (catalogRisk) {
|
|
14
|
+
return catalogRisk;
|
|
15
|
+
}
|
|
16
|
+
if (MUTATING_METHODS.has(method.toUpperCase())) {
|
|
17
|
+
return "unknown";
|
|
18
|
+
}
|
|
19
|
+
return "read";
|
|
20
|
+
}
|
|
21
|
+
function requiresHighRiskConfirmation(risk) {
|
|
22
|
+
return risk === "high-risk-write" || risk === "unknown";
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* High-risk writes (and uncataloged/unknown mutations) require explicit --yes.
|
|
6
26
|
* Server still enforces scopes/roles; this is CLI UX only.
|
|
7
27
|
*/
|
|
8
28
|
function assertHighRiskConfirmation(input) {
|
|
9
|
-
if (input.risk
|
|
29
|
+
if (!requiresHighRiskConfirmation(input.risk)) {
|
|
10
30
|
return { allowed: true };
|
|
11
31
|
}
|
|
12
32
|
if (input.dryRun) {
|
|
@@ -15,14 +35,17 @@ function assertHighRiskConfirmation(input) {
|
|
|
15
35
|
if (input.yes) {
|
|
16
36
|
return { allowed: true };
|
|
17
37
|
}
|
|
38
|
+
const unknown = input.risk === "unknown";
|
|
18
39
|
return {
|
|
19
40
|
allowed: false,
|
|
20
41
|
error: {
|
|
21
42
|
type: "confirmation",
|
|
22
43
|
subtype: "confirmation_required",
|
|
23
44
|
message: `${input.action} requires confirmation`,
|
|
24
|
-
hint:
|
|
25
|
-
|
|
45
|
+
hint: unknown
|
|
46
|
+
? "uncataloged mutation treated as high-risk; add --yes to confirm"
|
|
47
|
+
: "add --yes to confirm",
|
|
48
|
+
risk: unknown ? "unknown" : "high-risk-write",
|
|
26
49
|
action: input.action,
|
|
27
50
|
},
|
|
28
51
|
};
|
package/dist/version.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export declare const CLI_NAME = "alphafox";
|
|
2
2
|
export declare const CLI_PACKAGE = "@alphafox/cli";
|
|
3
|
-
export declare const CLI_VERSION = "0.
|
|
4
|
-
export
|
|
3
|
+
export declare const CLI_VERSION = "0.2.0";
|
|
4
|
+
export { CATALOG_VERSION as CLI_CONTRACT_VERSION } from "./catalog/operations";
|
package/dist/version.js
CHANGED
|
@@ -3,5 +3,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.CLI_CONTRACT_VERSION = exports.CLI_VERSION = exports.CLI_PACKAGE = exports.CLI_NAME = void 0;
|
|
4
4
|
exports.CLI_NAME = "alphafox";
|
|
5
5
|
exports.CLI_PACKAGE = "@alphafox/cli";
|
|
6
|
-
exports.CLI_VERSION = "0.
|
|
7
|
-
|
|
6
|
+
exports.CLI_VERSION = "0.2.0";
|
|
7
|
+
var operations_1 = require("./catalog/operations");
|
|
8
|
+
Object.defineProperty(exports, "CLI_CONTRACT_VERSION", { enumerable: true, get: function () { return operations_1.CATALOG_VERSION; } });
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Domain Docs
|
|
2
|
+
|
|
3
|
+
How the engineering skills should consume this repo's domain documentation when exploring the codebase.
|
|
4
|
+
|
|
5
|
+
## Before exploring, read these
|
|
6
|
+
|
|
7
|
+
- **`CONTEXT.md`** at the repo root, or
|
|
8
|
+
- **`CONTEXT-MAP.md`** at the repo root if it exists — it points at one `CONTEXT.md` per context. Read each one relevant to the topic.
|
|
9
|
+
- **`docs/adr/`** — read ADRs that touch the area you're about to work in. In multi-context repos, also check `src/<context>/docs/adr/` for context-scoped decisions.
|
|
10
|
+
|
|
11
|
+
If any of these files don't exist, **proceed silently**. Don't flag their absence; don't suggest creating them upfront. The `/domain-modeling` skill (reached via `/grill-with-docs` and `/improve-codebase-architecture`) creates them lazily when terms or decisions actually get resolved.
|
|
12
|
+
|
|
13
|
+
## File structure
|
|
14
|
+
|
|
15
|
+
Single-context repo (most repos):
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
/
|
|
19
|
+
├── CONTEXT.md
|
|
20
|
+
├── docs/adr/
|
|
21
|
+
│ ├── 0001-event-sourced-orders.md
|
|
22
|
+
│ └── 0002-postgres-for-write-model.md
|
|
23
|
+
└── src/
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Multi-context repo (presence of `CONTEXT-MAP.md` at the root):
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
/
|
|
30
|
+
├── CONTEXT-MAP.md
|
|
31
|
+
├── docs/adr/ ← system-wide decisions
|
|
32
|
+
└── src/
|
|
33
|
+
├── ordering/
|
|
34
|
+
│ ├── CONTEXT.md
|
|
35
|
+
│ └── docs/adr/ ← context-specific decisions
|
|
36
|
+
└── billing/
|
|
37
|
+
├── CONTEXT.md
|
|
38
|
+
└── docs/adr/
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Use the glossary's vocabulary
|
|
42
|
+
|
|
43
|
+
When your output names a domain concept (in an issue title, a refactor proposal, a hypothesis, a test name), use the term as defined in `CONTEXT.md`. Don't drift to synonyms the glossary explicitly avoids.
|
|
44
|
+
|
|
45
|
+
If the concept you need isn't in the glossary yet, that's a signal — either you're inventing language the project doesn't use (reconsider) or there's a real gap (note it for `/domain-modeling`).
|
|
46
|
+
|
|
47
|
+
## Flag ADR conflicts
|
|
48
|
+
|
|
49
|
+
If your output contradicts an existing ADR, surface it explicitly rather than silently overriding:
|
|
50
|
+
|
|
51
|
+
> _Contradicts ADR-0007 (event-sourced orders) — but worth reopening because…_
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# Issue tracker: Feishu Tasks
|
|
2
|
+
|
|
3
|
+
Issues and specs for this repo live as tasks in the **Alphafox-Issues** Feishu (Lark) tasklist. Use the `lark-cli` CLI ([`@larksuite/cli`](https://github.com/larksuite/cli)) for all operations, always with `--as user` — a task created by the bot identity is invisible in the human's task centre.
|
|
4
|
+
|
|
5
|
+
List: <https://applink.feishu.cn/client/todo/task_list?guid=6d628d79-cdfe-47cc-98f7-35561944494b>
|
|
6
|
+
|
|
7
|
+
## Identifiers
|
|
8
|
+
|
|
9
|
+
An issue's identity is its **task GUID** — a UUID. There is no usable short number: the `t101420`-style id the Feishu UI shows (`task_id`, `suite_entity_num`) is **display-only** and every API rejects it, so a bare `#42` or `t101420` cannot be looked up directly.
|
|
10
|
+
|
|
11
|
+
- Prefer the task **URL** (`https://applink.feishu.cn/client/todo/detail?guid=<GUID>`) — the GUID is the `guid` query param. Every write command below also accepts the URL in place of the GUID.
|
|
12
|
+
- Given only a `t1014xx` number or a title fragment, resolve it with `lark-cli task +search --query "<title words>" --as user` and match on `summary`. Search spans **all** the user's tasks, not just this tasklist, so confirm the match before writing.
|
|
13
|
+
- When narrating to the human, refer to an issue by its **title**, not its GUID.
|
|
14
|
+
|
|
15
|
+
## Setup values
|
|
16
|
+
|
|
17
|
+
- **Tasklist GUID**: `6d628d79-cdfe-47cc-98f7-35561944494b`
|
|
18
|
+
- **`Type` field GUID**: `227d69e7-e535-40f0-b64f-4e90247149e2`
|
|
19
|
+
|
|
20
|
+
Triage roles are **sections** — a task sits in exactly one, so the state machine can't be violated. `待分类` is the default section, so anything created without a section lands there as untriaged.
|
|
21
|
+
|
|
22
|
+
| Canonical role | Section | Section GUID |
|
|
23
|
+
| -------------- | ------- | ------------ |
|
|
24
|
+
| _(untriaged)_ | `待分类` (default) | `4ca8fd68-9a08-5794-1598-8e5a394e8638` |
|
|
25
|
+
| `needs-triage` | `待评估` | `44e58692-1ced-462b-99bb-bf082a745ef3` |
|
|
26
|
+
| `needs-info` | `待补充信息` | `0cff4cf9-00b9-42bd-b482-5cd06a51b99b` |
|
|
27
|
+
| `ready-for-agent` | `可交给 Agent` | `1ce586d5-7953-4d68-b41a-d4b2851f5806` |
|
|
28
|
+
| `ready-for-human` | `需人工处理` | `8713387c-e758-4730-9de8-0b30859674ff` |
|
|
29
|
+
| `wontfix` | `不予处理` | `00d4b874-1a4a-43d8-b0cf-fcb52460f9bf` |
|
|
30
|
+
| _(wayfinder — outside the triage queue)_ | `探路图` | `bab88abf-0bf9-407d-806b-680901a86dd7` |
|
|
31
|
+
|
|
32
|
+
Every other label is an option on the single-select `Type` field. Address an option by its GUID, never by name — the skills' canonical labels use a colon (`wayfinder:map`) while the Feishu options are named with a hyphen (`wayfinder-map`), so this table is the only mapping between them:
|
|
33
|
+
|
|
34
|
+
| Label | Option GUID |
|
|
35
|
+
| ----- | ----------- |
|
|
36
|
+
| `bug` | `0eb51704-bead-4e8e-93be-dd8d02c1d20c` |
|
|
37
|
+
| `enhancement` | `ebd05bc8-ab6c-4a56-8628-6c3284d9f1f9` |
|
|
38
|
+
| `spec` | `399a640f-5d51-4dab-a4f3-da3df638ec8a` |
|
|
39
|
+
| `ticket` | `3bb7ca44-ac8c-4b75-8925-96fc457fb974` |
|
|
40
|
+
| `wayfinder:map` | `8cd13987-010e-46f0-808f-a8f289791cf5` |
|
|
41
|
+
| `wayfinder:research` | `f18b808a-d978-41cb-9bef-cd0708fecf0c` |
|
|
42
|
+
| `wayfinder:prototype` | `49126969-48dc-4297-96d4-36d176ffb8bb` |
|
|
43
|
+
| `wayfinder:grilling` | `265feb46-9ac8-4c03-afa8-9400296defe4` |
|
|
44
|
+
| `wayfinder:task` | `ed48b848-01d0-406b-ac7d-a96ce66cee0d` |
|
|
45
|
+
|
|
46
|
+
## Conventions
|
|
47
|
+
|
|
48
|
+
- **Shared-list title** — prefix every issue/task title with `[alphafox-cli]` so the repository remains identifiable on the shared board.
|
|
49
|
+
- **Tracker boundary** — Matt Skills issues, specs, tickets, triage items, and wayfinder maps belong in `Alphafox-Issues`, not a quarterly execution list. If quarterly planning initiated the work, link its absolute Feishu task URL instead of duplicating workflow state.
|
|
50
|
+
|
|
51
|
+
- **Create an issue** — one call sets body, section (triage role) and `Type`:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
lark-cli task tasks create --as user --params '{"user_id_type":"open_id"}' --data '{
|
|
55
|
+
"summary": "<title>",
|
|
56
|
+
"description": "<body>",
|
|
57
|
+
"tasklists": [{"tasklist_guid": "6d628d79-cdfe-47cc-98f7-35561944494b", "section_guid": "<SECTION_GUID>"}],
|
|
58
|
+
"custom_fields": [{"guid": "227d69e7-e535-40f0-b64f-4e90247149e2", "single_select_value": "<OPTION_GUID>"}]
|
|
59
|
+
}'
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
For an untriaged issue with no label, `lark-cli task +create --tasklist-id 6d628d79-cdfe-47cc-98f7-35561944494b --summary "..." --description "..." --as user` is enough — it lands in `待分类`.
|
|
63
|
+
|
|
64
|
+
- **Read an issue** — the body and the comments are two calls:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
lark-cli task tasks get --as user --params '{"task_guid": "<GUID>", "user_id_type": "open_id"}'
|
|
68
|
+
lark-cli api GET /open-apis/task/v2/comments --as user \
|
|
69
|
+
--params '{"resource_type": "task", "resource_id": "<GUID>", "page_size": 50, "user_id_type": "open_id"}'
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
`tasks get` returns `description` (the body), `custom_fields` (the `Type` label), `tasklists[].section_guid` (the triage role), `members` (assignees), `status` (`todo`/`done`), `dependencies` (blockers) and `parent_task_guid`. Comments are **not** exposed by `lark-cli task`; the raw `api` call above is the only way to read them.
|
|
73
|
+
|
|
74
|
+
- **List a triage bucket** — one call per section, and this is the cheap path:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
lark-cli task sections tasks --as user --params '{"section_guid": "<SECTION_GUID>", "completed": false, "page_size": 100}'
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Returns a brief per task — `guid`, `summary`, `completed_at`, `subtask_count` — which is all a queue listing needs. Pass `created_from`/`created_to` to window by age; results come oldest-first. Reading a task's `Type` or assignees means a `tasks get` per task, so don't do it while building a queue listing.
|
|
81
|
+
|
|
82
|
+
- **List every issue**: `lark-cli task tasklists tasks --as user --params '{"tasklist_guid": "6d628d79-cdfe-47cc-98f7-35561944494b", "completed": false, "page_size": 100}'`.
|
|
83
|
+
|
|
84
|
+
- **Comment on an issue**: `lark-cli task +comment --task-id <GUID> --content "..." --as user`. Plain text — Feishu renders no markdown in comments, so keep formatting light.
|
|
85
|
+
|
|
86
|
+
- **Apply a triage role** — move the task to that role's section. Re-adding an existing task with a new `--section-guid` moves it; there is no separate "remove from old section" step:
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
lark-cli task +tasklist-task-add --tasklist-id 6d628d79-cdfe-47cc-98f7-35561944494b --task-id <GUID> --section-guid <SECTION_GUID> --as user
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
- **Apply a `Type` label** — patch the custom field. This replaces the previous value, since the field is single-select:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
lark-cli task tasks patch --as user --params '{"task_guid": "<GUID>"}' --data '{
|
|
96
|
+
"task": {"custom_fields": [{"guid": "227d69e7-e535-40f0-b64f-4e90247149e2", "single_select_value": "<OPTION_GUID>"}]},
|
|
97
|
+
"update_fields": ["custom_fields"]
|
|
98
|
+
}'
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
- **Edit the body**: same shape, with `{"task": {"description": "..."}, "update_fields": ["description"]}`. A patch **replaces** the description, so read it first and re-send the whole text.
|
|
102
|
+
|
|
103
|
+
- **Every markdown link in a body needs a real absolute URL.** Feishu parses `[text](target)` in `description` and validates the target, rejecting the entire write with `Invalid Param 'description', url in description is invalid` when it isn't one. A placeholder `(link)`, an empty `()`, and a repo-relative `(./src/foo.ts)` all fail. Link to an absolute `https://` URL, or drop the link syntax and name the thing in plain text. Headings, lists, bold, bare URLs and task GUIDs are all fine.
|
|
104
|
+
|
|
105
|
+
- **Close**: `lark-cli task +complete --task-id <GUID> --as user`. It takes no closing comment, so post the explanation with `+comment` first, then complete. Reopen with `lark-cli task +reopen --task-id <GUID> --as user`.
|
|
106
|
+
|
|
107
|
+
- **Assign**: `lark-cli task +assign --task-id <GUID> --add <open_id> --as user` (`--remove` to unassign). Get your own `open_id` from `lark-cli auth status --jq '.identities.user.openId'`.
|
|
108
|
+
|
|
109
|
+
## Pull requests as a triage surface
|
|
110
|
+
|
|
111
|
+
**PRs as a request surface: no.** _(Set to `yes` if this repo treats external PRs as feature requests; `/triage` reads this flag.)_
|
|
112
|
+
|
|
113
|
+
Feishu Tasks holds no code, so PRs live on the GitHub remote. When this flag is `yes`, triage reads a PR and its diff with `gh pr view` / `gh pr list` / `gh pr diff` while the roles and states stay here in the tasklist: a PR under triage gets a task in this list whose body links the PR. Numbering never collides, because a task GUID and a `#42` are different shapes.
|
|
114
|
+
|
|
115
|
+
## When a skill says "publish to the issue tracker"
|
|
116
|
+
|
|
117
|
+
Create a task in this tasklist. Put the whole document in `description`, set `Type`, and place it in the section matching its triage role.
|
|
118
|
+
|
|
119
|
+
## When a skill says "fetch the relevant ticket"
|
|
120
|
+
|
|
121
|
+
Run `tasks get` for the body plus the raw `comments` call for the history — an agent brief or a resolution lives in one or the other.
|
|
122
|
+
|
|
123
|
+
## Wayfinding operations
|
|
124
|
+
|
|
125
|
+
Used by `/wayfinder`. The **map** is a task; its tickets are **subtasks** of it.
|
|
126
|
+
|
|
127
|
+
- **Map**: a task with `Type` = `wayfinder:map`, in `探路图`, holding the Destination / Notes / Decisions-so-far / Fog body in `description`.
|
|
128
|
+
- **Child ticket**: create the task, then parent it — `lark-cli task +set-ancestor --task-id <CHILD_GUID> --ancestor-id <MAP_GUID> --as user`. Parenting **keeps** the child's tasklist and section membership, so tickets stay visible in `探路图`. Label each with `Type` = `wayfinder:<type>`.
|
|
129
|
+
- **Blocking**: Feishu's **native task dependencies** — the canonical, UI-visible representation. `type: "prev"` means "blocks this task", so a child blocked by another records the blocker as `prev`:
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
lark-cli api POST /open-apis/task/v2/tasks/<CHILD_GUID>/add_dependencies --as user \
|
|
133
|
+
--data '{"dependencies": [{"task_guid": "<BLOCKER_GUID>", "type": "prev"}]}'
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
The reciprocal `next` edge appears on the blocker automatically. Drop an edge with `--data '{"dependencies": [{"task_guid": "<BLOCKER_GUID>"}]}'` against `.../remove_dependencies`. Dependencies are **not** settable through `tasks create` or `tasks patch` — the raw `api` call is the only route, so tickets must exist before they can be wired, which is why charting creates first and wires second.
|
|
137
|
+
|
|
138
|
+
- **Frontier query** — one call, because `subtasks list` returns *full* task objects rather than briefs:
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
lark-cli task subtasks list --as user --params '{"task_guid": "<MAP_GUID>", "page_size": 100, "user_id_type": "open_id"}'
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Each child carries `status`, `members`, `dependencies` and `description`. Filter locally: keep `status` of `todo`, drop any with a `members` entry whose `role` is `assignee` (claimed), and drop any whose `prev` dependencies include a task still `todo` (blocked). Blockers that are siblings on the same map are already in this response; resolve any others with `tasks get`. First in map order wins.
|
|
145
|
+
|
|
146
|
+
- **Claim**: `lark-cli task +assign --task-id <GUID> --add <your open_id> --as user` — the session's first write.
|
|
147
|
+
- **Resolve**: `+comment` with the answer, then `+complete`, then patch the map's `description` to append a context pointer to Decisions-so-far. That pointer's link must be the ticket's full applink URL — a Decisions-so-far line written as `[title](link)` is rejected by the description's URL validation, taking the whole map update with it.
|
|
148
|
+
|
|
149
|
+
## Re-reading the setup values
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
lark-cli task sections list --as user --params '{"resource_type": "tasklist", "resource_id": "6d628d79-cdfe-47cc-98f7-35561944494b", "page_size": 50}'
|
|
153
|
+
lark-cli task custom_fields list --as user --params '{"resource_type": "tasklist", "resource_id": "6d628d79-cdfe-47cc-98f7-35561944494b", "page_size": 50}'
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
`custom_fields list` returns each option's `guid` alongside its `name`, which is what the `Type` table above records.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Triage Labels
|
|
2
|
+
|
|
3
|
+
The skills speak in terms of five canonical triage roles. On this repo's tracker (Feishu Tasks) a role is not a label but a **section** of the Alphafox-Issues tasklist — a task sits in exactly one, so the state machine can't be violated. `issue-tracker.md` holds the section GUIDs and the command that moves a task between them.
|
|
4
|
+
|
|
5
|
+
| Label in mattpocock/skills | Section in our tracker | Meaning |
|
|
6
|
+
| -------------------------- | ---------------------- | ---------------------------------------- |
|
|
7
|
+
| _(no label yet)_ | `待分类` | Never triaged — the default section |
|
|
8
|
+
| `needs-triage` | `待评估` | Maintainer needs to evaluate this issue |
|
|
9
|
+
| `needs-info` | `待补充信息` | Waiting on reporter for more information |
|
|
10
|
+
| `ready-for-agent` | `可交给 Agent` | Fully specified, ready for an AFK agent |
|
|
11
|
+
| `ready-for-human` | `需人工处理` | Requires human implementation |
|
|
12
|
+
| `wontfix` | `不予处理` | Will not be actioned |
|
|
13
|
+
|
|
14
|
+
When a skill mentions a role (e.g. "apply the AFK-ready triage label"), move the task to the corresponding section from this table.
|
|
15
|
+
|
|
16
|
+
The remaining labels — the `bug` / `enhancement` categories and wayfinder's `wayfinder:<type>` — are **not** sections. They are options on the single-select `Type` field, because they coexist with a triage role rather than replacing it. `issue-tracker.md` holds their option GUIDs.
|
|
17
|
+
|
|
18
|
+
Editing the right-hand column here is not enough on its own: rename a section in Feishu and its GUID stays the same, so update this table and leave `issue-tracker.md`'s GUIDs alone. Adding or removing a section is the case that changes GUIDs, and then both files need re-syncing.
|
package/docs/e2e-staging.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# Staging E2E checklist (t101364 / t101369)
|
|
2
2
|
|
|
3
|
+
Run only against the stable public facade `https://staging.alphafox.app`. Do not use Preview URLs, internal service tokens, or production.
|
|
4
|
+
|
|
5
|
+
Staging CLI issuer: `https://staging.alphafox.app`. Test login (staging-only, gated on `ALPHAFOX_DEPLOY_ENV=staging`): `test@local.com` / `localtest`. Device Flow approve without a human click: `node scripts/e2e-staging-device-approve.mjs --user-code <code>`. Isolate credentials with `ALPHAFOX_CONFIG_DIR` and `ALPHAFOX_FORCE_FILE_KEYCHAIN=1`.
|
|
6
|
+
|
|
3
7
|
## Scenarios
|
|
4
8
|
|
|
5
9
|
1. Install CLI → `version` / `doctor`
|
|
@@ -12,13 +16,79 @@
|
|
|
12
16
|
8. Logout / revoke
|
|
13
17
|
9. Cross-env token rejection (prod token on staging)
|
|
14
18
|
|
|
15
|
-
## Evidence
|
|
19
|
+
## Evidence (2026-08-13) — vertical slice pass
|
|
20
|
+
|
|
21
|
+
Anonymous `GET https://staging.alphafox.app/api/v1/meta` → **HTTP 200**, `environment=staging`, `contractVersion=2026-08-13`, `commitSha=65d1f816007adc7acc09db5e86671931737e0379`, `x-request-id=66511bcf-dda9-4974-9378-3aacd8d938ff`. No 302 to `vercel.com/sso-api`. Vercel Authentication is disabled for the whole `alphafox-web` project (Preview `*.vercel.app` is also public; accepted).
|
|
22
|
+
|
|
23
|
+
CLI used: local `alphafox-cli` `fd04570747b1096b57a5a12ef20994b6c79531d7` (`dist/cli.js --profile staging --format json --no-input`). Config dir `/tmp/alphafox-e2e-staging`.
|
|
24
|
+
|
|
25
|
+
| Step | Result | Evidence |
|
|
26
|
+
|------|--------|----------|
|
|
27
|
+
| 1. `version` | pass | `contractsSha` matches freeze `5a4f9c0175951a6bedc52640fe917abe992ec824` |
|
|
28
|
+
| 1. `doctor` | pass | `apiBaseUrl` / issuer `https://staging.alphafox.app` |
|
|
29
|
+
| 2. password sign-in | pass | HTTP 200, session cookie, `userId=019f3073-307f-76e9-adf1-0203af9ab22b` |
|
|
30
|
+
| 2. Device Flow `--no-wait` | pass | `user_code` issued; verification `https://staging.alphafox.app/cli/device` |
|
|
31
|
+
| 2. device approve helper | pass | HTTP 200, `requestId=821fa89f-04c9-490a-8e45-88f83a7e69e6` |
|
|
32
|
+
| 2. `auth login --device-code` | pass | authenticated, `requestId=a90ef882-c341-4af5-b68c-f27b2a280bfd` |
|
|
33
|
+
| 3. `whoami` | pass | same `userId`, `requestId=8202f6b0-517d-44fa-acf3-0e19c39db7c7` |
|
|
34
|
+
| 3. `auth status --verify` | pass | `verified: true`, issuer/audience/clientId staging |
|
|
35
|
+
| 4. `GET /api/v1/trading/strategy-definitions` | pass | HTTP 200 via CLI, `requestId=22afdb63-d8cf-4512-a6b7-326f97a876a9` |
|
|
36
|
+
| 4. `GET /api/v1/exchange-connectors` | pass | HTTP 200, `requestId=1284cfe7-a06a-4f30-bac1-6322eaf05e1b` |
|
|
37
|
+
| 4. `GET /api/v1/trading/traders` | pass | HTTP 200, `requestId=c09ced70-d704-4b78-bc72-96a7048d4c7a` |
|
|
38
|
+
| 7. high-risk without `--yes` | pass | `api POST /api/v1/trading/traders/{id}/start` → **exit 10**, `confirmation_required` (no HTTP call) |
|
|
39
|
+
| Rate limit | pass (live) | 80 sequential `POST /api/auth/oauth/device/code` → first **HTTP 429** at request 72, `x-request-id=e585251a-2768-4cfc-aab6-77da1ae20570`, `Retry-After: 43`. Limit is 10/min **per serverless instance**; cross-instance windows do not share memory. Feishu alert is debounce 10 min/key via `ALPHAFOX_OPS_FEISHU_WEBHOOK_URL`. |
|
|
40
|
+
|
|
41
|
+
Login UI on the website is still OTP/passwordless. E2E used API password + the approve helper. Remaining human click if someone uses the browser `/cli/device` page: they must already have a web session (OTP unless they hit the sign-in API).
|
|
42
|
+
|
|
43
|
+
Web PR: https://github.com/alphafoxai/alphafox-web/pull/448 (head `65d1f816`). Do not enable production OAuth client or npm `latest` from this evidence.
|
|
44
|
+
|
|
45
|
+
## Evidence (2026-08-13) — leftover checklist (5 / 6 / 8 / 9)
|
|
46
|
+
|
|
47
|
+
Reused staging Device Flow session `userId=019f3073-307f-76e9-adf1-0203af9ab22b` (`whoami` `requestId=51222050-1226-4d2f-92d2-96059778fb36`). Anonymous `GET /api/v1/meta` still **HTTP 200**, `commitSha=65d1f816007adc7acc09db5e86671931737e0379`, `x-request-id=d0f964ef-1f5a-4271-ad6e-0cf0f57b6ae2`. CLI: local `alphafox-cli` `1db8f416` (`--profile staging --format json --no-input`). Staging is **not** MVP in-process stubs (`ALPHAFOX_PUBLIC_API_USE_MVP_HANDLERS` unset); product handlers go through the BFF / llm-gateway / trader.
|
|
48
|
+
|
|
49
|
+
| Step | Result | Evidence |
|
|
50
|
+
|------|--------|----------|
|
|
51
|
+
| 5. `POST /api/v1/chats` create | **pass** | HTTP **201**, `{chatId}`, `x-matched-path=/api/v1/chats`. Raw: `chatId=7fb9b5ce-f4c2-47e1-a8f6-804bcbfadedb`, `x-request-id=6a091c9b-e08d-4bb6-9e05-9e66e124f073`. CLI: `chatId=ce72df27-b207-49cd-a99a-d6d692e4e725`, `requestId=ea3f253d-3050-4060-9296-e56687c71313` |
|
|
52
|
+
| 5. same `Idempotency-Key` replay | **fail** | Expected HTTP 200 + same `chatId` / `replayed=true`. Got a **second HTTP 201** with a **new** chat. Raw replay `chatId=be5664ad-5fc8-4288-9a29-f865ec554dd6`, `x-request-id=ab54ca06-1b59-4b4c-89f2-4909204b8813`. CLI replay `chatId=d880e9e5-ae39-46c2-8719-6546f637575b`, `requestId=3fed3750-6564-45c9-94cf-89b6ac8634b4`. BFF `/api/chats` does not honor `Idempotency-Key` |
|
|
53
|
+
| 6. `POST /api/v1/backtests` `{}` | **fail** | HTTP **400** `chatId is required`, `x-request-id=946c02c7-1ce8-4ef9-9405-9cd407af3ace` |
|
|
54
|
+
| 6. `POST /api/v1/backtests` contract `{backtestSettings:{}}` | **fail** | HTTP **400** `unknown request keys: backtestSettings`, `x-request-id=49610211-7980-4726-97a1-47b87ee98bd5`. Facade BFF expects `{chatId, strategyId}`; contracts catalog wants `backtestSettings` |
|
|
55
|
+
| 6. `POST /api/v1/backtests` `{chatId, strategyId:1}` | **fail** | HTTP **404** `JOB_NOT_FOUND` `strategy not found` (trader `POST /v2/backtests`), `x-request-id=5715eb82-0fe8-427b-86e2-bb1ce1bd6029`. New chat has no compiled strategy |
|
|
56
|
+
| 6. get / stream / cancel (no live job) | **fail** (blocked by create) | Handlers are on the facade (not catch-all 404). Missing id `00000000-0000-0000-0000-000000000001`: GET **404** `JOB_NOT_FOUND` `x-request-id=406b0439-4c33-4f4c-b117-bac6787e7f59`; stream GET **404** `x-request-id=0e1f335b-2865-4fdf-ae5f-e5450514ad8a`; cancel POST **404** `x-request-id=8793823c-3ea8-405e-9c15-1eae47b4a4af`. Did **not** watch a live stream or cancel a created job |
|
|
57
|
+
| 8. `POST /api/auth/oauth/revoke` | **pass** | HTTP **200** `{revoked:true}`, `x-request-id=f8dbc0da-1784-46ba-8180-0ad56225e253` |
|
|
58
|
+
| 8. revoked AT on `GET /api/v1/me` | **pass** | HTTP **401** `unauthorized`, `x-request-id=992b12f5-f1b7-4750-ab7e-f162278be8e8` |
|
|
59
|
+
| 8. `auth logout` | **pass** | `localCleared: true`, `remoteRevoke: ok`, `fullyLoggedOut: true`. Keychain file removed. Follow-up `whoami` HTTP **401**, `requestId=c52c8c33-d898-4471-b008-27b4d558664a`, CLI exit 77 |
|
|
60
|
+
| 9. CLI prod-audience token → staging | **pass** | No HTTP. CLI **exit 77**, `subtype=cross_origin_token`, `status=403`: refuses to send tokens whose audience origin is `https://alphafox.app` to `https://staging.alphafox.app` |
|
|
61
|
+
| 9. unknown / prod-shaped bearer on staging `/api/v1/me` | **pass** (fail-closed) | No production OAuth client (not enabled). Unknown opaque bearer HTTP **401** `x-request-id=c73deb73-8ae4-472f-9d19-ad0b930f04fe`. Forged JWT with `iss=https://alphafox.app/api/auth` HTTP **401** `x-request-id=cb92f0f7-05ab-4e25-9455-a770467d4a07`. Staging token table does not accept foreign tokens |
|
|
62
|
+
|
|
63
|
+
### Leftover blockers (do not treat the full checklist as green)
|
|
64
|
+
|
|
65
|
+
- **Idempotency:** `chats.create` on staging creates a real llm-gateway chat (201) but ignores `Idempotency-Key`.
|
|
66
|
+
- **Backtest vertical slice:** cannot `create → stream → cancel` until a chat has an integer `strategyId`, and until the facade body matches contracts (`backtestSettings`) or the catalog is updated to `{chatId, strategyId}`.
|
|
67
|
+
- Parent Feishu task and t101364 (production publish / npm `latest` / production OAuth) stay **todo**. Do not merge web PR 448 to production `main` from this evidence.
|
|
68
|
+
|
|
69
|
+
## Evidence (2026-08-13) — gapfix retest (idempotency + backtests.create)
|
|
70
|
+
|
|
71
|
+
Anonymous `GET https://staging.alphafox.app/api/v1/meta` → **HTTP 200**, `environment=staging`, `contractVersion=2026-08-13`, `commitSha=fba21ef4b2c3909c51a5a19e2d2a45b30d1d598c`, `x-request-id=8702a21b-a749-4007-82eb-76ca1dd0caaf`. No SSO redirect. Device Flow `test@local.com`: approve `requestId=77293035-ebfd-4b03-b0e1-0779dce81fdd`, token `requestId=9dcc28c1-c1cc-49fc-9435-687dfdb591c2`, `whoami` `userId=019f3073-307f-76e9-adf1-0203af9ab22b` `requestId=1473f107-d742-42eb-99e7-b60c6b583928`. CLI local `92c8610250b30e7881f79a2fde60b00fe50b4628`, catalog `contractsSha=d1f184e3d72581f155497978880d9ab3029ff858`. Staging llm-gateway image `0a10d8e3a1304b04eff2f21c503922a5b7c11491` (workflow `31683345740` failed after the container was healthy: `/opt/alphafox/images/alphafox-llm-gateway.env` permission denied). Do not merge web PR 448 to production `main`.
|
|
72
|
+
|
|
73
|
+
| Step | Result | Evidence |
|
|
74
|
+
|------|--------|----------|
|
|
75
|
+
| 5. `POST /api/v1/chats` create | **pass** | HTTP **201** `chatId=8d1a569a-e19e-44f6-84e3-4f5f28f8e1e6`, `x-request-id=6cfe0cdc-db13-455b-87a7-7ddcc56aa30f`. CLI create `chatId=6ca09650-b259-4343-95fb-dc5891dd81a4`, `requestId=d647b1aa-2a07-4d42-80bc-b88384f6c8b5` |
|
|
76
|
+
| 5. same `Idempotency-Key` replay | **pass** | HTTP **200** same `chatId=8d1a569a-e19e-44f6-84e3-4f5f28f8e1e6`, `x-request-id=eaea63ac-07ff-49b9-b049-ff8c402a8fb6`. CLI replay same `chatId=6ca09650-b259-4343-95fb-dc5891dd81a4`, `requestId=3e9d6246-3f6c-4be3-a804-1de5d8abf772`. Same key + different title → HTTP **409** `IDEMPOTENCY_CONFLICT`, `x-request-id=f18d8aaf-16b2-4784-a42e-b2a088a586d3` |
|
|
77
|
+
| 6. `POST /api/v1/backtests` `{}` | **pass** (honest 400) | HTTP **400** `chatId is required`, `code=validation_error`, `x-request-id=811c9938-337e-4364-85c6-1a5e34f8427f`. CLI `requestId=2bd9e4c8-c33c-4123-84fa-ff999c1a2bed` |
|
|
78
|
+
| 6. `POST /api/v1/backtests` `{backtestSettings:{}}` | **pass** (honest 400) | HTTP **400** `chatId is required` (no longer `unknown request keys: backtestSettings`), `x-request-id=6ee3856f-73da-4888-b23d-f3981f8bec22`. CLI `requestId=1b26087b-73ea-4422-98d8-9050a403322e` |
|
|
79
|
+
| 6. `POST /api/v1/backtests` `{chatId, strategyId:1}` on empty chat | **pass** (honest 404) | HTTP **404** `STRATEGY_NOT_FOUND` (not `JOB_NOT_FOUND`), `x-request-id=1c58acd7-ed0f-40d3-9d71-618adbf634b5`, chat `88497dc2-1f86-4edc-ae08-f11e8ded57f8` |
|
|
80
|
+
| 6. `POST /api/v1/backtests` `{chatId}` omit strategyId | **pass** (honest 422) | HTTP **422** `CHAT_HAS_NO_COMPILED_STRATEGY`, `x-request-id=011c1b47-df76-4a5e-b045-799666eac304`. CLI `requestId=16e44bcc-8780-4146-8f72-8383bc415e07` |
|
|
81
|
+
| 6. create → stream → cancel (live job) | **fail** (blocked) | `test@local.com` has no compiled `strategyId`. Chat stream `x-request-id=2e26667b-0cfd-4bfe-9a75-25971120f1ab` ended `finishReason=tool-calls` (`modifyBacktestSettings`) without `save_strategy`. Follow-up create still **422** `x-request-id=37198479-fbd1-462f-8d5a-3a0d1d8f49bb` |
|
|
16
82
|
|
|
17
|
-
|
|
83
|
+
### Remaining blockers after this retest
|
|
18
84
|
|
|
19
|
-
-
|
|
20
|
-
-
|
|
85
|
+
- Live `backtests.create` → GET stream → POST cancel still needs a chat with a compiled `strategyId`. Generating one is the multi-step chat tool loop, not this facade fix.
|
|
86
|
+
- llm-gateway staging deploy workflow `31683345740` is red on a post-start file permission; the new image is serving (replay evidence above). Do not merge gateway `feat/chats-create-idempotency` to `main`.
|
|
87
|
+
- Parent Feishu task and t101364 stay **todo**. Do not enable production OAuth or npm `latest`.
|
|
21
88
|
|
|
22
|
-
|
|
89
|
+
## Policy
|
|
23
90
|
|
|
24
|
-
|
|
91
|
+
- Capture the real HTTP status, redirect, and request-id. Never fabricate staging success.
|
|
92
|
+
- External dependency outage → fail the test. No mock success, no silent skip.
|
|
93
|
+
- Parent Feishu task stays `todo` until production publish / OAuth latest is accepted.
|
|
94
|
+
- Do not enable production OAuth client or npm latest from a staging-only pass.
|