tirtc-device-builder 0.4.0 → 0.5.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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tirtc-device-builder",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Codex workflows for building and validating TiRTC device firmware across supported chip platforms.",
5
5
  "author": {
6
6
  "name": "TangeAI",
package/CHANGELOG.md CHANGED
@@ -2,6 +2,14 @@
2
2
 
3
3
  This project follows Semantic Versioning.
4
4
 
5
+ ## 0.5.0
6
+
7
+ - Split Hardware IR assessment into explicit `intake`, `build`, and `hil` phases so implementation- and build-resolvable facts do not require runtime proof before adapter work begins.
8
+ - Add `BUILD_VERIFIED` with exact artifact SHA-256 binding, while keeping L5/L6 runtime evidence exclusive to `HIL_VERIFIED`.
9
+ - Classify unresolved facts by their next evidence source and stop pre-build work only for facts that genuinely require user input, unavailable hardware/SDK, or a public-contract decision.
10
+ - Update the developer intake prompt and reporting guidance so missing serial access produces L2-L7 `SKIP` results without blocking L0/L1.
11
+ - Add phase regression coverage while retaining schema v1 compatibility and independent MJPEG, H.264, and H.265 profile validation.
12
+
5
13
  ## 0.4.0
6
14
 
7
15
  - Add Hardware IR v2 with selected MJPEG, H.264, or H.265 video profiles while retaining schema v1 compatibility.
package/README.md CHANGED
@@ -90,8 +90,8 @@ Skill 在 Codex 会话启动时被发现。安装完成后,关闭当前 Codex
90
90
 
91
91
  工程:<输出目录或现有工程的绝对路径>
92
92
 
93
- 请先运行 Doctor,分析全部资料并生成 Hardware IR v2。资料不足时列出最小补充项;达到 READY_TO_PORT 后再生成、适配和编译,并输出 TIRTC_PORTING_REPORT.md。
94
- 本轮不访问串口、不烧录、不擦除 NVS,也不要把任何凭证写入源码或报告。
93
+ 请先运行 Doctor,分析全部资料并生成 Hardware IR v2。把未知项区分为可由资料、实现、构建或 HIL 解决,以及必须由用户补充的阻塞项。READY_TO_PORT 表示资料足以开始设计,不要求最终 ELF;随后生成、适配和编译,以 artifact SHA-256 运行 build 阶段评估,并输出 TIRTC_PORTING_REPORT.md。
94
+ 本轮不访问串口、不烧录、不擦除 NVS;缺少串口只让 L2-L7 记为 SKIP,不得阻止 L0/L1。不要把任何凭证写入源码或报告。
95
95
  ```
96
96
 
97
97
  手头只有型号也可以开始:
@@ -450,26 +450,33 @@ python3 ~/.codex/skills/tirtc-esp32-builder/scripts/hardware_ir.py init \
450
450
  python3 ~/.codex/skills/tirtc-esp32-builder/scripts/hardware_ir.py validate \
451
451
  /absolute/path/hardware-ir.json
452
452
 
453
- python3 ~/.codex/skills/tirtc-esp32-builder/scripts/hardware_ir.py assess --strict \
453
+ python3 ~/.codex/skills/tirtc-esp32-builder/scripts/hardware_ir.py assess \
454
+ --phase intake --strict \
454
455
  /absolute/path/hardware-ir.json
455
456
  ```
456
457
 
457
- 能力门禁有四种状态:
458
+ 能力门禁有五种状态:
458
459
 
459
460
  | 状态 | 含义 | 怎么处理 |
460
461
  |---|---|---|
461
- | `NEEDS_CONFIRMATION` | 关键事实未知、冲突或只有单一来源 | 补原理图、BSP、数据手册或实测证据 |
462
+ | `NEEDS_CONFIRMATION` | 当前阶段的关键事实未知、冲突或证据等级不足 | 按资料、实现、构建、HIL 或用户输入来源继续闭环 |
462
463
  | `BLOCKED` | 现有硬件或 SDK 已确认不满足 | 更换硬件,补编码/播放路径,或取得匹配 SDK |
463
464
  | `READY_TO_PORT` | 资料足以开始生成和板级实现 | 进入工程生成与编译 |
465
+ | `BUILD_VERIFIED` | 精确 artifact 通过源码、编译和 post-link 门禁 | 记录 BIN/ELF SHA-256;按授权进入实机验收 |
464
466
  | `HIL_VERIFIED` | 已完成端到端实机验证 | 固定版本并保存证据 |
465
467
 
466
468
  `assess --strict` 在条件不足时返回非零,这是门禁在阻止过早生成,不代表脚本损坏。
467
469
 
468
- Hardware IR v2 只有在运行证据绑定到同一固件 SHA-256 时才会给出 `HIL_VERIFIED`:
470
+ 构建后运行 build 阶段;只有运行证据绑定到同一固件 SHA-256 时,hil 阶段才会给出 `HIL_VERIFIED`:
469
471
 
470
472
  ```bash
471
473
  python3 ~/.codex/skills/tirtc-esp32-builder/scripts/hardware_ir.py assess \
472
474
  /absolute/path/hardware-ir.json \
475
+ --phase build --artifact-sha256 <64-character-sha256> --strict
476
+
477
+ python3 ~/.codex/skills/tirtc-esp32-builder/scripts/hardware_ir.py assess \
478
+ /absolute/path/hardware-ir.json \
479
+ --phase hil \
473
480
  --artifact-sha256 <64-character-sha256> --strict
474
481
  ```
475
482
 
@@ -750,7 +757,7 @@ WSL 默认不一定能看到 USB 设备。按 [Microsoft WSL USB 连接说明](h
750
757
 
751
758
  ### Hardware IR 一直是 `NEEDS_CONFIRMATION`
752
759
 
753
- 先查看各个未知项要求什么来源。常见缺口包括准确的 PCB 版本、摄像头数据格式、所选视频 profile 的完整输出路径、Codec 时钟、功放使能脚、配网方法和可工作的厂商示例。不要从相似开发板复制管脚来填补这些信息。
760
+ 先确认当前运行的是 intake、build 还是 hil 阶段,再标记每个未知项的下一证据来源。资料/固定源码可解决的事实继续检查;adapter 或 ELF 可解决的事实进入对应实现/构建层;运行指标留到 HIL。只有准确 PCB 版本、关键连接、器件身份、产品合同或不可用 SDK 等无法安全推导的事实需要用户补充。不要从相似开发板复制管脚来填补这些信息。
754
761
 
755
762
  ### 工程能编译,浏览器没有画面
756
763
 
@@ -874,8 +881,8 @@ gh release create kit-esp32s3-v1.0.0 \
874
881
 
875
882
  ```bash
876
883
  npm test
877
- git tag -a v0.4.0 -m "v0.4.0"
878
- git push origin v0.4.0
884
+ git tag -a v0.5.0 -m "v0.5.0"
885
+ git push origin v0.5.0
879
886
  ```
880
887
 
881
888
  不要重复发布已经存在的 npm 版本。版本变化同步更新 `package.json`、`.codex-plugin/plugin.json` 和发布说明。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tirtc-device-builder",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Install and run TiRTC device-development skills for Codex.",
5
5
  "license": "MIT",
6
6
  "author": {
@@ -14,7 +14,9 @@ Turn board evidence into an evidence-backed ESP-IDF project. Treat the Hardware
14
14
  3. Run the Doctor through the managed environment helper when one exists; otherwise run `python3 <skill-dir>/scripts/doctor.py --expected-idf 5.5 --target esp32s3`. Add `--require-workspace` when generation or repository reference documents are needed; a self-contained generated project can instead resolve its bundled SDK through `--project`. Resolve every required failure before claiming build readiness.
15
15
  4. Read [workflow.md](references/workflow.md). Select the registered-board, new-board intake, or existing-project branch. The branch is selected when every supplied artifact has been accounted for and the exact board revision is known or explicitly unresolved.
16
16
  5. Read [hardware-ir.md](references/hardware-ir.md) when a Hardware IR must be created or updated. New intake uses schema v2; schema v1 remains readable for existing H.264 projects. Record a source and verification level for every hardware fact that affects a requested feature.
17
- 6. Run `python3 <skill-dir>/scripts/hardware_ir.py validate <hardware-ir.json>` and then `assess --strict`. Generation may proceed for a requested feature only when it is `READY_TO_PORT` or `HIL_VERIFIED`; otherwise report the exact missing evidence and continue with safe discovery or scaffolding only.
17
+ 6. Run `python3 <skill-dir>/scripts/hardware_ir.py validate <hardware-ir.json>` and then `assess --phase intake --strict`. `READY_TO_PORT` means the evidence is sufficient to design the adapter without guessing wiring or changing an unapproved public contract; it does not require a final ELF or runtime measurements.
18
+
19
+ Before stopping on `NEEDS_CONFIRMATION`, classify every unresolved fact as `source_resolvable`, `implementation_resolvable`, `build_resolvable`, `hil_resolvable`, or `user_blocked`. Resolve source facts by inspection. For implementation/build facts, record the evidenced design plan at `corroborated`, generate a compile-safe adapter, and verify it at L1. Defer HIL-only measurements to L2-L7 when hardware access is unavailable. Stop before implementation only for `user_blocked` facts such as unknown wiring or identity, a missing product choice, an unavailable SDK, or a required public-contract change.
18
20
 
19
21
  ## Build the project
20
22
 
@@ -26,11 +28,15 @@ Keep the Skill board-agnostic. The prompt supplies product intent and artifact l
26
28
 
27
29
  Run focused tests before ESP-IDF build. Resolve the TiRTC SDK target and `manifest/build-contract.env` against the generated `sdkconfig`; a mismatched precompiled SDK is a blocked build, not a code-generation problem.
28
30
 
31
+ After a successful build, promote only facts actually established by source, compile, or post-link gates to `build_verified`. Hash the exact BIN/ELF and run `hardware_ir.py assess --phase build --artifact-sha256 <sha256> --strict`. A design plan at `corroborated` can pass intake but cannot pass the build phase.
32
+
29
33
  ## Flash and verify
30
34
 
31
35
  Flash only when the user requested hardware mutation and the exact serial port and chip have been resolved. When more than one candidate device exists, obtain the target choice before writing. Keep credentials outside generated files and redact device keys, Wi-Fi passwords, MQTT/WHIP tokens, and user media from logs and reports.
32
36
 
33
- Read [reporting.md](references/reporting.md) before end-to-end verification. Report every acceptance level as `PASS`, `FAIL`, or `SKIP`, with commands and evidence. Bind HIL observations to the exact firmware SHA-256 with `assess --artifact-sha256`; an older artifact cannot verify a newer build. A build-only result is not H5 or AI completion; missing hardware, browser, account, service, or network evidence remains an explicit `SKIP` or blocker.
37
+ Read [reporting.md](references/reporting.md) before end-to-end verification. Report every acceptance level as `PASS`, `FAIL`, or `SKIP`, with commands and evidence. Bind HIL observations to the exact firmware SHA-256 with `assess --phase hil --artifact-sha256`; an older artifact cannot verify a newer build. A build-only result is not H5 or AI completion; missing hardware, browser, account, service, or network evidence remains an explicit `SKIP` or blocker.
38
+
39
+ For HIL assessment, run `hardware_ir.py assess --phase hil --artifact-sha256 <sha256> --strict`. The HIL phase first requires build-verified paths, then promotes only features whose L5/L6 runtime evidence matches that exact artifact.
34
40
 
35
41
  ## Finish
36
42
 
@@ -25,7 +25,7 @@ $tirtc-esp32-builder
25
25
  Wi-Fi 与设备绑定:<指定方案/根据 BSP 和平台合同选择>
26
26
  工程:<输出目录或现有工程的绝对路径>
27
27
 
28
- 先运行 Doctor,再生成 Hardware IR v2。达到 READY_TO_PORT 后完成板级适配和编译,输出 TIRTC_PORTING_REPORT.md。
28
+ 先运行 Doctor,再生成 Hardware IR v2READY_TO_PORT 只表示资料足以设计板级适配;随后完成适配和编译,以精确 artifact SHA-256 运行 build 阶段评估并输出 TIRTC_PORTING_REPORT.md。
29
29
  本轮不访问串口、不烧录、不擦除 NVS,也不把凭证写入源码或报告。
30
30
  ```
31
31
 
@@ -92,10 +92,14 @@ python3 <skill-dir>/scripts/doctor.py \
92
92
  ```bash
93
93
  python3 <skill-dir>/scripts/hardware_ir.py init /tmp/hardware-ir.json
94
94
  python3 <skill-dir>/scripts/hardware_ir.py validate /tmp/hardware-ir.json
95
- python3 <skill-dir>/scripts/hardware_ir.py assess --strict /tmp/hardware-ir.json
95
+ python3 <skill-dir>/scripts/hardware_ir.py assess --phase intake --strict /tmp/hardware-ir.json
96
+ python3 <skill-dir>/scripts/hardware_ir.py assess --phase build \
97
+ --artifact-sha256 <64-character-sha256> --strict /tmp/hardware-ir.json
98
+ python3 <skill-dir>/scripts/hardware_ir.py assess --phase hil \
99
+ --artifact-sha256 <64-character-sha256> --strict /tmp/hardware-ir.json
96
100
  ```
97
101
 
98
- `init` 默认创建 schema v2;v1 仅用于兼容已有 H.264 IR。`BLOCKED` 表示资料已确认硬件/合同/资源或凭证策略不满足;`NEEDS_CONFIRMATION` 表示仍有未知项或只有单一来源;`READY_TO_PORT` 表示可以生成并实现板级适配;`HIL_VERIFIED` 还要求 `--artifact-sha256` 匹配该固件的 L5/L6 运行证据。
102
+ `init` 默认创建 schema v2;v1 仅用于兼容已有 H.264 IR。`BLOCKED` 表示资料已确认硬件/合同/资源或凭证策略不满足;`NEEDS_CONFIRMATION` 表示当前阶段仍有未知项或证据不足;`READY_TO_PORT` 表示可以生成并实现板级适配;`BUILD_VERIFIED` 表示精确 artifact 已通过源码、编译和 post-link 门禁;`HIL_VERIFIED` 还要求同一 SHA-256 的 L5/L6 运行证据。
99
103
 
100
104
  ## 当前边界
101
105
 
@@ -30,10 +30,12 @@ $tirtc-esp32-builder
30
30
 
31
31
  执行要求:
32
32
  1. 先运行 Device Kit Doctor,读完全部资料,再生成 Hardware IR v2。没有证据的器件、GPIO 和媒体能力保持未知。
33
- 2. 资料不足时,列出矛盾、缺失项和最小补充动作;达到 READY_TO_PORT 后再生成板级 adapter、编译并记录 BIN/ELF SHA-256。
34
- 3. 移植前核对媒体合同、板级资源、内存预算、配网和绑定状态。具体板卡参数只写入该板的 Hardware IR adapter
35
- 4. 输出 TIRTC_PORTING_REPORT.md,并把编译结果与 L2-L7 实机验收分开记录。
36
- 5. 本轮不访问串口、不烧录、不擦除 NVS。Wi-Fi 密码、设备密钥、token、私钥和用户音视频也不能写入工程或报告。
33
+ 2. 把每个未知项标为 source_resolvable、implementation_resolvable、build_resolvable、hil_resolvable user_blocked。先通过资料和固定版本源码解决 source_resolvable;只有 user_blocked 会阻止开始 adapter 开发。
34
+ 3. READY_TO_PORT 表示硬件身份、连接、媒体合同和资源设计已经有证据,足以开始实现;它不要求最终 ELF 或实机数据。可通过实现或构建解决的项目必须继续生成 compile-safe adapter、锁定依赖、运行门禁并编译。
35
+ 4. 移植前核对媒体合同、板级资源、静态内存预算、配网和绑定状态。构建后只把源代码、锁定依赖、编译或 post-link 门禁实际证明的事实升级为 build_verified。
36
+ 5. 记录 BIN/ELF 路径、大小和 SHA-256,运行 build 阶段评估并输出 TIRTC_PORTING_REPORT.md。编译结果必须与 L2-L7 实机验收分开。
37
+ 6. 本轮不访问串口、不烧录、不擦除 NVS;因此缺少启动、浏览器、实体声音和运行时资源数据时,把相应 L2-L7 标为 SKIP,不得阻止 L0/L1。
38
+ 7. Wi-Fi 密码、设备密钥、token、私钥和用户音视频不能写入工程或报告。
37
39
  ```
38
40
 
39
41
  ## 只有已知时才补充
@@ -46,5 +48,6 @@ $tirtc-esp32-builder
46
48
  - 凭证保存与重配方式、已有绑定处理和独立清除入口;
47
49
  - 已知资料矛盾、实机 PID、串口日志或已知良好固件 SHA-256;
48
50
  - 非默认 ESP-IDF、TiRTC SDK、Device Kit、服务发现地址或 HTTP/HTTPS 分阶段要求。
51
+ - 若用于 clean-room 验证,明确允许的资料根目录,并列出不得读取的历史工程或旧 artifact。
49
52
 
50
53
  直接把它们追加在提示词末尾即可,不必逐项填表。
@@ -1,6 +1,6 @@
1
1
  # Capability rules
2
2
 
3
- Run `hardware_ir.py assess --strict` before generation. Hardware IR v2 validates the selected product contract rather than assuming one video codec or provisioning method. Schema v1 remains readable for existing H.264 projects.
3
+ Run `hardware_ir.py assess --phase intake --strict` before generation. Hardware IR v2 validates the selected product contract rather than assuming one video codec or provisioning method. Schema v1 remains readable for existing H.264 projects.
4
4
 
5
5
  ## Current starter contract
6
6
 
@@ -11,7 +11,9 @@ Run `hardware_ir.py assess --strict` before generation. Hardware IR v2 validates
11
11
  | `h5_talkback` | G.711 A-law, 8 kHz downlink decode and speaker path for stream 14; audio controller/GPIO ownership and memory budget resolved |
12
12
  | `ai_talk` | A-law 8 kHz microphone and speaker paths for AI stream 1, started only after `start_session`; audio ownership/channel mapping and memory budget resolved |
13
13
 
14
- The complete media path must be at least `corroborated` to become `READY_TO_PORT`. A path with unknown facts is `NEEDS_CONFIRMATION`; a confirmed missing or incompatible resource is `BLOCKED`.
14
+ Hardware identity, wiring, the selected media contract, and the adapter/resource design must be at least `corroborated` to become `READY_TO_PORT`. At intake, `available=true` means a pinned source path can implement the selected profile; it does not claim that the final adapter or physical media path has run. Implementation composition, final ELF policy, runtime memory margin, browser media, and stability belong to the build or HIL phases.
15
+
16
+ A design fact that is still unknown is `NEEDS_CONFIRMATION`; a confirmed missing or incompatible resource is `BLOCKED`. Facts that can be resolved safely through source inspection, adapter implementation, compilation, or post-link inspection should be resolved in that layer rather than converted into a request for HIL evidence.
15
17
 
16
18
  ## Selected video profiles
17
19
 
@@ -25,14 +27,16 @@ Hardware IR v2 stores one or more `camera.video_profiles` and exactly one select
25
27
 
26
28
  Available but unselected profiles do not satisfy or block the selected contract. Stream IDs and codec support must come from the applicable ThingConnect/H5 contract, not this table alone.
27
29
 
28
- ## Project gates
30
+ ## Phased project gates
29
31
 
30
- Schema v2 also requires:
32
+ The intake phase requires:
31
33
 
32
- - one evidenced I2C driver family when I2C is used;
34
+ - one selected and evidenced I2C driver-family plan when I2C is used;
33
35
  - a selected Wi-Fi credential method that is available, reprovisionable, and keeps credentials outside source control;
34
36
  - one evidenced binding method—verification code, factory-bound identity, development credentials, or documented custom flow—plus stored-binding behavior and reset control;
35
- - feature-specific I2S/GPIO ownership, channel/TDM mapping, realtime camera policy, and startup/media memory budget.
37
+ - feature-specific I2S/GPIO ownership plans, channel/TDM mapping, realtime camera policy, and a static startup/media memory budget.
38
+
39
+ At intake, `corroborated` on these fields means the design is resolved from authoritative sources and is safe to implement. After compilation, promote a field to `build_verified` only when the generated source, component lock, compile result, or post-link gate establishes it. The build phase requires an exact artifact SHA-256 and returns `BUILD_VERIFIED`. Runtime measurements never need to be invented to pass intake or build.
36
40
 
37
41
  SoftAP is one Wi-Fi option, not a universal requirement. BLE, SmartConfig, factory NVS, development configuration, or a documented custom method can satisfy intake when the selected path is evidenced. Committed plaintext credentials are always `BLOCKED`.
38
42
 
@@ -43,7 +47,7 @@ SoftAP is one Wi-Fi option, not a universal requirement. BLE, SmartConfig, facto
43
47
  - Start AI media only after the successful `start_session` response; stop and flush media before disconnecting.
44
48
  - Copy SDK callback payloads into bounded queues before returning. Perform decoding, playback, HTTP, and lifecycle changes outside callbacks.
45
49
  - Use monotonic timestamps and session generation to reject stale frames and delayed callbacks.
46
- - Record runtime evidence with the exact BIN/ELF SHA-256. Use `assess --artifact-sha256 <sha>`; documentation verification alone never becomes v2 `HIL_VERIFIED`.
50
+ - Record runtime evidence with the exact BIN/ELF SHA-256. Use `assess --phase hil --artifact-sha256 <sha>`; documentation verification alone never becomes v2 `HIL_VERIFIED`.
47
51
 
48
52
  ## Typical blocked cases
49
53
 
@@ -27,6 +27,8 @@ Verification levels are ordered:
27
27
  4. `hardware_verified`: the local peripheral works on the exact board revision.
28
28
  5. `hil_verified`: retained for legacy facts; schema v2 feature HIL additionally requires matching artifact evidence.
29
29
 
30
+ Use the same fact across phases without overstating it. `corroborated` means authoritative sources establish an implementable design; `build_verified` means the generated source, locked dependencies, compile, or post-link gates establish that implementation; runtime behavior requires matching artifact evidence. For example, a corroborated single-I2C-family field records a dependency plan, while its build-verified form records the final ELF audit.
31
+
30
32
  ## Schema v2 contracts
31
33
 
32
34
  The IR contains:
@@ -46,11 +48,22 @@ The selected video profile controls assessment. An unselected H.264 fallback can
46
48
 
47
49
  ## Artifact-bound HIL
48
50
 
51
+ Run the phase gates in order:
52
+
53
+ ```bash
54
+ python3 <skill-dir>/scripts/hardware_ir.py assess hardware-ir.json \
55
+ --phase intake --strict
56
+ python3 <skill-dir>/scripts/hardware_ir.py assess hardware-ir.json \
57
+ --phase build --artifact-sha256 <64-character-sha256> --strict
58
+ ```
59
+
60
+ The intake phase returns `READY_TO_PORT`; the build phase returns `BUILD_VERIFIED`. Missing serial or browser access does not block either phase.
61
+
49
62
  Run:
50
63
 
51
64
  ```bash
52
65
  python3 <skill-dir>/scripts/hardware_ir.py assess hardware-ir.json \
53
- --artifact-sha256 <64-character-sha256> --strict
66
+ --phase hil --artifact-sha256 <64-character-sha256> --strict
54
67
  ```
55
68
 
56
69
  H5 features require matching L5 evidence; AI requires matching L6 evidence. Evidence from an older firmware remains provenance but does not verify the current artifact.
@@ -36,7 +36,7 @@ For every method, keep device credentials outside source control, handle an alre
36
36
 
37
37
  Platform service discovery and the TiRTC SDK service endpoint are different settings. Record both. HTTPS requires valid time, DNS, certificate validation, TLS client/TLS 1.2, and enough contiguous internal memory.
38
38
 
39
- Before sizing TiRTC buffers, measure internal free/largest blocks, PSRAM, frame size distribution, queue watermarks, and send/drop rates. A larger queue can prevent drops, exhaust startup memory, or add buffer latency. If an authorized HTTP baseline exists, stage transport changes separately from media changes and retain the HTTPS requirements as a pending acceptance item.
39
+ Define a conservative static budget before implementation using the locked SDK contract, framebuffer geometry, DMA/queue bounds, task stacks, and an internal-memory reserve. Before claiming runtime margin or tuning TiRTC buffers, measure internal free/largest blocks, PSRAM, frame size distribution, queue watermarks, and send/drop rates on the exact artifact. A larger queue can prevent drops, exhaust startup memory, or add buffer latency. If an authorized HTTP baseline exists, stage transport changes separately from media changes and retain the HTTPS requirements as a pending acceptance item.
40
40
 
41
41
  ## Network and media evidence
42
42
 
@@ -16,11 +16,13 @@ Use [the report template](../assets/report-template.md) and preserve separate `P
16
16
  | L6 AI | Token, WHIP, `start_session`, bidirectional audio, stop, and H5 recovery work |
17
17
  | L7 Stability | Requested weak-network, repeated-session, resource, and soak criteria pass |
18
18
 
19
+ Run and record the intake assessment before L0, the build assessment with the exact artifact SHA-256 at L1, and the HIL assessment only when matching runtime evidence exists. Missing serial or browser access is a `SKIP` for the affected L2-L7 levels, not an L0/L1 failure.
20
+
19
21
  ## Evidence
20
22
 
21
23
  Record exact board revision, selected media, Wi-Fi, and binding profiles, toolchain and SDK versions, source/adapter revisions, commands, return codes, firmware size and SHA-256, serial port/chip, sanitized log paths, browser or platform observations, and every unavailable dependency.
22
24
 
23
- Runtime evidence belongs to one artifact. Label superseded artifacts and keep their observations as history; do not promote them to the current BIN/ELF. For HIL assessment, add the full SHA-256 to `runtime_evidence` and run `hardware_ir.py assess --artifact-sha256 <sha> --strict`.
25
+ Runtime evidence belongs to one artifact. Label superseded artifacts and keep their observations as history; do not promote them to the current BIN/ELF. For HIL assessment, add the full SHA-256 to `runtime_evidence` and run `hardware_ir.py assess --phase hil --artifact-sha256 <sha> --strict`.
24
26
 
25
27
  For L3-L7 media runs, capture the signals that distinguish software regressions from environment changes: onboarding/binding state, BSSID/channel/RSSI, reconnect or roaming events, audio/video send/receive/drop/error counters, queue watermarks, camera overflow count, internal heap/largest block, PSRAM, and measured latency when available. Never record credential values.
26
28
 
@@ -23,8 +23,8 @@ Use this branch when the user supplies a board model, vendor URL, schematic, BOM
23
23
  3. Prefer official schematic/BOM and BSP facts. For a PDF schematic, inspect page labels and net names; prefer an exported netlist, pin CSV, or vendor board definition when available.
24
24
  4. Cross-check critical pins, clocks, power enables, reset lines, sensor/codec variants, ESP-IDF version, resource ownership, and onboarding behavior across at least two independent artifacts when possible.
25
25
  5. Create the Hardware IR v2. Use `null` for unknown facts and retain contradictory values as an explicit issue instead of selecting one silently. Store concrete board values in the IR/adapter rather than Skill files.
26
- 6. Validate and assess requested features. Ask only for unresolved facts that block the next safe step. SoftAP is optional when another evidenced Wi-Fi credential method is available, keeps credentials outside source, and defines reprovisioning.
27
- 7. When all requirements reach `READY_TO_PORT`, generate the starter and implement the board adapter. When requirements remain blocked, generate only the IR, capability report, and an optional compile-safe skeleton if requested.
26
+ 6. Validate and run the intake assessment. Classify unresolved facts by their next evidence source: source, implementation, build, HIL, or user input. Ask only for `user_blocked` facts that prevent a safe design. SoftAP is optional when another evidenced Wi-Fi credential method is available, keeps credentials outside source, and defines reprovisioning.
27
+ 7. When hardware identity, wiring, product contracts, and an evidenced resource plan reach `READY_TO_PORT`, generate the starter and implement the board adapter. Generate a compile-safe adapter by default when remaining uncertainty is implementation-, build-, or HIL-resolvable. Stop at the IR/report only when missing user evidence or an incompatible dependency makes a safe implementation impossible.
28
28
 
29
29
  The branch is complete when every supplied artifact maps to an IR fact, provenance entry, contradiction, or declared irrelevant item.
30
30
 
@@ -68,4 +68,12 @@ The stable modules own stream IDs, negotiated/contracted formats, TiRTC callback
68
68
 
69
69
  Use a bounded loop per layer: diagnose one failing invariant, make the smallest correction, and rerun that layer before moving forward. Change one high-risk variable per HIL comparison. Turn reusable invariants into tests or post-link gates. Stop and report when the remaining failure requires unavailable hardware, credentials, a new SDK binary, a public protocol change, or a user choice.
70
70
 
71
+ Run the assessor once per layer:
72
+
73
+ - `--phase intake`: corroborated design evidence; success is `READY_TO_PORT`.
74
+ - `--phase build --artifact-sha256 <sha>`: compile/post-link evidence; success is `BUILD_VERIFIED`.
75
+ - `--phase hil --artifact-sha256 <sha>`: matching L5/L6 runtime evidence; success is `HIL_VERIFIED`.
76
+
77
+ No serial authorization is required for L0/L1. When serial, browser, account, service, or network access is unavailable, complete the safe build work and report the affected L2-L7 levels as `SKIP`.
78
+
71
79
  Do not use successful compilation as evidence for camera frames, speaker output, Web rendering, AI audio, or long-run stability. Bind every runtime conclusion to the tested firmware SHA-256.
@@ -25,7 +25,12 @@ VERIFICATION_LEVELS = {
25
25
  "hardware_verified": 4,
26
26
  "hil_verified": 5,
27
27
  }
28
- READY_STATUSES = {"READY_TO_PORT", "HIL_VERIFIED"}
28
+ ASSESSMENT_PHASES = {"intake", "build", "hil"}
29
+ PHASE_SUCCESS_STATUSES = {
30
+ "intake": {"READY_TO_PORT", "HIL_VERIFIED"},
31
+ "build": {"BUILD_VERIFIED", "HIL_VERIFIED"},
32
+ "hil": {"HIL_VERIFIED"},
33
+ }
29
34
  VIDEO_CONTRACTS = {
30
35
  "mjpeg": "jpeg_complete_frames",
31
36
  "h264": "h264_annex_b_access_units",
@@ -482,7 +487,15 @@ def validate_ir(data: dict[str, Any]) -> list[str]:
482
487
  return errors
483
488
 
484
489
 
485
- def codec_requirement(media: dict[str, Any], section: str) -> Requirement:
490
+ def minimum_verification_for_phase(phase: str) -> str:
491
+ if phase == "intake":
492
+ return "corroborated"
493
+ return "build_verified"
494
+
495
+
496
+ def codec_requirement(
497
+ media: dict[str, Any], section: str, minimum_verification: str = "corroborated"
498
+ ) -> Requirement:
486
499
  present = media.get("present")
487
500
  if present is None:
488
501
  return "NEEDS_CONFIRMATION", f"{section} presence is unknown", 0
@@ -496,17 +509,20 @@ def codec_requirement(media: dict[str, Any], section: str) -> Requirement:
496
509
  continue
497
510
  verification = codec.get("verification")
498
511
  level = VERIFICATION_LEVELS.get(verification, 0)
499
- if level < VERIFICATION_LEVELS["corroborated"]:
512
+ if level < VERIFICATION_LEVELS[minimum_verification]:
500
513
  return (
501
514
  "NEEDS_CONFIRMATION",
502
- f"{section} A-law 8 kHz path is only {verification}",
515
+ f"{section} A-law 8 kHz path is only {verification}; "
516
+ f"{minimum_verification} is required",
503
517
  level,
504
518
  )
505
519
  return "SATISFIED", f"{section} provides A-law 8 kHz", level
506
520
  return "BLOCKED", f"{section} has no A-law 8 kHz path", 0
507
521
 
508
522
 
509
- def legacy_video_requirement(camera: dict[str, Any]) -> Requirement:
523
+ def legacy_video_requirement(
524
+ camera: dict[str, Any], minimum_verification: str = "corroborated"
525
+ ) -> Requirement:
510
526
  present = camera.get("present")
511
527
  if present is None:
512
528
  return "NEEDS_CONFIRMATION", "camera presence is unknown", 0
@@ -530,10 +546,11 @@ def legacy_video_requirement(camera: dict[str, Any]) -> Requirement:
530
546
  return "BLOCKED", "key-frame requests cannot reach the encoder", 0
531
547
  verification = h264.get("verification")
532
548
  level = VERIFICATION_LEVELS.get(verification, 0)
533
- if level < VERIFICATION_LEVELS["corroborated"]:
549
+ if level < VERIFICATION_LEVELS[minimum_verification]:
534
550
  return (
535
551
  "NEEDS_CONFIRMATION",
536
- f"H.264 Annex-B path is only {verification}",
552
+ f"H.264 Annex-B path is only {verification}; "
553
+ f"{minimum_verification} is required",
537
554
  level,
538
555
  )
539
556
  return "SATISFIED", "camera provides H.264 Annex-B and IDR control", level
@@ -548,7 +565,9 @@ def selected_item(items: Any, selected_id: Any) -> dict[str, Any] | None:
548
565
  return None
549
566
 
550
567
 
551
- def video_requirement_v2(camera: dict[str, Any]) -> Requirement:
568
+ def video_requirement_v2(
569
+ camera: dict[str, Any], minimum_verification: str = "corroborated"
570
+ ) -> Requirement:
552
571
  present = camera.get("present")
553
572
  if present is None:
554
573
  return "NEEDS_CONFIRMATION", "camera presence is unknown", 0
@@ -585,10 +604,11 @@ def video_requirement_v2(camera: dict[str, Any]) -> Requirement:
585
604
  return "BLOCKED", "H5 refresh requests cannot reach the media pipeline", 0
586
605
  verification = profile.get("verification")
587
606
  level = VERIFICATION_LEVELS.get(verification, 0)
588
- if level < VERIFICATION_LEVELS["corroborated"]:
607
+ if level < VERIFICATION_LEVELS[minimum_verification]:
589
608
  return (
590
609
  "NEEDS_CONFIRMATION",
591
- f"selected {codec} path is only {verification}",
610
+ f"selected {codec} path is only {verification}; "
611
+ f"{minimum_verification} is required",
592
612
  level,
593
613
  )
594
614
  return (
@@ -599,7 +619,10 @@ def video_requirement_v2(camera: dict[str, Any]) -> Requirement:
599
619
 
600
620
 
601
621
  def verified_bool_requirement(
602
- section: dict[str, Any], field: str, label: str
622
+ section: dict[str, Any],
623
+ field: str,
624
+ label: str,
625
+ minimum_verification: str = "corroborated",
603
626
  ) -> Requirement:
604
627
  value = section.get(field)
605
628
  if value is None:
@@ -608,12 +631,18 @@ def verified_bool_requirement(
608
631
  return "BLOCKED", f"{label} is unresolved", 0
609
632
  verification = section.get("verification")
610
633
  level = VERIFICATION_LEVELS.get(verification, 0)
611
- if level < VERIFICATION_LEVELS["corroborated"]:
612
- return "NEEDS_CONFIRMATION", f"{label} is only {verification}", level
634
+ if level < VERIFICATION_LEVELS[minimum_verification]:
635
+ return (
636
+ "NEEDS_CONFIRMATION",
637
+ f"{label} is only {verification}; {minimum_verification} is required",
638
+ level,
639
+ )
613
640
  return "SATISFIED", f"{label} is resolved", level
614
641
 
615
642
 
616
- def i2c_requirement(resources: dict[str, Any]) -> Requirement:
643
+ def i2c_requirement(
644
+ resources: dict[str, Any], minimum_verification: str = "corroborated"
645
+ ) -> Requirement:
617
646
  i2c = resources.get("i2c", {})
618
647
  used = i2c.get("used")
619
648
  if used is None:
@@ -626,11 +655,16 @@ def i2c_requirement(resources: dict[str, Any]) -> Requirement:
626
655
  if family == "none":
627
656
  return "BLOCKED", "I2C is used but no driver family is selected", 0
628
657
  return verified_bool_requirement(
629
- i2c, "single_driver_family", f"single {family} I2C driver family"
658
+ i2c,
659
+ "single_driver_family",
660
+ f"single {family} I2C driver family plan",
661
+ minimum_verification,
630
662
  )
631
663
 
632
664
 
633
- def i2s_requirement(resources: dict[str, Any]) -> Requirement:
665
+ def i2s_requirement(
666
+ resources: dict[str, Any], minimum_verification: str = "corroborated"
667
+ ) -> Requirement:
634
668
  i2s = resources.get("i2s", {})
635
669
  used = i2s.get("used")
636
670
  if used is None:
@@ -640,11 +674,14 @@ def i2s_requirement(resources: dict[str, Any]) -> Requirement:
640
674
  return verified_bool_requirement(
641
675
  i2s,
642
676
  "controller_and_gpio_ownership_resolved",
643
- "I2S controller and GPIO ownership",
677
+ "I2S controller and GPIO ownership plan",
678
+ minimum_verification,
644
679
  )
645
680
 
646
681
 
647
- def audio_mapping_requirement(resources: dict[str, Any]) -> Requirement:
682
+ def audio_mapping_requirement(
683
+ resources: dict[str, Any], minimum_verification: str = "corroborated"
684
+ ) -> Requirement:
648
685
  channel = resources.get("audio_channel_mapping", {})
649
686
  required = channel.get("required")
650
687
  if required is None:
@@ -652,23 +689,32 @@ def audio_mapping_requirement(resources: dict[str, Any]) -> Requirement:
652
689
  if required is False:
653
690
  return "SATISFIED", "audio channel/TDM mapping is not required", 2
654
691
  return verified_bool_requirement(
655
- channel, "resolved", "audio channel/TDM mapping"
692
+ channel,
693
+ "resolved",
694
+ "audio channel/TDM mapping",
695
+ minimum_verification,
656
696
  )
657
697
 
658
698
 
659
- def camera_realtime_requirement(resources: dict[str, Any]) -> Requirement:
699
+ def camera_realtime_requirement(
700
+ resources: dict[str, Any], minimum_verification: str = "corroborated"
701
+ ) -> Requirement:
660
702
  return verified_bool_requirement(
661
703
  resources.get("camera_realtime", {}),
662
704
  "pipeline_safe",
663
- "camera DMA/task realtime policy",
705
+ "camera DMA/task realtime policy design",
706
+ minimum_verification,
664
707
  )
665
708
 
666
709
 
667
- def memory_requirement(resources: dict[str, Any]) -> Requirement:
710
+ def memory_requirement(
711
+ resources: dict[str, Any], minimum_verification: str = "corroborated"
712
+ ) -> Requirement:
668
713
  return verified_bool_requirement(
669
714
  resources.get("memory", {}),
670
715
  "startup_and_media_budgeted",
671
- "startup and media memory budget",
716
+ "static startup and media memory budget",
717
+ minimum_verification,
672
718
  )
673
719
 
674
720
 
@@ -750,7 +796,9 @@ def binding_requirement(onboarding: dict[str, Any]) -> Requirement:
750
796
 
751
797
 
752
798
  def combine_requirements(
753
- requirements: list[Requirement], legacy_hil_from_levels: bool = False
799
+ requirements: list[Requirement],
800
+ success_status: str = "READY_TO_PORT",
801
+ legacy_hil_from_levels: bool = False,
754
802
  ) -> dict[str, Any]:
755
803
  reasons = [reason for _, reason, _ in requirements]
756
804
  states = {state for state, _, _ in requirements}
@@ -766,7 +814,7 @@ def combine_requirements(
766
814
  ):
767
815
  status = "HIL_VERIFIED"
768
816
  else:
769
- status = "READY_TO_PORT"
817
+ status = success_status
770
818
  return {"status": status, "reasons": reasons}
771
819
 
772
820
 
@@ -839,9 +887,32 @@ def matching_runtime_evidence(
839
887
  return None
840
888
 
841
889
 
890
+ def artifact_requirement(artifact_sha256: str | None) -> Requirement:
891
+ if artifact_sha256 is None:
892
+ return "NEEDS_CONFIRMATION", "exact build artifact SHA-256 is missing", 0
893
+ if not SHA256_RE.fullmatch(artifact_sha256):
894
+ return "BLOCKED", "build artifact SHA-256 is invalid", 0
895
+ return (
896
+ "SATISFIED",
897
+ f"build evidence is bound to artifact {artifact_sha256}",
898
+ VERIFICATION_LEVELS["build_verified"],
899
+ )
900
+
901
+
842
902
  def assess_ir(
843
- data: dict[str, Any], artifact_sha256: str | None = None
903
+ data: dict[str, Any],
904
+ artifact_sha256: str | None = None,
905
+ phase: str | None = None,
844
906
  ) -> dict[str, Any]:
907
+ selected_phase = phase or ("hil" if artifact_sha256 else "intake")
908
+ if selected_phase not in ASSESSMENT_PHASES:
909
+ raise ValueError(
910
+ f"assessment phase must be one of {', '.join(sorted(ASSESSMENT_PHASES))}"
911
+ )
912
+ minimum_verification = minimum_verification_for_phase(selected_phase)
913
+ success_status = (
914
+ "READY_TO_PORT" if selected_phase == "intake" else "BUILD_VERIFIED"
915
+ )
845
916
  schema_version = data.get("schema_version")
846
917
  requested = data["features"]["requested"]
847
918
  audio_input = data["audio_input"]
@@ -858,7 +929,7 @@ def assess_ir(
858
929
  onboarding = data["onboarding"]
859
930
  project.extend(
860
931
  [
861
- i2c_requirement(resources),
932
+ i2c_requirement(resources, minimum_verification),
862
933
  wifi_requirement(onboarding),
863
934
  binding_requirement(onboarding),
864
935
  ]
@@ -875,73 +946,101 @@ def assess_ir(
875
946
  else:
876
947
  resources = {}
877
948
 
949
+ if selected_phase in {"build", "hil"}:
950
+ project.append(artifact_requirement(artifact_sha256))
951
+
878
952
  for feature in requested:
879
953
  if schema_version == 1:
880
954
  if feature == "h5_live_audio":
881
- requirements = [codec_requirement(audio_input, "audio_input")]
955
+ requirements = [
956
+ codec_requirement(
957
+ audio_input, "audio_input", minimum_verification
958
+ )
959
+ ]
882
960
  elif feature == "h5_live_video":
883
- requirements = [legacy_video_requirement(camera)]
961
+ requirements = [
962
+ legacy_video_requirement(camera, minimum_verification)
963
+ ]
884
964
  elif feature == "h5_talkback":
885
- requirements = [codec_requirement(audio_output, "audio_output")]
965
+ requirements = [
966
+ codec_requirement(
967
+ audio_output, "audio_output", minimum_verification
968
+ )
969
+ ]
886
970
  else:
887
971
  requirements = [
888
- codec_requirement(audio_input, "audio_input"),
889
- codec_requirement(audio_output, "audio_output"),
972
+ codec_requirement(
973
+ audio_input, "audio_input", minimum_verification
974
+ ),
975
+ codec_requirement(
976
+ audio_output, "audio_output", minimum_verification
977
+ ),
890
978
  ]
891
979
  result[feature] = combine_requirements(
892
- requirements, legacy_hil_from_levels=True
980
+ requirements,
981
+ success_status=success_status,
982
+ legacy_hil_from_levels=(selected_phase == "intake"),
893
983
  )
894
984
  continue
895
985
 
896
986
  if feature == "h5_live_audio":
897
987
  requirements = [
898
- codec_requirement(audio_input, "audio_input"),
899
- i2s_requirement(resources),
900
- audio_mapping_requirement(resources),
901
- memory_requirement(resources),
988
+ codec_requirement(audio_input, "audio_input", minimum_verification),
989
+ i2s_requirement(resources, minimum_verification),
990
+ audio_mapping_requirement(resources, minimum_verification),
991
+ memory_requirement(resources, minimum_verification),
902
992
  ]
903
993
  elif feature == "h5_live_video":
904
994
  requirements = [
905
- video_requirement_v2(camera),
906
- camera_realtime_requirement(resources),
907
- memory_requirement(resources),
995
+ video_requirement_v2(camera, minimum_verification),
996
+ camera_realtime_requirement(resources, minimum_verification),
997
+ memory_requirement(resources, minimum_verification),
908
998
  ]
909
999
  elif feature == "h5_talkback":
910
1000
  requirements = [
911
- codec_requirement(audio_output, "audio_output"),
912
- i2s_requirement(resources),
913
- memory_requirement(resources),
1001
+ codec_requirement(audio_output, "audio_output", minimum_verification),
1002
+ i2s_requirement(resources, minimum_verification),
1003
+ memory_requirement(resources, minimum_verification),
914
1004
  ]
915
1005
  else:
916
1006
  requirements = [
917
- codec_requirement(audio_input, "audio_input"),
918
- codec_requirement(audio_output, "audio_output"),
919
- i2s_requirement(resources),
920
- audio_mapping_requirement(resources),
921
- memory_requirement(resources),
1007
+ codec_requirement(audio_input, "audio_input", minimum_verification),
1008
+ codec_requirement(audio_output, "audio_output", minimum_verification),
1009
+ i2s_requirement(resources, minimum_verification),
1010
+ audio_mapping_requirement(resources, minimum_verification),
1011
+ memory_requirement(resources, minimum_verification),
922
1012
  ]
923
- result[feature] = combine_requirements(requirements)
1013
+ result[feature] = combine_requirements(
1014
+ requirements, success_status=success_status
1015
+ )
924
1016
 
925
1017
  project_gate = combine_requirements(
926
- project, legacy_hil_from_levels=(schema_version == 1)
1018
+ project,
1019
+ success_status=success_status,
1020
+ legacy_hil_from_levels=(schema_version == 1 and selected_phase == "intake"),
927
1021
  )
928
1022
  evidence = matching_runtime_evidence(data, artifact_sha256)
929
- if schema_version == 2 and evidence is not None:
930
- evidence_features = set(evidence.get("features", []))
931
- evidence_levels = set(evidence.get("acceptance_levels", []))
932
- for feature, assessment in result.items():
933
- if (
934
- assessment["status"] == "READY_TO_PORT"
935
- and feature in evidence_features
936
- and FEATURE_HIL_LEVEL[feature] in evidence_levels
937
- ):
938
- assessment["status"] = "HIL_VERIFIED"
939
- assessment["reasons"].append(
940
- f"artifact {artifact_sha256} passed {FEATURE_HIL_LEVEL[feature]}"
1023
+ if schema_version == 2 and selected_phase == "hil":
1024
+ evidence_features = set(evidence.get("features", [])) if evidence else set()
1025
+ evidence_levels = (
1026
+ set(evidence.get("acceptance_levels", [])) if evidence else set()
1027
+ )
1028
+ for feature, feature_assessment in result.items():
1029
+ if feature_assessment["status"] != "BUILD_VERIFIED":
1030
+ continue
1031
+ required_level = FEATURE_HIL_LEVEL[feature]
1032
+ if feature in evidence_features and required_level in evidence_levels:
1033
+ feature_assessment["status"] = "HIL_VERIFIED"
1034
+ feature_assessment["reasons"].append(
1035
+ f"artifact {artifact_sha256} passed {required_level}"
1036
+ )
1037
+ else:
1038
+ feature_assessment["reasons"].append(
1039
+ f"artifact {artifact_sha256} has no matching {required_level} runtime evidence"
941
1040
  )
942
1041
  if result and all(
943
1042
  item["status"] == "HIL_VERIFIED" for item in result.values()
944
- ):
1043
+ ) and project_gate["status"] == "BUILD_VERIFIED":
945
1044
  project_gate["status"] = "HIL_VERIFIED"
946
1045
  project_gate["reasons"].append(
947
1046
  f"all requested features have matching artifact evidence for {artifact_sha256}"
@@ -949,6 +1048,7 @@ def assess_ir(
949
1048
 
950
1049
  assessment: dict[str, Any] = {
951
1050
  "schema_version": schema_version,
1051
+ "phase": selected_phase,
952
1052
  "board_id": data["board"]["id"],
953
1053
  "hardware_revision": data["board"]["hardware_revision"],
954
1054
  "project_gate": project_gate,
@@ -1005,12 +1105,26 @@ def command_assess(args: argparse.Namespace) -> int:
1005
1105
  for error in errors:
1006
1106
  print(f"error: {error}", file=sys.stderr)
1007
1107
  return 2
1008
- assessment = assess_ir(data, artifact_sha256=args.artifact_sha256)
1108
+ if args.artifact_sha256 is not None and not SHA256_RE.fullmatch(
1109
+ args.artifact_sha256
1110
+ ):
1111
+ print("error: --artifact-sha256 must be a 64-character SHA-256", file=sys.stderr)
1112
+ return 2
1113
+ try:
1114
+ assessment = assess_ir(
1115
+ data,
1116
+ artifact_sha256=args.artifact_sha256,
1117
+ phase=args.phase,
1118
+ )
1119
+ except ValueError as exc:
1120
+ print(f"error: {exc}", file=sys.stderr)
1121
+ return 2
1009
1122
  print(json.dumps(assessment, ensure_ascii=False, indent=2))
1010
1123
  if args.strict:
1011
1124
  statuses = {item["status"] for item in assessment["features"].values()}
1012
1125
  statuses.add(assessment["project_gate"]["status"])
1013
- if not statuses.issubset(READY_STATUSES):
1126
+ allowed_statuses = PHASE_SUCCESS_STATUSES[assessment["phase"]]
1127
+ if not statuses.issubset(allowed_statuses):
1014
1128
  return 3
1015
1129
  return 0
1016
1130
 
@@ -1036,11 +1150,16 @@ def parse_args() -> argparse.Namespace:
1036
1150
  assess_parser.add_argument(
1037
1151
  "--strict",
1038
1152
  action="store_true",
1039
- help="return non-zero unless every requested feature is ready or HIL verified",
1153
+ help="return non-zero unless every requested feature passes the selected phase",
1154
+ )
1155
+ assess_parser.add_argument(
1156
+ "--phase",
1157
+ choices=sorted(ASSESSMENT_PHASES),
1158
+ help="assessment phase; defaults to intake, or hil when an artifact SHA is supplied",
1040
1159
  )
1041
1160
  assess_parser.add_argument(
1042
1161
  "--artifact-sha256",
1043
- help="bind HIL status to runtime evidence for this exact firmware artifact",
1162
+ help="bind build or HIL assessment to this exact firmware artifact",
1044
1163
  )
1045
1164
  assess_parser.set_defaults(handler=command_assess)
1046
1165
  return parser.parse_args()