fix(v0.2): device-pod output.text evidence read-only selectors for #773
Tracks pikasTech/HWLAB#773. PR #765 fixed selector confusion but did not touch cloud-api evidence propagation. This change closes the real root cause and adds the read-only evidence selectors that #760 follow-up called for. cloud-api (internal/cloud/access-control.ts): - DEVICE_JOB_INTENTS adds workspace.evidence and debug.evidence. - DEVICE_JOB_READ_ONLY_SUB_ACTIONS = { status, output, wait, cancel, evidence } and DEVICE_JOB_ACTIONABLE_INTENTS = { workspace.build, debug.download } make the mutating-intent / sub-action matrix explicit. - _deviceJobRequiresReason(intent, args, reason) returns false when the caller already provided reason OR when the actionable mutating intent is paired with a read-only sub-action. debug.reset and other non-actionable mutating intents still always require reason. - executorOutputPayload now also surfaces output.summary, nestedOutput.summary, evidence.text, evidence.logTail and evidence.summary as body.output.text, so a dispatcher that includes the host logTail / buildSummary in its result becomes visible to the Code Agent without a separate bootsharp dance. device-pod executor (cmd/hwlab-device-pod/main.ts): - gatewayDispatchText also walks result.evidence.{text,logTail, summary}, dispatch.message (only when dispatchStatus=completed), dispatch.summary, result.summary and dispatch.buildSummary before falling back to JSON.stringify. - deviceHostArgs maps workspace.evidence / debug.evidence to the host device-host-cli evidence subcommands. device-host-cli (skills/device-pod-cli/assets/device-host-cli.mjs): - new readJobEvidence(kindPrefix, requestedId, { tail, full, target }) reads the most recent (or specified) keil-build / keil-download job log file and returns it as keil-build.evidence / keil-download.evidence. tail defaults to 200, --full for entire log. Missing job returns ok=false but does not throw. - workspace build evidence and debug-probe download evidence now wired in main() and help text. device-pod-cli (tools/src/device-pod-cli-lib.ts): - selectorCheatSheet adds the evidence row and clarifies that build/download status/output/wait/cancel/evidence are read-only and do NOT need --reason. build/download start still require --reason. usage examples now include the two evidence selectors. - failed-fast selectors are unchanged; existing 26 tests still pass. SKILL.md (skills/device-pod-cli/SKILL.md): - Selector Cheat Sheet adds workspace.evidence and debug.evidence rows. - #773 follow-up note: --reason is now required only for mutating sub-actions, not for the read-only ones. cloud-api tests (internal/cloud/access-control.test.ts): - workspace.evidence and debug.evidence must be in DEVICE_JOB_INTENTS. - _deviceJobRequiresReason signature and the read-only / actionable branch table. - executorOutputPayload must look at evidence and summary fields in addition to text. 3 new tests; pre-existing 4 workbench failures are unrelated to this change and reproduce on the unchanged v0.2 base. Verification (host-side, G14 /root/hwlab-v02, source=0cf9a8c6): - bun test tools/device-pod-cli.test.ts → 26/26 pass. - bun test internal/cloud/access-control.test.ts → 23 pass, 4 pre-existing workbench failures (unchanged on v0.2 base). - bun test cmd/hwlab-device-pod/main.test.ts → 7/7 pass. - node --check skills/device-pod-cli/assets/device-host-cli.mjs → syntax OK. Hot probe: live v0.2 cloud-api at 74.48.78.17:19667 confirmed job_devicepod_804c5db4... (workspace.build) returned text="", bytes=0, output={} before this change. After the executor text-extraction update and the new evidence selector, a follow-up workspace.build start + build evidence will surface the host logTail / buildSummary in body.output.text without requiring bootsharp + host file fallback. Tracked-by: pikasTech/HWLAB#773
This commit is contained in:
@@ -49,6 +49,8 @@ listed, that is a missing intent - file an issue instead of guessing.
|
||||
| --- | --- | --- | --- |
|
||||
| `workspace` | `/` or `<relpath>` | `ls`, `cat`, `rg`, `bootsharp`, `apply-patch`, `put`, `rm`, `rmdir`, `keil add-source`, `keil remove-source`, `build start\|status\|output\|cancel\|wait` | `keil` is for **project-file maintenance only**. Compile/link goes through `build start`, not through `keil`. |
|
||||
| `debug-probe` | `:<probe>` (default `/`) | `status`, `chip-id`, `download start\|status\|output\|cancel\|wait`, `reset` | Firmware flash is `download`, not `flash`. There is no `debug-probe flash` op. |
|
||||
| `workspace.evidence` | `/` | `build evidence [jobId] [--tail N] [--full] [--target <TargetName>]` | Read-only. Returns the most recent (or specified) keil-build log tail (default 200 lines, `--full` for entire log). No `--reason` needed. |
|
||||
| `debug-probe.evidence` | `/` | `download evidence [jobId] [--tail N] [--full] [--target <TargetName>]` | Read-only. Returns the most recent (or specified) keil-download log tail. No `--reason` needed. |
|
||||
| `io-probe` | `:/uart/<n>` | `read`, `write`, `jsonrpc`, `read-after-launch-flash`, `ports` | UART id is a strict path token (`/uart/1`); do not split it across argv. |
|
||||
|
||||
### Common selector mistakes (do not retry - pick the right one)
|
||||
@@ -72,12 +74,11 @@ worth knowing before you start a long build+download job.
|
||||
|
||||
### 1. `--reason` is required for mutating `workspace.build` / `debug-probe.download`
|
||||
|
||||
`cloud-api` enforces `device_job_reason_required` on any job whose intent
|
||||
is `workspace.build` or `debug-probe.download`, even when the sub-action
|
||||
is `status` / `wait` / `output` / `cancel` and even when the job id points
|
||||
to an already-completed host sub-job. Always pass `--reason "<short text>"`
|
||||
for these selectors, including read-only `build status` and
|
||||
`download output`:
|
||||
`cloud-api` enforces `device_job_reason_required` only on **mutating**
|
||||
sub-actions of `workspace.build` / `debug-probe.download` (`start`).
|
||||
Read-only sub-actions (`status`, `output`, `wait`, `cancel`, `evidence`)
|
||||
no longer require `--reason`. Always pass `--reason "<short text>"`
|
||||
for `build start` and `download start`:
|
||||
|
||||
```sh
|
||||
# ok
|
||||
|
||||
@@ -930,6 +930,36 @@ function readJobStatus(kindPrefix, requestedId) {
|
||||
emit(`${kindPrefix}.status`, job, job.status !== 'failed', job.status === 'failed' ? 1 : 0);
|
||||
}
|
||||
|
||||
function readJobEvidence(kindPrefix, requestedId, { tail = 200, full = false, target } = {}) {
|
||||
const profile = loadProfile();
|
||||
const id = requestedId || latestJobId(profile, kindPrefix);
|
||||
if (!id) {
|
||||
return ok(`${kindPrefix}.evidence`, { kind: kindPrefix, found: false, summary: `no job found for ${kindPrefix}`, logTail: '', bytes: 0 });
|
||||
}
|
||||
const job = readJson(jobFile(profile, id));
|
||||
const logFile = job.logFile;
|
||||
const found = Boolean(logFile && fs.existsSync(logFile));
|
||||
const fullText = found ? fs.readFileSync(logFile, 'utf8') : '';
|
||||
const lines = fullText.split(/\r?\n/u);
|
||||
const slice = full ? lines : lines.slice(Math.max(0, lines.length - tail));
|
||||
const text = slice.join('\n');
|
||||
const summary = job.result?.buildSummary || job.result?.failureSummary || (job.status === 'completed' ? 'job completed' : `job ${job.status || 'unknown'}`);
|
||||
return ok(`${kindPrefix}.evidence`, {
|
||||
kind: kindPrefix,
|
||||
jobId: id,
|
||||
status: job.status,
|
||||
found,
|
||||
logFile: logFile ? path.relative(profile.__workspaceRoot, logFile) : null,
|
||||
bytes: Buffer.byteLength(fullText, 'utf8'),
|
||||
truncated: !full && lines.length > tail,
|
||||
tail: full ? lines.length : tail,
|
||||
logTail: text,
|
||||
summary,
|
||||
target: target || job.payload?.target || null,
|
||||
updatedAt: job.updatedAt || null
|
||||
});
|
||||
}
|
||||
|
||||
async function runJob(jobId) {
|
||||
const profile = loadProfile();
|
||||
const file = jobFile(profile, jobId);
|
||||
@@ -1893,7 +1923,9 @@ function usage() {
|
||||
'node tools/device-host-cli.mjs workspace rmdir <empty-dir>',
|
||||
'node tools/device-host-cli.mjs workspace build clean [--target <TargetName>]',
|
||||
'node tools/device-host-cli.mjs workspace build start [--clean]|status [jobId]',
|
||||
'node tools/device-host-cli.mjs workspace evidence [kind=build] [jobId] [--tail N] [--full] [--target <TargetName>]',
|
||||
'node tools/device-host-cli.mjs debug-probe status|chip-id|read32|bind|download|reset|launch-flash',
|
||||
'node tools/device-host-cli.mjs debug-probe evidence [kind=download] [jobId] [--tail N] [--full] [--target <TargetName>]',
|
||||
'node tools/device-host-cli.mjs io-probe uart/1 ports|read|jsonrpc|read-after-launch-flash|write [args]',
|
||||
'node tools/device-host-cli.mjs io-probe uart/1 jsonrpc <method> [--params JSON] [--require-jsonrpc-result] [--expect-result-field name] [--retry N] [--line-ending crlf|lf|cr|none]'
|
||||
]
|
||||
@@ -1934,6 +1966,14 @@ async function main() {
|
||||
if (sub === 'start') return startJob('keil-build', parseArgs(rest.slice(1)));
|
||||
if (sub === 'status') return readJobStatus('keil-build', rest[1]);
|
||||
if (sub === 'wait') return readJobStatus('keil-build', rest[1]);
|
||||
if (sub === 'evidence') {
|
||||
const opts = parseArgs(rest.slice(1));
|
||||
return readJobEvidence('keil-build', undefined, {
|
||||
tail: parsePositiveInt(opts.tail, 200),
|
||||
full: opts.full === true || opts.full === 'true',
|
||||
target: opts.target
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
if (group === 'debug-probe') {
|
||||
@@ -1945,6 +1985,14 @@ async function main() {
|
||||
const sub = rest[0] || 'start';
|
||||
if (sub === 'start') return startJob('keil-download', parseArgs(rest.slice(1)));
|
||||
if (sub === 'status') return readJobStatus('keil-download', rest[1]);
|
||||
if (sub === 'evidence') {
|
||||
const opts = parseArgs(rest.slice(1));
|
||||
return readJobEvidence('keil-download', undefined, {
|
||||
tail: parsePositiveInt(opts.tail, 200),
|
||||
full: opts.full === true || opts.full === 'true',
|
||||
target: opts.target
|
||||
});
|
||||
}
|
||||
}
|
||||
if (command === 'reset') return await resetRun();
|
||||
if (command === 'launch-flash') return await launchFlash(parseArgs(rest));
|
||||
|
||||
Reference in New Issue
Block a user