taphound 0.2.0-dev.11 → 0.2.0-dev.13
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/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/assets/skills/taphound-case-suite/SKILL.md +8 -6
- package/assets/skills/taphound-case-suite/scripts/ledger.mjs +84 -19
- package/assets/skills/taphound-journey-brief-author/SKILL.md +8 -3
- package/assets/skills/taphound-journey-brief-author/prompts/brief-author-role.md +3 -1
- package/assets/skills/taphound-journey-brief-author/prompts/brief-author-role.zh-CN.md +1 -1
- package/assets/skills/taphound-journey-generator/SKILL.md +21 -5
- package/assets/skills/taphound-journey-generator/prompts/consume-journey-brief.md +3 -1
- package/assets/skills/taphound-journey-generator/prompts/generate-step.md +9 -0
- package/assets/skills/taphound-journey-generator/scripts/envelope.mjs +80 -10
- package/assets/skills/taphound-verify-change/references/preserve.md +15 -7
- package/assets/skills/taphound-verify-change/scripts/ui-refactor.mjs +1 -1
- package/dist/adapters/appium/appium-ui-snapshot-provider.d.ts +8 -0
- package/dist/adapters/appium/appium-ui-snapshot-provider.js +123 -54
- package/dist/adapters/filesystem/diagnostics-journal.d.ts +17 -0
- package/dist/adapters/filesystem/diagnostics-journal.js +130 -0
- package/dist/application/diagnostics/diagnostics-exporter.d.ts +29 -0
- package/dist/application/diagnostics/diagnostics-exporter.js +283 -0
- package/dist/application/diagnostics/ui-capture-telemetry.d.ts +16 -0
- package/dist/application/diagnostics/ui-capture-telemetry.js +81 -0
- package/dist/application/generation/proposed-step-validator.js +5 -1
- package/dist/application/journey/journey-check-service.d.ts +7 -1
- package/dist/application/journey/journey-check-service.js +16 -1
- package/dist/application/report/report-writer.d.ts +5 -0
- package/dist/application/report/report-writer.js +20 -0
- package/dist/application/runtime/verify-runtime.js +32 -2
- package/dist/application/ui/observed-ui-snapshot-provider.d.ts +15 -0
- package/dist/application/ui/observed-ui-snapshot-provider.js +66 -0
- package/dist/application/ui/ui-stability-probe.d.ts +6 -0
- package/dist/application/ui/ui-stability-probe.js +21 -5
- package/dist/application/wait/idle-waiter.js +29 -6
- package/dist/cli/commands/diagnose.d.ts +3 -0
- package/dist/cli/commands/diagnose.js +84 -0
- package/dist/cli/commands/generation/session-commands.js +4 -1
- package/dist/cli/commands/journey.js +3 -1
- package/dist/cli/commands/verify.js +55 -3
- package/dist/cli/dependencies.d.ts +11 -0
- package/dist/cli/dependencies.js +58 -5
- package/dist/cli/diagnostics-recorder.d.ts +31 -0
- package/dist/cli/diagnostics-recorder.js +82 -0
- package/dist/cli/main.d.ts +1 -1
- package/dist/cli/main.js +58 -1
- package/dist/cli/program.js +3 -1
- package/dist/domain/contract.d.ts +6 -6
- package/dist/domain/diagnostics.d.ts +1396 -0
- package/dist/domain/diagnostics.js +193 -0
- package/dist/domain/failure.js +1 -1
- package/dist/domain/report.d.ts +11 -3
- package/dist/domain/report.js +1 -0
- package/dist/domain/verify-receipt.d.ts +17 -0
- package/dist/domain/verify-receipt.js +17 -0
- package/dist/domain/workspace.d.ts +11 -0
- package/dist/domain/workspace.js +14 -0
- package/dist/ports/diagnostics.d.ts +15 -0
- package/dist/ports/diagnostics.js +1 -0
- package/package.json +4 -3
- package/scripts/feedback-pack.mjs +399 -0
package/README.md
CHANGED
|
@@ -90,7 +90,7 @@ taphound init --agent claude,codex,cursor,droid
|
|
|
90
90
|
- [Principles](https://github.com/caikaidev/TapHound/blob/main/docs/principles.md) · [Workflow Skills and development scenarios](https://github.com/caikaidev/TapHound/blob/main/docs/workflow-skills.md) · [Agent integration](https://github.com/caikaidev/TapHound/blob/main/docs/agent-integration.md) · [Journey Generator guide](https://github.com/caikaidev/TapHound/blob/main/docs/journey-generator-guide.md)
|
|
91
91
|
- [Acceptance Contracts](https://github.com/caikaidev/TapHound/blob/main/docs/contract-schema.md) · [Baselines & regression](https://github.com/caikaidev/TapHound/blob/main/docs/checkpoint-regression.md) · [Failure classification](https://github.com/caikaidev/TapHound/blob/main/docs/failure-classification.md)
|
|
92
92
|
- [Semantic Anchors](https://github.com/caikaidev/TapHound/blob/main/docs/semantic-anchor.md) · [Capability matrix](https://github.com/caikaidev/TapHound/blob/main/docs/capability-matrix.md) · [`observe`](https://github.com/caikaidev/TapHound/blob/main/docs/observe.md)
|
|
93
|
-
- [Runtime backends](https://github.com/caikaidev/TapHound/blob/main/docs/architecture/runtime-backend.md) · [Local development & testing](https://github.com/caikaidev/TapHound/blob/main/docs/local-testing.md) · [Releasing](https://github.com/caikaidev/TapHound/blob/main/docs/releasing.md)
|
|
93
|
+
- [Runtime backends](https://github.com/caikaidev/TapHound/blob/main/docs/architecture/runtime-backend.md) · [Local development & testing](https://github.com/caikaidev/TapHound/blob/main/docs/local-testing.md) · [Releasing](https://github.com/caikaidev/TapHound/blob/main/docs/releasing.md) · [Diagnostics and feedback](https://github.com/caikaidev/TapHound/blob/main/docs/diagnostics.md)
|
|
94
94
|
|
|
95
95
|
## Current Limitations
|
|
96
96
|
|
package/README.zh-CN.md
CHANGED
|
@@ -90,7 +90,7 @@ taphound init --agent claude,codex,cursor,droid
|
|
|
90
90
|
- [设计原则](https://github.com/caikaidev/TapHound/blob/main/docs/principles.md) · [Workflow Skill 与开发场景](https://github.com/caikaidev/TapHound/blob/main/docs/workflow-skills.md) · [Agent 集成](https://github.com/caikaidev/TapHound/blob/main/docs/agent-integration.md) · [Journey Generator 指南](https://github.com/caikaidev/TapHound/blob/main/docs/journey-generator-guide.md)
|
|
91
91
|
- [Acceptance Contract](https://github.com/caikaidev/TapHound/blob/main/docs/contract-schema.md) · [Baseline 与回归对比](https://github.com/caikaidev/TapHound/blob/main/docs/checkpoint-regression.md) · [失败分类](https://github.com/caikaidev/TapHound/blob/main/docs/failure-classification.md)
|
|
92
92
|
- [Semantic Anchor](https://github.com/caikaidev/TapHound/blob/main/docs/semantic-anchor.md) · [能力矩阵](https://github.com/caikaidev/TapHound/blob/main/docs/capability-matrix.md) · [`observe`](https://github.com/caikaidev/TapHound/blob/main/docs/observe.md)
|
|
93
|
-
- [运行时后端](https://github.com/caikaidev/TapHound/blob/main/docs/architecture/runtime-backend.md) · [本地开发与测试](https://github.com/caikaidev/TapHound/blob/main/docs/local-testing.md) · [发布](https://github.com/caikaidev/TapHound/blob/main/docs/releasing.md)
|
|
93
|
+
- [运行时后端](https://github.com/caikaidev/TapHound/blob/main/docs/architecture/runtime-backend.md) · [本地开发与测试](https://github.com/caikaidev/TapHound/blob/main/docs/local-testing.md) · [发布](https://github.com/caikaidev/TapHound/blob/main/docs/releasing.md) · [诊断与反馈](https://github.com/caikaidev/TapHound/blob/main/docs/diagnostics.md)
|
|
94
94
|
|
|
95
95
|
## 当前限制
|
|
96
96
|
|
|
@@ -53,11 +53,13 @@ Use `node <skill>/scripts/ledger.mjs help` for the helper contract.
|
|
|
53
53
|
|
|
54
54
|
## Durable suite directory
|
|
55
55
|
|
|
56
|
-
|
|
57
|
-
|
|
56
|
+
A Suite always lives in `.taphound/suites/<suite-id>/` under the project
|
|
57
|
+
root; the helper derives that directory from the Suite ID and rejects any
|
|
58
|
+
other location (and a Brief anywhere but its Case directory) with
|
|
59
|
+
`CASE_SUITE_LOCATION`:
|
|
58
60
|
|
|
59
61
|
```text
|
|
60
|
-
|
|
62
|
+
.taphound/suites/<suite-id>/
|
|
61
63
|
├── cases.json # frozen user-approved Case catalog
|
|
62
64
|
├── case-ledger.json # mutable, revisioned orchestration state
|
|
63
65
|
├── STATUS.md # generated human view; never the Source of Truth
|
|
@@ -95,8 +97,7 @@ Create an input from `templates/suite-input.example.json`, then:
|
|
|
95
97
|
|
|
96
98
|
```bash
|
|
97
99
|
node <skill>/scripts/ledger.mjs init \
|
|
98
|
-
--input /tmp/taphound-suite-input.json
|
|
99
|
-
--out <project>/doc/development/<suite-id>
|
|
100
|
+
--input /tmp/taphound-suite-input.json
|
|
100
101
|
```
|
|
101
102
|
|
|
102
103
|
Initialization canonicalizes `projectRoot`, validates dependency cycles and
|
|
@@ -215,7 +216,8 @@ Dispatch exactly one Case to `taphound-journey-brief-author` with:
|
|
|
215
216
|
- exact catalog `sourceText` as `caseGoal`;
|
|
216
217
|
- explicit context paths only;
|
|
217
218
|
- output
|
|
218
|
-
|
|
219
|
+
`.taphound/suites/<suite-id>/briefs/<case-id>/taphound-journey-brief.md`
|
|
220
|
+
(the only path `briefReady` accepts).
|
|
219
221
|
|
|
220
222
|
After authoring, compute its exact hash and transition:
|
|
221
223
|
|
|
@@ -7,7 +7,11 @@ import {
|
|
|
7
7
|
import {
|
|
8
8
|
dirname, isAbsolute, join, relative, resolve, sep
|
|
9
9
|
} from "node:path";
|
|
10
|
+
import { fileURLToPath } from "node:url";
|
|
10
11
|
|
|
12
|
+
// Suites are committed TapHound project material; the layout mirrors
|
|
13
|
+
// SUITES_DIR in src/domain/workspace.ts.
|
|
14
|
+
const SUITES_DIR = ".taphound/suites";
|
|
11
15
|
const CATALOG = "cases.json";
|
|
12
16
|
const LEDGER = "case-ledger.json";
|
|
13
17
|
const STATUS = "STATUS.md";
|
|
@@ -82,23 +86,36 @@ function canonicalHash(value) {
|
|
|
82
86
|
return digest(JSON.stringify(canonical(value)));
|
|
83
87
|
}
|
|
84
88
|
|
|
85
|
-
|
|
89
|
+
// `contract` names the object and, for command inputs, the shipped template
|
|
90
|
+
// so a rejected field points at the shape the command accepts.
|
|
91
|
+
function exactKeys(value, required, optional = [], contract = undefined) {
|
|
92
|
+
const where = contract === undefined ? "" : ` in ${contract.name}`;
|
|
93
|
+
const hint = () => {
|
|
94
|
+
const fields = [...required, ...optional.map((key) => `${key}?`)];
|
|
95
|
+
return `; allowed fields: ${fields.join(", ")}${
|
|
96
|
+
contract?.template === undefined
|
|
97
|
+
? ""
|
|
98
|
+
: `; see ${contract.template}`
|
|
99
|
+
}`;
|
|
100
|
+
};
|
|
86
101
|
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
87
|
-
fail("CASE_SUITE_INVALID",
|
|
102
|
+
fail("CASE_SUITE_INVALID", `Expected a JSON object${where}`);
|
|
88
103
|
}
|
|
89
104
|
const allowed = new Set([...required, ...optional]);
|
|
90
105
|
for (const key of Object.keys(value)) {
|
|
91
106
|
if (!allowed.has(key)) {
|
|
92
|
-
fail("CASE_SUITE_INVALID", `Unknown field "${key}"`);
|
|
107
|
+
fail("CASE_SUITE_INVALID", `Unknown field "${key}"${where}${hint()}`);
|
|
93
108
|
}
|
|
94
109
|
}
|
|
95
110
|
for (const key of required) {
|
|
96
111
|
if (!(key in value)) {
|
|
97
|
-
fail("CASE_SUITE_INVALID", `Missing required field "${key}"`);
|
|
112
|
+
fail("CASE_SUITE_INVALID", `Missing required field "${key}"${where}${hint()}`);
|
|
98
113
|
}
|
|
99
114
|
}
|
|
100
115
|
}
|
|
101
116
|
|
|
117
|
+
const TEMPLATES = join(dirname(fileURLToPath(import.meta.url)), "..", "templates");
|
|
118
|
+
|
|
102
119
|
function nonempty(value, label, max = 1000) {
|
|
103
120
|
if (typeof value !== "string") {
|
|
104
121
|
fail("CASE_SUITE_INVALID", `${label} must be a string`);
|
|
@@ -125,6 +142,14 @@ function assertSha(value, label) {
|
|
|
125
142
|
}
|
|
126
143
|
}
|
|
127
144
|
|
|
145
|
+
function suiteDirectory(projectRoot, suiteId) {
|
|
146
|
+
return join(projectRoot, ...SUITES_DIR.split("/"), suiteId);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function suiteBriefPath(suiteId, caseId) {
|
|
150
|
+
return `${SUITES_DIR}/${suiteId}/briefs/${caseId}/taphound-journey-brief.md`;
|
|
151
|
+
}
|
|
152
|
+
|
|
128
153
|
function inside(root, path) {
|
|
129
154
|
const rel = relative(root, path);
|
|
130
155
|
return rel === "" || (
|
|
@@ -459,6 +484,13 @@ async function loadSuite(suitePath) {
|
|
|
459
484
|
if (projectRoot !== catalog.projectRoot || !inside(projectRoot, root)) {
|
|
460
485
|
fail("CASE_SUITE_INVALID", "Suite must stay beneath canonical projectRoot");
|
|
461
486
|
}
|
|
487
|
+
if (root !== suiteDirectory(projectRoot, catalog.suiteId)) {
|
|
488
|
+
fail(
|
|
489
|
+
"CASE_SUITE_LOCATION",
|
|
490
|
+
`Suite ${catalog.suiteId} must live in ${SUITES_DIR}/${catalog.suiteId}, `
|
|
491
|
+
+ `found ${relative(projectRoot, root)}; initialize it there`
|
|
492
|
+
);
|
|
493
|
+
}
|
|
462
494
|
const ledger = parseLedger(await json(ledgerPath, LEDGER), catalog);
|
|
463
495
|
const catalogSha256 = digest(catalogBytes);
|
|
464
496
|
if (ledger.catalog.sha256 !== catalogSha256) {
|
|
@@ -599,7 +631,8 @@ function parseSuiteInput(value) {
|
|
|
599
631
|
exactKeys(
|
|
600
632
|
value,
|
|
601
633
|
["version", "suiteId", "title", "projectRoot", "cases"],
|
|
602
|
-
["deviceSerial", "contextPath"]
|
|
634
|
+
["deviceSerial", "contextPath"],
|
|
635
|
+
{ name: "init input", template: join(TEMPLATES, "suite-input.example.json") }
|
|
603
636
|
);
|
|
604
637
|
if (value.version !== 1 || !suiteIdPattern.test(value.suiteId ?? "")
|
|
605
638
|
|| !isAbsolute(value.projectRoot ?? "")
|
|
@@ -629,9 +662,13 @@ function parseSuiteInput(value) {
|
|
|
629
662
|
async function init(inputPath, outputPath) {
|
|
630
663
|
const input = parseSuiteInput(await json(resolve(inputPath), "Suite input"));
|
|
631
664
|
const projectRoot = await realpath(input.projectRoot);
|
|
632
|
-
const output =
|
|
633
|
-
if (
|
|
634
|
-
fail(
|
|
665
|
+
const output = suiteDirectory(projectRoot, input.suiteId);
|
|
666
|
+
if (outputPath !== undefined && resolve(outputPath) !== output) {
|
|
667
|
+
fail(
|
|
668
|
+
"CASE_SUITE_LOCATION",
|
|
669
|
+
`Suite directory must be ${SUITES_DIR}/${input.suiteId} under projectRoot `
|
|
670
|
+
+ `(${output}); omit --out to use it: ${outputPath}`
|
|
671
|
+
);
|
|
635
672
|
}
|
|
636
673
|
await access(output).then(
|
|
637
674
|
() => fail("CASE_SUITE_EXISTS", "Suite output already exists"),
|
|
@@ -708,7 +745,8 @@ function parseTransition(value) {
|
|
|
708
745
|
exactKeys(
|
|
709
746
|
value,
|
|
710
747
|
["version", "expectedRevision", "caseId", "from", "to", "reason"],
|
|
711
|
-
["brief", "generation", "failure", "nextAction", "completion"]
|
|
748
|
+
["brief", "generation", "failure", "nextAction", "completion"],
|
|
749
|
+
{ name: "transition input", template: join(TEMPLATES, "transition.example.json") }
|
|
712
750
|
);
|
|
713
751
|
if (value.version !== 1 || !caseIdPattern.test(value.caseId ?? "")
|
|
714
752
|
|| !statuses.has(value.from) || !statuses.has(value.to)) {
|
|
@@ -861,10 +899,12 @@ async function transition(suitePath, inputPath) {
|
|
|
861
899
|
const briefFile = await regularProjectFile(
|
|
862
900
|
suite.projectRoot, request.brief, `${entry.id}.brief`
|
|
863
901
|
);
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
902
|
+
const expectedBrief = suiteBriefPath(suite.catalog.suiteId, entry.id);
|
|
903
|
+
if (briefFile.relativePath !== expectedBrief) {
|
|
904
|
+
fail(
|
|
905
|
+
"CASE_SUITE_LOCATION",
|
|
906
|
+
`Brief for ${entry.id} must be ${expectedBrief}: ${briefFile.relativePath}`
|
|
907
|
+
);
|
|
868
908
|
}
|
|
869
909
|
}
|
|
870
910
|
if (request.to === "generating") {
|
|
@@ -958,10 +998,18 @@ async function transition(suitePath, inputPath) {
|
|
|
958
998
|
}
|
|
959
999
|
|
|
960
1000
|
function parseFlowRecord(value) {
|
|
961
|
-
exactKeys(
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
1001
|
+
exactKeys(
|
|
1002
|
+
value,
|
|
1003
|
+
[
|
|
1004
|
+
"version", "expectedRevision", "name", "path", "sha256",
|
|
1005
|
+
"exitActivity", "journey", "resolutionManifest", "report"
|
|
1006
|
+
],
|
|
1007
|
+
[],
|
|
1008
|
+
{
|
|
1009
|
+
name: "record-flow input",
|
|
1010
|
+
template: join(TEMPLATES, "base-flow-record.example.json")
|
|
1011
|
+
}
|
|
1012
|
+
);
|
|
965
1013
|
if (value.version !== 1 || !flowNamePattern.test(value.name ?? "")) {
|
|
966
1014
|
fail("CASE_SUITE_INVALID", "Invalid Base Flow record");
|
|
967
1015
|
}
|
|
@@ -1154,12 +1202,26 @@ function help() {
|
|
|
1154
1202
|
"TapHound Case Suite Ledger",
|
|
1155
1203
|
"",
|
|
1156
1204
|
"Commands:",
|
|
1157
|
-
" init --input <json> --out <suite-
|
|
1205
|
+
" init --input <json> [--out <project>/.taphound/suites/<suite-id>]",
|
|
1158
1206
|
" validate --suite <suite-directory>",
|
|
1159
1207
|
" status --suite <suite-directory> [--case <case-id>]",
|
|
1160
1208
|
" transition --suite <suite-directory> --input <json>",
|
|
1161
1209
|
" record-flow --suite <suite-directory> --input <json>",
|
|
1162
|
-
" recover-lock --suite <suite-directory>"
|
|
1210
|
+
" recover-lock --suite <suite-directory>",
|
|
1211
|
+
"",
|
|
1212
|
+
`Input templates live in ${TEMPLATES}: suite-input.example.json (init),`,
|
|
1213
|
+
"transition.example.json (transition), base-flow-record.example.json",
|
|
1214
|
+
"(record-flow). Unknown or missing fields are rejected with the allowed list.",
|
|
1215
|
+
"",
|
|
1216
|
+
`Layout: a Suite lives in <projectRoot>/${SUITES_DIR}/<suite-id>/ and each`,
|
|
1217
|
+
"Case Brief in its briefs/<case-id>/taphound-journey-brief.md. Other",
|
|
1218
|
+
"locations fail with CASE_SUITE_LOCATION.",
|
|
1219
|
+
"",
|
|
1220
|
+
"Ledger revision: every successful transition or record-flow increments",
|
|
1221
|
+
"ledger.revision by exactly 1. Pass the current value as expectedRevision",
|
|
1222
|
+
"(read it from `status`); a stale value fails without writing. It is",
|
|
1223
|
+
"unrelated to TapHound generation session revisions (observe +1, step +3",
|
|
1224
|
+
"with its post-action observation), which the ledger never stores."
|
|
1163
1225
|
].join("\n");
|
|
1164
1226
|
}
|
|
1165
1227
|
|
|
@@ -1172,6 +1234,9 @@ async function main() {
|
|
|
1172
1234
|
let output;
|
|
1173
1235
|
if (command === "init") {
|
|
1174
1236
|
const input = options(argv, ["--input", "--out"]);
|
|
1237
|
+
if (input.input === undefined) {
|
|
1238
|
+
fail("CASE_SUITE_USAGE", "init requires --input");
|
|
1239
|
+
}
|
|
1175
1240
|
output = await init(input.input, input.out);
|
|
1176
1241
|
} else if (command === "validate") {
|
|
1177
1242
|
const input = options(argv, ["--suite"]);
|
|
@@ -104,7 +104,7 @@ Orchestrator (lean context)
|
|
|
104
104
|
| contextOnly | no | `false` | Run only the Context lifecycle (ensure/refresh/regenerate); skip Brief authoring |
|
|
105
105
|
| observeSnapshot | no | — | Pre-captured `taphound observe --json` result |
|
|
106
106
|
| device | no | doctor auto-selects | Device serial |
|
|
107
|
-
| output | no | `.taphound/
|
|
107
|
+
| output | no | `.taphound/briefs/<caseId>/taphound-journey-brief.md` | Brief output path (relative to project); must be under `.taphound/briefs/` or `.taphound/suites/<suite-id>/briefs/` |
|
|
108
108
|
|
|
109
109
|
**Hard rule**: NEVER search for or assume files named `plan.md`,
|
|
110
110
|
`requirement.md`, or any convention. Read ONLY files the caller explicitly
|
|
@@ -114,13 +114,18 @@ passes via `contextPaths`. If no `contextPaths` are supplied, work from
|
|
|
114
114
|
## Output
|
|
115
115
|
|
|
116
116
|
For a Brief run, the Skill writes a `taphound-journey-brief.md` at the
|
|
117
|
-
`output` path and returns a structured JSON summary
|
|
117
|
+
`output` path and returns a structured JSON summary. Briefs are committed
|
|
118
|
+
TapHound material: write them only under `.taphound/briefs/<caseId>/`
|
|
119
|
+
(standalone) or `.taphound/suites/<suite-id>/briefs/<caseId>/` (Case Suite),
|
|
120
|
+
never elsewhere in the project (for example `doc/` or the project root).
|
|
121
|
+
`generation start --brief` rejects any other location with `BRIEF_INVALID`.
|
|
122
|
+
Without a `caseId`, use a short kebab-case name of the Goal as the directory.
|
|
118
123
|
|
|
119
124
|
```json
|
|
120
125
|
{
|
|
121
126
|
"status": "authored",
|
|
122
127
|
"caseId": "CASE-002",
|
|
123
|
-
"path": ".taphound/
|
|
128
|
+
"path": ".taphound/briefs/CASE-002/taphound-journey-brief.md",
|
|
124
129
|
"sha256": "<exact-byte-hash>",
|
|
125
130
|
"edgesVerified": 2,
|
|
126
131
|
"edgesNeedsObservation": 1
|
|
@@ -43,7 +43,9 @@ The orchestrator dispatches your task with:
|
|
|
43
43
|
- **observeSnapshot** (optional): Pre-captured `taphound observe --json`
|
|
44
44
|
result. Use it directly; do NOT call `taphound observe` when provided.
|
|
45
45
|
- **output** (optional): Brief output path, defaults to
|
|
46
|
-
`.taphound/
|
|
46
|
+
`.taphound/briefs/<caseId>/taphound-journey-brief.md` (relative to
|
|
47
|
+
project). It must stay under `.taphound/briefs/` or
|
|
48
|
+
`.taphound/suites/<suite-id>/briefs/`; refuse any other location.
|
|
47
49
|
|
|
48
50
|
## Output
|
|
49
51
|
|
|
@@ -31,7 +31,7 @@ generate`/`refresh`/`rehash`/`validate`/`status`/`list`、`taphound observe`。
|
|
|
31
31
|
| contextPaths | 否 | 显式文档路径数组,只读这些 |
|
|
32
32
|
| contextOnly | 否 | 为 `true` 时只运行 Context 生命周期(Phase 0),返回 Context 摘要 JSON,不写 Brief |
|
|
33
33
|
| observeSnapshot | 否 | 预采集的 `taphound observe --json` 结果;提供则直接用,不再调 observe |
|
|
34
|
-
| output | 否 | Brief 输出路径,默认 `.taphound/
|
|
34
|
+
| output | 否 | Brief 输出路径,默认 `.taphound/briefs/<caseId>/taphound-journey-brief.md`;只能位于 `.taphound/briefs/` 或 `.taphound/suites/<suite-id>/briefs/` 下,拒绝其他位置 |
|
|
35
35
|
|
|
36
36
|
## 输出
|
|
37
37
|
|
|
@@ -86,7 +86,8 @@ risk confirmation, recovery, or final Replay rules.
|
|
|
86
86
|
|
|
87
87
|
`journeyBrief` is the Skill-level handoff for one Journey Case. When present,
|
|
88
88
|
it carries `{path, sha256}` pointing to a project-relative
|
|
89
|
-
`taphound-journey-brief.md
|
|
89
|
+
`taphound-journey-brief.md` under `.taphound/briefs/` or
|
|
90
|
+
`.taphound/suites/<suite-id>/briefs/`. Bind the same path into Core with
|
|
90
91
|
`generation start --brief <path>`: Core reads the file itself, computes the
|
|
91
92
|
SHA-256 (never trust an agent-supplied hash), and persists `sourceBrief` in
|
|
92
93
|
the session and the exported meta sidecar, so `journey check` reports
|
|
@@ -264,12 +265,18 @@ them with `MANUAL_STEP_REQUIRED`.
|
|
|
264
265
|
node <skill>/scripts/envelope.mjs bind \
|
|
265
266
|
--input <draft-envelope-path> \
|
|
266
267
|
--from <previous-observe-or-step-output-path> \
|
|
267
|
-
--out <envelope-path>
|
|
268
|
+
--out <envelope-path> \
|
|
269
|
+
--project <project>
|
|
268
270
|
```
|
|
269
271
|
The draft envelope needs only `version` and `proposal` (binding may be
|
|
270
272
|
omitted or stale); `bind` fills `proposal.binding` from the preceding
|
|
271
273
|
observe output, step output, or raw binding, adds `snapshotRef` when
|
|
272
|
-
absent, and validates the result offline.
|
|
274
|
+
absent, and validates the result offline. It also compares
|
|
275
|
+
`proposal.activity.before` with the bound snapshot's Activity (read
|
|
276
|
+
from `snapshotRef` under `--project`) and fails with
|
|
277
|
+
`ENVELOPE_ACTIVITY_MISMATCH`, naming the snapshot Activity, before any
|
|
278
|
+
device work. `activityCheck: "unverified"` in its output means no
|
|
279
|
+
snapshot was readable; Core still enforces the check on `step`. The helper contract:
|
|
273
280
|
`node <skill>/scripts/envelope.mjs help`. The resulting shape:
|
|
274
281
|
```json
|
|
275
282
|
{
|
|
@@ -375,10 +382,12 @@ the index with `taphound knowledge rehash --project <project> --json` and
|
|
|
375
382
|
taphound journey check \
|
|
376
383
|
--project <project> \
|
|
377
384
|
--context .taphound/context/project-context.json \
|
|
385
|
+
--journey .taphound/journeys/<name>.json \
|
|
378
386
|
--json
|
|
379
387
|
```
|
|
380
|
-
The newly published Journey must classify as `fresh`.
|
|
381
|
-
|
|
388
|
+
The newly published Journey must classify as `fresh`. `--journey`
|
|
389
|
+
limits the audit to that Journey; without it `journey check` audits every
|
|
390
|
+
committed Journey under `.taphound/journeys` by comparing
|
|
382
391
|
its sidecar bindings (project, config, and `contextSelection` module
|
|
383
392
|
hashes, plus the Brief content hash when `sourceBrief` is bound) against
|
|
384
393
|
the live project. `--strict` exits `1` when any Journey
|
|
@@ -488,6 +497,13 @@ the index with `taphound knowledge rehash --project <project> --json` and
|
|
|
488
497
|
session's selected module shards, stop and report a Context coverage gap.
|
|
489
498
|
Do not add modules after start because `contextSelection` is bound to the
|
|
490
499
|
authoritative session.
|
|
500
|
+
- When TapHound itself misbehaves (a crash, a result that contradicts the
|
|
501
|
+
device, or unexplained slowness), run
|
|
502
|
+
`taphound diagnose export --project <project>` and give the user the printed
|
|
503
|
+
bundle path to attach to their report. The bundle is redacted; do not add
|
|
504
|
+
paths, screenshots, or log excerpts to the report yourself.
|
|
505
|
+
- `UI_SNAPSHOT_FAILED` exits `3`: it is an environment failure, not
|
|
506
|
+
evidence about the app. Rerun before diagnosing the Journey.
|
|
491
507
|
- Repeated `UI_SNAPSHOT_FAILED` ("UIAutomator dump failed") on a slow or
|
|
492
508
|
busy device usually means the dump deadline is too tight, not that the
|
|
493
509
|
device is broken. Raise `ui.snapshotTimeoutMs` in `.taphound/config.json`
|
|
@@ -11,7 +11,9 @@ Use this prompt only when the caller supplied `journeyBrief`.
|
|
|
11
11
|
## Validation
|
|
12
12
|
|
|
13
13
|
1. Resolve the path beneath the project root without following a symlink
|
|
14
|
-
outside it. Its basename must be `taphound-journey-brief.md
|
|
14
|
+
outside it. Its basename must be `taphound-journey-brief.md`, and it must
|
|
15
|
+
live under `.taphound/briefs/` or `.taphound/suites/<suite-id>/briefs/`
|
|
16
|
+
(Core rejects other locations with `BRIEF_INVALID`).
|
|
15
17
|
2. Compute SHA-256 over the exact file bytes and compare it with the binding.
|
|
16
18
|
3. Require YAML frontmatter values:
|
|
17
19
|
- `schemaVersion: 2`
|
|
@@ -102,6 +102,15 @@ the observe result).
|
|
|
102
102
|
varying middle can identify the event, use `match: "regex"` and anchor
|
|
103
103
|
the stable words instead of the varying text.
|
|
104
104
|
- `activity`: a specific Activity should be foregrounded.
|
|
105
|
+
- **Screens that load asynchronously**: idle detection only proves the
|
|
106
|
+
layout tree stopped changing. A full-screen spinner or skeleton is a
|
|
107
|
+
static tree, so the post-action snapshot can be the loading state. When
|
|
108
|
+
an action opens a screen that loads, prefer an `element` expect on a
|
|
109
|
+
control of the loaded screen: Core polls until it appears and returns
|
|
110
|
+
that settled layout as the next snapshot. When the step needs a `logcat`
|
|
111
|
+
expect instead, follow it with a `wait` step whose `element` expect names
|
|
112
|
+
the loaded screen's control, rather than proposing the next action
|
|
113
|
+
against a loading snapshot.
|
|
105
114
|
- Do not add expectations you cannot verify from source code or Context.
|
|
106
115
|
- Do not invent log patterns that don't exist in the source.
|
|
107
116
|
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
// schema changes, update this script and its test together.
|
|
10
10
|
import process from "node:process";
|
|
11
11
|
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
12
|
-
import { dirname, resolve } from "node:path";
|
|
12
|
+
import { dirname, join, resolve } from "node:path";
|
|
13
13
|
|
|
14
14
|
const sha256Pattern = /^[a-f\d]{64}$/;
|
|
15
15
|
const generationIdPattern = /^[A-Za-z\d](?:[A-Za-z\d._-]*[A-Za-z\d])?$/;
|
|
@@ -370,6 +370,14 @@ function validateEnvelope(envelope) {
|
|
|
370
370
|
|
|
371
371
|
// A bind source is one observe output, one step output, or a raw binding
|
|
372
372
|
// object. The binding fields are copied verbatim; nothing is invented.
|
|
373
|
+
const BIND_SOURCE_HINT = "bind --from expects the unmodified stdout of "
|
|
374
|
+
+ "`taphound generation observe --json` (status \"observed\" with "
|
|
375
|
+
+ "generationId, baseRevision, snapshotHash, snapshotRef) or of a succeeded "
|
|
376
|
+
+ "`taphound generation step --json` (status \"succeeded\" with nextBinding "
|
|
377
|
+
+ "and nextSnapshotRef); save it with `> file` instead of assembling a "
|
|
378
|
+
+ "subset. A bare binding {generationId, baseRevision, snapshotHash} is also "
|
|
379
|
+
+ "accepted";
|
|
380
|
+
|
|
373
381
|
function readBindingFromSource(source) {
|
|
374
382
|
if (!isPlainObject(source)) {
|
|
375
383
|
fail("ENVELOPE_INVALID", "bind source must be a JSON object");
|
|
@@ -448,7 +456,36 @@ async function readJsonFile(path, label) {
|
|
|
448
456
|
}
|
|
449
457
|
}
|
|
450
458
|
|
|
451
|
-
|
|
459
|
+
// The Activity the bound snapshot was captured on, taken from the bind source
|
|
460
|
+
// (full observe/step output), the inline envelope snapshot, or the
|
|
461
|
+
// Store-owned snapshot file under the project root. Undefined when none of
|
|
462
|
+
// them is readable; Core still enforces the check on `generation step`.
|
|
463
|
+
async function boundSnapshotActivity(source, envelope, snapshotRef, projectRoot) {
|
|
464
|
+
const inline = [
|
|
465
|
+
["bind source snapshot", source.snapshot],
|
|
466
|
+
["bind source nextSnapshot", source.nextSnapshot],
|
|
467
|
+
["envelope.snapshot", envelope.snapshot]
|
|
468
|
+
];
|
|
469
|
+
for (const [from, snapshot] of inline) {
|
|
470
|
+
if (isPlainObject(snapshot) && typeof snapshot.activity === "string") {
|
|
471
|
+
return { activity: snapshot.activity, from };
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
const ref = envelope.snapshotRef ?? snapshotRef;
|
|
475
|
+
if (typeof ref !== "string" || !snapshotRefPattern.test(ref)) {
|
|
476
|
+
return undefined;
|
|
477
|
+
}
|
|
478
|
+
try {
|
|
479
|
+
const snapshot = JSON.parse(await readFile(join(projectRoot, ref), "utf8"));
|
|
480
|
+
return isPlainObject(snapshot) && typeof snapshot.activity === "string"
|
|
481
|
+
? { activity: snapshot.activity, from: ref }
|
|
482
|
+
: undefined;
|
|
483
|
+
} catch {
|
|
484
|
+
return undefined;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
async function bind(inputPath, fromPath, outPath, projectRoot) {
|
|
452
489
|
const envelope = await readJsonFile(inputPath, "envelope input");
|
|
453
490
|
exactKeys(
|
|
454
491
|
"envelope input",
|
|
@@ -460,7 +497,16 @@ async function bind(inputPath, fromPath, outPath) {
|
|
|
460
497
|
fail("ENVELOPE_INVALID", "envelope.version must be 1");
|
|
461
498
|
}
|
|
462
499
|
const source = await readJsonFile(fromPath, "bind source");
|
|
463
|
-
|
|
500
|
+
let bindingSource;
|
|
501
|
+
try {
|
|
502
|
+
bindingSource = readBindingFromSource(source);
|
|
503
|
+
} catch (error) {
|
|
504
|
+
if (error?.code === "ENVELOPE_INVALID") {
|
|
505
|
+
fail("ENVELOPE_INVALID", `${error.message}. ${BIND_SOURCE_HINT}`);
|
|
506
|
+
}
|
|
507
|
+
throw error;
|
|
508
|
+
}
|
|
509
|
+
const { binding, snapshotRef } = bindingSource;
|
|
464
510
|
const proposal = isPlainObject(envelope.proposal)
|
|
465
511
|
? { ...envelope.proposal, binding }
|
|
466
512
|
: undefined;
|
|
@@ -477,6 +523,23 @@ async function bind(inputPath, fromPath, outPath) {
|
|
|
477
523
|
: { snapshot: envelope.snapshot })
|
|
478
524
|
};
|
|
479
525
|
validateEnvelope(bound);
|
|
526
|
+
const snapshotActivity = await boundSnapshotActivity(
|
|
527
|
+
source,
|
|
528
|
+
bound,
|
|
529
|
+
snapshotRef,
|
|
530
|
+
projectRoot
|
|
531
|
+
);
|
|
532
|
+
const before = proposal.activity?.before;
|
|
533
|
+
if (snapshotActivity !== undefined && before !== snapshotActivity.activity) {
|
|
534
|
+
fail(
|
|
535
|
+
"ENVELOPE_ACTIVITY_MISMATCH",
|
|
536
|
+
`proposal.activity.before ${String(before)} does not match the bound snapshot Activity ${
|
|
537
|
+
snapshotActivity.activity
|
|
538
|
+
} (from ${snapshotActivity.from}); set activity.before to ${
|
|
539
|
+
snapshotActivity.activity
|
|
540
|
+
}`
|
|
541
|
+
);
|
|
542
|
+
}
|
|
480
543
|
if (outPath !== undefined) {
|
|
481
544
|
const absolute = resolve(outPath);
|
|
482
545
|
await mkdir(dirname(absolute), { recursive: true });
|
|
@@ -488,7 +551,8 @@ async function bind(inputPath, fromPath, outPath) {
|
|
|
488
551
|
exitCode: 0,
|
|
489
552
|
path: outPath,
|
|
490
553
|
binding,
|
|
491
|
-
...(snapshotRef === undefined ? {} : { snapshotRef })
|
|
554
|
+
...(snapshotRef === undefined ? {} : { snapshotRef }),
|
|
555
|
+
activityCheck: snapshotActivity === undefined ? "unverified" : "matched"
|
|
492
556
|
})}\n`);
|
|
493
557
|
return;
|
|
494
558
|
}
|
|
@@ -515,16 +579,22 @@ function help() {
|
|
|
515
579
|
"Commands:",
|
|
516
580
|
" validate --input <envelope.json>",
|
|
517
581
|
" Validate one generation step envelope offline (no device, no session).",
|
|
518
|
-
" bind --input <envelope.json> --from <observe-or-step-output.json> [--out <path>]",
|
|
582
|
+
" bind --input <envelope.json> --from <observe-or-step-output.json> [--out <path>] [--project <root>]",
|
|
519
583
|
" Fill proposal.binding (and snapshotRef when absent) from the preceding",
|
|
520
584
|
" observe output, step output, or raw binding, then validate.",
|
|
585
|
+
" It also checks proposal.activity.before against the bound snapshot's",
|
|
586
|
+
" Activity (from the full output, the inline snapshot, or the snapshot",
|
|
587
|
+
" file under --project, default the current directory) and fails with",
|
|
588
|
+
" ENVELOPE_ACTIVITY_MISMATCH naming the snapshot Activity. With --out,",
|
|
589
|
+
" activityCheck reports \"matched\" or \"unverified\" (no readable snapshot).",
|
|
521
590
|
"",
|
|
522
591
|
"bind writes the bound envelope to --out; without --out the bound envelope",
|
|
523
592
|
"itself is the single stdout JSON value.",
|
|
524
593
|
"",
|
|
525
|
-
"Revision rule:
|
|
526
|
-
"
|
|
527
|
-
"
|
|
594
|
+
"Revision rule: observe advances the session revision by 1. A succeeded",
|
|
595
|
+
"step advances it by 2 for the step plus 1 for the post-action observation",
|
|
596
|
+
"behind nextBinding, so consecutive steps are 3 apart. Never compute",
|
|
597
|
+
"revisions: bind from the latest observe or step output."
|
|
528
598
|
].join("\n");
|
|
529
599
|
}
|
|
530
600
|
|
|
@@ -541,11 +611,11 @@ async function main() {
|
|
|
541
611
|
return;
|
|
542
612
|
}
|
|
543
613
|
if (command === "bind") {
|
|
544
|
-
const input = options(argv, ["--input", "--from", "--out"]);
|
|
614
|
+
const input = options(argv, ["--input", "--from", "--out", "--project"]);
|
|
545
615
|
if (input.input === undefined || input.from === undefined) {
|
|
546
616
|
fail("ENVELOPE_USAGE", "bind requires --input and --from");
|
|
547
617
|
}
|
|
548
|
-
await bind(input.input, input.from, input.out);
|
|
618
|
+
await bind(input.input, input.from, input.out, resolve(input.project ?? "."));
|
|
549
619
|
return;
|
|
550
620
|
}
|
|
551
621
|
fail("ENVELOPE_USAGE", `Unknown command ${command}`);
|
|
@@ -256,13 +256,16 @@ supported semantic element or structured event. Such a Case stays `PAUSED`,
|
|
|
256
256
|
never silently weaker.
|
|
257
257
|
|
|
258
258
|
The helper also requires a machine-generated process receipt because a report
|
|
259
|
-
file alone cannot establish an independent CLI invocation
|
|
259
|
+
file alone cannot establish an independent CLI invocation. `taphound verify
|
|
260
|
+
--journey` writes it as `receipt.json` beside the published `report.json` and
|
|
261
|
+
prints its path as `receiptPath` in the `--json` output:
|
|
260
262
|
|
|
261
263
|
```json
|
|
262
264
|
{
|
|
263
265
|
"version": 1,
|
|
264
266
|
"argv": [
|
|
265
267
|
"verify", "--project", "/absolute/project",
|
|
268
|
+
"--config", "/absolute/project/.taphound/config.json",
|
|
266
269
|
"--journey", "/absolute/project/.taphound/journeys/forward.json",
|
|
267
270
|
"--device", "emulator-5554", "--policy-from-meta", "--json"
|
|
268
271
|
],
|
|
@@ -273,9 +276,13 @@ file alone cannot establish an independent CLI invocation:
|
|
|
273
276
|
}
|
|
274
277
|
```
|
|
275
278
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
+
`argv` is the normalized invocation TapHound ran: absolute project, config,
|
|
280
|
+
and Journey paths plus the selected device serial. Pass the `receiptPath`
|
|
281
|
+
that `verify` printed; never write or edit a receipt by hand. A missing
|
|
282
|
+
`receiptPath` (the receipt could not be written, reported on stderr) means the
|
|
283
|
+
evidence is unavailable, so rerun `verify`. Pass absolute, symlink-free
|
|
284
|
+
`--project` and `--journey` paths so the recorded paths match the helper's
|
|
285
|
+
checks.
|
|
279
286
|
|
|
280
287
|
**A, historical worktree:**
|
|
281
288
|
|
|
@@ -288,7 +295,8 @@ exists.
|
|
|
288
295
|
--device <serial> --policy-from-meta --json
|
|
289
296
|
```
|
|
290
297
|
|
|
291
|
-
2.
|
|
298
|
+
2. Take `reportPath` and `receiptPath` from that `verify --json` output.
|
|
299
|
+
Prepare with a private input:
|
|
292
300
|
|
|
293
301
|
```json
|
|
294
302
|
{
|
|
@@ -306,7 +314,7 @@ exists.
|
|
|
306
314
|
"journeyPath": "/absolute/old-project/.taphound/journeys/forward.json",
|
|
307
315
|
"metaPath": "/absolute/old-project/.taphound/journeys/forward.meta.json",
|
|
308
316
|
"reportPath": "/absolute/old-project/.taphound/build/runs/<run>/report.json",
|
|
309
|
-
"receiptPath": "/absolute/old-project/.taphound/build/
|
|
317
|
+
"receiptPath": "/absolute/old-project/.taphound/build/runs/<run>/receipt.json"
|
|
310
318
|
}
|
|
311
319
|
}
|
|
312
320
|
```
|
|
@@ -332,7 +340,7 @@ exists.
|
|
|
332
340
|
goal/scenario against B's current Project Context and UI, but copy every
|
|
333
341
|
observable exactly into its deterministic expectations. Finalize it, then
|
|
334
342
|
launch a separate strict `verify` process on the installed refactored APK
|
|
335
|
-
and
|
|
343
|
+
and keep the `reportPath` and `receiptPath` it prints.
|
|
336
344
|
3. Compare only after that independent Replay:
|
|
337
345
|
|
|
338
346
|
```
|
|
@@ -449,7 +449,7 @@ async function compare(handoff, projectPath, journeyPath, reportPath, receiptPat
|
|
|
449
449
|
await checkReceipt(receiptFile, project, journeyFile, reportFile,
|
|
450
450
|
data.manifest.base.device, {
|
|
451
451
|
journey: journeyHash(journey), report: await fileHash(reportFile)
|
|
452
|
-
}, report.status === "failed" ?
|
|
452
|
+
}, report.status === "failed" ? 1 : 0);
|
|
453
453
|
const covered = checkReplay(project, data.manifest.base.device,
|
|
454
454
|
data.caseData, journey, meta, report, reportFile);
|
|
455
455
|
if (report.runId === data.manifest.base.runId) {
|
|
@@ -15,6 +15,12 @@ export interface AppiumHttpClient {
|
|
|
15
15
|
export interface AppiumProviderOptions {
|
|
16
16
|
endpoint?: string | undefined;
|
|
17
17
|
mapTestTagToResourceId?: boolean | undefined;
|
|
18
|
+
/** Called after each attempt to recreate a degraded session. */
|
|
19
|
+
onSessionRecovery?: ((succeeded: boolean) => void) | undefined;
|
|
20
|
+
}
|
|
21
|
+
export declare class AppiumHttpError extends Error {
|
|
22
|
+
readonly status: number;
|
|
23
|
+
constructor(status: number);
|
|
18
24
|
}
|
|
19
25
|
export declare class FetchAppiumHttpClient implements AppiumHttpClient {
|
|
20
26
|
private readonly endpoint;
|
|
@@ -28,7 +34,9 @@ export declare class AppiumUiSnapshotProviderFactory implements UiSnapshotProvid
|
|
|
28
34
|
private readonly endpoint;
|
|
29
35
|
private readonly http;
|
|
30
36
|
private readonly settings;
|
|
37
|
+
private readonly onSessionRecovery;
|
|
31
38
|
constructor(runner: ProcessRunner, http?: AppiumHttpClient, options?: AppiumProviderOptions);
|
|
32
39
|
probe(timeoutMs?: number): Promise<boolean>;
|
|
33
40
|
open(options: OpenUiSnapshotProviderOptions): Promise<UiSnapshotProvider>;
|
|
41
|
+
private createSession;
|
|
34
42
|
}
|