@hizliemre/horse-code 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +150 -0
- package/dist/app-SB2L34JW.js +6217 -0
- package/dist/chunk-2DGO2BUB.js +4490 -0
- package/dist/chunk-2SVAHH5N.js +60 -0
- package/dist/chunk-3XVZXTB6.js +4469 -0
- package/dist/chunk-5UWA2UBM.js +69 -0
- package/dist/chunk-7TBYMFMG.js +147 -0
- package/dist/chunk-B67BK5GQ.js +34 -0
- package/dist/chunk-BY4DP7IE.js +20 -0
- package/dist/chunk-DKVIN43T.js +54 -0
- package/dist/chunk-DTWKSZXY.js +162 -0
- package/dist/chunk-F2IALVBU.js +212 -0
- package/dist/chunk-FFYBY2NA.js +392 -0
- package/dist/chunk-FGVJFMK5.js +123 -0
- package/dist/chunk-H2FDGPVW.js +42 -0
- package/dist/chunk-HBSC2HT2.js +85 -0
- package/dist/chunk-IW2KBAVZ.js +21 -0
- package/dist/chunk-JWAEW7AJ.js +121 -0
- package/dist/chunk-NNTIACT4.js +163 -0
- package/dist/chunk-O74BDQKS.js +28 -0
- package/dist/chunk-PGOYDOI4.js +426 -0
- package/dist/chunk-QF4MP6BS.js +69 -0
- package/dist/chunk-SSDLHWSF.js +35 -0
- package/dist/chunk-TOPZL5SU.js +1052 -0
- package/dist/chunk-YBWTCXUS.js +153 -0
- package/dist/chunk-YILDXPSI.js +1363 -0
- package/dist/clean-YOQATBMZ.js +18 -0
- package/dist/cli.js +1495 -0
- package/dist/discover-5URG7C4J.js +52 -0
- package/dist/fix-HBBOTUWM.js +34 -0
- package/dist/frontmatter-UNIPNLLO.js +6 -0
- package/dist/git-VTSZALSR.js +6 -0
- package/dist/install-O34KMWJB.js +113 -0
- package/dist/main-branch-KGWUINYQ.js +19 -0
- package/dist/ongoing-OV5XROTU.js +70 -0
- package/dist/project-graph-IOPCSZUA.js +56 -0
- package/dist/run-LQOZ5I7Z.js +610 -0
- package/dist/save-skills-OHYGVTQ4.js +13 -0
- package/dist/source-cache-XEK5WN7I.js +29 -0
- package/dist/trace-ZMB7LT7W.js +66 -0
- package/dist/trace-adopt-C6TUWFJL.js +79 -0
- package/dist/trace-run-F23MFTY4.js +24 -0
- package/dist/triage-2J3T5PVQ.js +30 -0
- package/dist/verify-WQ3GHION.js +479 -0
- package/dist/worktree-F7TWLWLN.js +87 -0
- package/package.json +64 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import {
|
|
2
|
+
GITIGNORE_MARKER,
|
|
3
|
+
MAX_TRACE_FILE_CHARS,
|
|
4
|
+
TRACEABLE_EXT,
|
|
5
|
+
TRACE_DIR,
|
|
6
|
+
TRACE_INDEX,
|
|
7
|
+
TRACE_OUTPUT_TOKENS,
|
|
8
|
+
discoverTraceRoot,
|
|
9
|
+
ensureGitignore,
|
|
10
|
+
everTraceable,
|
|
11
|
+
hashContent,
|
|
12
|
+
loadTraceIndex,
|
|
13
|
+
localOnly,
|
|
14
|
+
mergeTraceIndexes,
|
|
15
|
+
parseTraceIndex,
|
|
16
|
+
planTraces,
|
|
17
|
+
pruneTraces,
|
|
18
|
+
readTrace,
|
|
19
|
+
readTraceSync,
|
|
20
|
+
saveTrace,
|
|
21
|
+
saveTraceIndex,
|
|
22
|
+
serializeTraceIndex,
|
|
23
|
+
setTraceRoot,
|
|
24
|
+
sharedDerived,
|
|
25
|
+
traceCoverage,
|
|
26
|
+
traceDir,
|
|
27
|
+
tracePath,
|
|
28
|
+
tracePrompt,
|
|
29
|
+
traceRootRel,
|
|
30
|
+
traceState,
|
|
31
|
+
traceable
|
|
32
|
+
} from "./chunk-FFYBY2NA.js";
|
|
33
|
+
import "./chunk-SSDLHWSF.js";
|
|
34
|
+
import "./chunk-B67BK5GQ.js";
|
|
35
|
+
export {
|
|
36
|
+
GITIGNORE_MARKER,
|
|
37
|
+
MAX_TRACE_FILE_CHARS,
|
|
38
|
+
TRACEABLE_EXT,
|
|
39
|
+
TRACE_DIR,
|
|
40
|
+
TRACE_INDEX,
|
|
41
|
+
TRACE_OUTPUT_TOKENS,
|
|
42
|
+
discoverTraceRoot,
|
|
43
|
+
ensureGitignore,
|
|
44
|
+
everTraceable,
|
|
45
|
+
hashContent,
|
|
46
|
+
loadTraceIndex,
|
|
47
|
+
localOnly,
|
|
48
|
+
mergeTraceIndexes,
|
|
49
|
+
parseTraceIndex,
|
|
50
|
+
planTraces,
|
|
51
|
+
pruneTraces,
|
|
52
|
+
readTrace,
|
|
53
|
+
readTraceSync,
|
|
54
|
+
saveTrace,
|
|
55
|
+
saveTraceIndex,
|
|
56
|
+
serializeTraceIndex,
|
|
57
|
+
setTraceRoot,
|
|
58
|
+
sharedDerived,
|
|
59
|
+
traceCoverage,
|
|
60
|
+
traceDir,
|
|
61
|
+
tracePath,
|
|
62
|
+
tracePrompt,
|
|
63
|
+
traceRootRel,
|
|
64
|
+
traceState,
|
|
65
|
+
traceable
|
|
66
|
+
};
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import {
|
|
2
|
+
hashContent,
|
|
3
|
+
traceDir,
|
|
4
|
+
traceRootRel
|
|
5
|
+
} from "./chunk-FFYBY2NA.js";
|
|
6
|
+
import "./chunk-SSDLHWSF.js";
|
|
7
|
+
import "./chunk-B67BK5GQ.js";
|
|
8
|
+
|
|
9
|
+
// src/engine/trace-adopt.ts
|
|
10
|
+
import { readFile, readdir } from "fs/promises";
|
|
11
|
+
import { join, relative, sep } from "path";
|
|
12
|
+
var REF = /(?:^|[\s`"'(\[])((?:src|tests?|libs?|apps?|packages?|toucan|integrators)\/[A-Za-z0-9_./-]+\.[A-Za-z]{1,5})(?=[\s`"')\].,;:]|$)/gm;
|
|
13
|
+
function isPerFileTrace(rel, sourceFiles) {
|
|
14
|
+
return rel.endsWith(".md") && sourceFiles.has(rel.slice(0, -".md".length));
|
|
15
|
+
}
|
|
16
|
+
async function markdownUnder(dir, base = dir) {
|
|
17
|
+
const out = [];
|
|
18
|
+
let entries;
|
|
19
|
+
try {
|
|
20
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
21
|
+
} catch {
|
|
22
|
+
return out;
|
|
23
|
+
}
|
|
24
|
+
for (const e of entries) {
|
|
25
|
+
const abs = join(dir, e.name);
|
|
26
|
+
if (e.isDirectory()) out.push(...await markdownUnder(abs, base));
|
|
27
|
+
else if (e.name.toLowerCase().endsWith(".md")) out.push(relative(base, abs).split(sep).join("/"));
|
|
28
|
+
}
|
|
29
|
+
return out;
|
|
30
|
+
}
|
|
31
|
+
async function adoptDocs(cwd, sourceFiles) {
|
|
32
|
+
const root = traceDir(cwd);
|
|
33
|
+
const rootRel = traceRootRel().split(sep).join("/");
|
|
34
|
+
const covered = {};
|
|
35
|
+
const bare = [];
|
|
36
|
+
for (const rel of await markdownUnder(root)) {
|
|
37
|
+
if (isPerFileTrace(rel, sourceFiles)) continue;
|
|
38
|
+
let text;
|
|
39
|
+
try {
|
|
40
|
+
text = await readFile(join(root, rel), "utf8");
|
|
41
|
+
} catch {
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
const doc = `${rootRel}/${rel}`;
|
|
45
|
+
let hits = 0;
|
|
46
|
+
for (const m of text.matchAll(REF)) {
|
|
47
|
+
const file = m[1];
|
|
48
|
+
if (!sourceFiles.has(file)) continue;
|
|
49
|
+
hits++;
|
|
50
|
+
if (!covered[file]) covered[file] = doc;
|
|
51
|
+
}
|
|
52
|
+
if (!hits) bare.push(doc);
|
|
53
|
+
}
|
|
54
|
+
return { covered, bare };
|
|
55
|
+
}
|
|
56
|
+
async function indexAdoption(cwd, index, adoption, read) {
|
|
57
|
+
const traces = { ...index.traces };
|
|
58
|
+
let added = 0;
|
|
59
|
+
for (const [file, doc] of Object.entries(adoption.covered)) {
|
|
60
|
+
if (traces[file]) continue;
|
|
61
|
+
const text = await read(file);
|
|
62
|
+
if (text === void 0) continue;
|
|
63
|
+
traces[file] = { hash: hashContent(text), file, writtenAt: 0, doc };
|
|
64
|
+
added++;
|
|
65
|
+
}
|
|
66
|
+
return { index: { ...index, traces }, added };
|
|
67
|
+
}
|
|
68
|
+
function describeAdoption(a, added) {
|
|
69
|
+
const docs = new Set(Object.values(a.covered)).size;
|
|
70
|
+
if (!added) return "No project documents cited files this repository has \u2014 nothing to adopt.";
|
|
71
|
+
return `\u{1F4DA} Adopted **${added} file(s)** already described by ${docs} of the project's own document(s). \`/graph trace\` will skip them; \`graph_trace\` serves the document itself.` + (a.bare.length ? `
|
|
72
|
+
|
|
73
|
+
_${a.bare.length} document(s) cited no source file and were left alone._` : "");
|
|
74
|
+
}
|
|
75
|
+
export {
|
|
76
|
+
adoptDocs,
|
|
77
|
+
describeAdoption,
|
|
78
|
+
indexAdoption
|
|
79
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import {
|
|
2
|
+
TRACE_CONCURRENCY,
|
|
3
|
+
buildBrief,
|
|
4
|
+
coverageFor,
|
|
5
|
+
describePlan,
|
|
6
|
+
planFor,
|
|
7
|
+
runTraces,
|
|
8
|
+
traceableFiles
|
|
9
|
+
} from "./chunk-NNTIACT4.js";
|
|
10
|
+
import "./chunk-IW2KBAVZ.js";
|
|
11
|
+
import "./chunk-DTWKSZXY.js";
|
|
12
|
+
import "./chunk-FFYBY2NA.js";
|
|
13
|
+
import "./chunk-PGOYDOI4.js";
|
|
14
|
+
import "./chunk-SSDLHWSF.js";
|
|
15
|
+
import "./chunk-B67BK5GQ.js";
|
|
16
|
+
export {
|
|
17
|
+
TRACE_CONCURRENCY,
|
|
18
|
+
buildBrief,
|
|
19
|
+
coverageFor,
|
|
20
|
+
describePlan,
|
|
21
|
+
planFor,
|
|
22
|
+
runTraces,
|
|
23
|
+
traceableFiles
|
|
24
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import {
|
|
2
|
+
RequestSizeSchema,
|
|
3
|
+
SIZE_ATTEMPT_MS,
|
|
4
|
+
SIZE_MAX_TURNS,
|
|
5
|
+
SIZE_TOTAL_MS,
|
|
6
|
+
TriageSchema,
|
|
7
|
+
describeEscalation,
|
|
8
|
+
describeSizeDoubt,
|
|
9
|
+
sizeRequest,
|
|
10
|
+
triageFinding
|
|
11
|
+
} from "./chunk-7TBYMFMG.js";
|
|
12
|
+
import "./chunk-TOPZL5SU.js";
|
|
13
|
+
import "./chunk-2SVAHH5N.js";
|
|
14
|
+
import "./chunk-YILDXPSI.js";
|
|
15
|
+
import "./chunk-DTWKSZXY.js";
|
|
16
|
+
import "./chunk-FFYBY2NA.js";
|
|
17
|
+
import "./chunk-PGOYDOI4.js";
|
|
18
|
+
import "./chunk-SSDLHWSF.js";
|
|
19
|
+
import "./chunk-B67BK5GQ.js";
|
|
20
|
+
export {
|
|
21
|
+
RequestSizeSchema,
|
|
22
|
+
SIZE_ATTEMPT_MS,
|
|
23
|
+
SIZE_MAX_TURNS,
|
|
24
|
+
SIZE_TOTAL_MS,
|
|
25
|
+
TriageSchema,
|
|
26
|
+
describeEscalation,
|
|
27
|
+
describeSizeDoubt,
|
|
28
|
+
sizeRequest,
|
|
29
|
+
triageFinding
|
|
30
|
+
};
|
|
@@ -0,0 +1,479 @@
|
|
|
1
|
+
import {
|
|
2
|
+
commitFix,
|
|
3
|
+
describeFix,
|
|
4
|
+
dirtyPaths,
|
|
5
|
+
runFix
|
|
6
|
+
} from "./chunk-JWAEW7AJ.js";
|
|
7
|
+
import {
|
|
8
|
+
buildAskUserTool,
|
|
9
|
+
respondIn
|
|
10
|
+
} from "./chunk-F2IALVBU.js";
|
|
11
|
+
import {
|
|
12
|
+
buildRememberTool,
|
|
13
|
+
buildSkillTool,
|
|
14
|
+
constitutionNote,
|
|
15
|
+
createWebFetchTool,
|
|
16
|
+
deferMcp,
|
|
17
|
+
editFileTool,
|
|
18
|
+
featureSlugFor,
|
|
19
|
+
gitTool,
|
|
20
|
+
refreshAfterChange,
|
|
21
|
+
shellTool,
|
|
22
|
+
specsDir,
|
|
23
|
+
verifyPaths,
|
|
24
|
+
writeFileTool
|
|
25
|
+
} from "./chunk-3XVZXTB6.js";
|
|
26
|
+
import {
|
|
27
|
+
askInUserLanguage,
|
|
28
|
+
inUserLanguage
|
|
29
|
+
} from "./chunk-HBSC2HT2.js";
|
|
30
|
+
import "./chunk-FGVJFMK5.js";
|
|
31
|
+
import "./chunk-NNTIACT4.js";
|
|
32
|
+
import {
|
|
33
|
+
defaultGitRunner
|
|
34
|
+
} from "./chunk-IW2KBAVZ.js";
|
|
35
|
+
import {
|
|
36
|
+
describeEscalation,
|
|
37
|
+
triageFinding
|
|
38
|
+
} from "./chunk-7TBYMFMG.js";
|
|
39
|
+
import {
|
|
40
|
+
BATCH_TOOLS_NOTE,
|
|
41
|
+
contextTools,
|
|
42
|
+
globTool,
|
|
43
|
+
grepTool,
|
|
44
|
+
memoryHints,
|
|
45
|
+
projectToolsNote,
|
|
46
|
+
readFileTool,
|
|
47
|
+
reinforceUsed
|
|
48
|
+
} from "./chunk-TOPZL5SU.js";
|
|
49
|
+
import "./chunk-2SVAHH5N.js";
|
|
50
|
+
import {
|
|
51
|
+
ToolRegistry,
|
|
52
|
+
handedOver,
|
|
53
|
+
runToCompletion
|
|
54
|
+
} from "./chunk-YILDXPSI.js";
|
|
55
|
+
import "./chunk-DTWKSZXY.js";
|
|
56
|
+
import "./chunk-FFYBY2NA.js";
|
|
57
|
+
import {
|
|
58
|
+
loadGraphSync
|
|
59
|
+
} from "./chunk-PGOYDOI4.js";
|
|
60
|
+
import {
|
|
61
|
+
sessionBase
|
|
62
|
+
} from "./chunk-SSDLHWSF.js";
|
|
63
|
+
import "./chunk-B67BK5GQ.js";
|
|
64
|
+
|
|
65
|
+
// src/engine/verify.ts
|
|
66
|
+
import { existsSync, readFileSync } from "fs";
|
|
67
|
+
import { relative, join, resolve } from "path";
|
|
68
|
+
|
|
69
|
+
// src/engine/finding.ts
|
|
70
|
+
import { z } from "zod";
|
|
71
|
+
var FindingSchema = z.object({
|
|
72
|
+
title: z.string().describe(`Short, in the product's terms \u2014 e.g. "Summary screen omits the Product Description label".`),
|
|
73
|
+
detail: z.string().describe("What is wrong, where you saw it, and what should have happened instead."),
|
|
74
|
+
files: z.array(z.string()).default([]).describe("Repo-relative files this appears to involve, if you can name them."),
|
|
75
|
+
acceptance: z.array(z.string()).default([]).describe("What must be true for this to be settled. One checkable statement per line."),
|
|
76
|
+
scenario: z.string().optional().describe("The scenario id you were running when you noticed it, if any.")
|
|
77
|
+
});
|
|
78
|
+
var FindingQueue = class {
|
|
79
|
+
items = [];
|
|
80
|
+
taken = 0;
|
|
81
|
+
add(f) {
|
|
82
|
+
this.items.push(f);
|
|
83
|
+
return this.items.length;
|
|
84
|
+
}
|
|
85
|
+
/** The ones not yet handed to a fix — drained once, so a resumed loop does not re-fix what it fixed. */
|
|
86
|
+
drain() {
|
|
87
|
+
const out = this.items.slice(this.taken);
|
|
88
|
+
this.taken = this.items.length;
|
|
89
|
+
return out;
|
|
90
|
+
}
|
|
91
|
+
all() {
|
|
92
|
+
return [...this.items];
|
|
93
|
+
}
|
|
94
|
+
get length() {
|
|
95
|
+
return this.items.length;
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
function buildReportFindingTool(queue) {
|
|
99
|
+
return {
|
|
100
|
+
name: "report_finding",
|
|
101
|
+
description: "Report something the product does wrong, so that it gets FIXED. It is recorded, and a separate role fixes it \u2014 you never fix it yourself. Give the acceptance criteria: what must be true for it to be settled.\n\nUse it for BOTH: something that misbehaves in a way the current scenario was not asking about, and \u2014 above all \u2014 the reason a scenario does not pass. A scenario that fails is not a result to write down and walk away from; it is a defect, and reporting it here is the only thing that gets it fixed and the step re-run. One call per distinct defect, not one for the scenario.",
|
|
102
|
+
permissionLevel: "safe",
|
|
103
|
+
parameters: FindingSchema,
|
|
104
|
+
describe: (args) => ({
|
|
105
|
+
allowKey: "finding:report",
|
|
106
|
+
preview: `finding: ${String(args.title ?? "").slice(0, 80)}`
|
|
107
|
+
}),
|
|
108
|
+
async run(rawArgs) {
|
|
109
|
+
const parsed = FindingSchema.safeParse(rawArgs);
|
|
110
|
+
if (!parsed.success) {
|
|
111
|
+
return { content: `report_finding: ${parsed.error.issues.map((i) => i.message).join("; ")}`, isError: true };
|
|
112
|
+
}
|
|
113
|
+
const n = queue.add(parsed.data);
|
|
114
|
+
return {
|
|
115
|
+
content: `Finding #${n} recorded: "${parsed.data.title}". Write it into the report now, under a **Findings** section, as OPEN \u2014 with what you saw and where. It is queued to be triaged and fixed by another role, and you will be told the outcome so you can re-check it and update that same entry. Then carry on with the scenario you were running \u2014 do not fix it yourself, and do not mark the scenario failed because of it.`,
|
|
116
|
+
isError: false
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// src/engine/verify.ts
|
|
123
|
+
var VERIFY_MAX_TURNS = 300;
|
|
124
|
+
function testerTools(deps, askUser, findings) {
|
|
125
|
+
const r = new ToolRegistry();
|
|
126
|
+
r.register(readFileTool);
|
|
127
|
+
r.register(writeFileTool);
|
|
128
|
+
r.register(editFileTool);
|
|
129
|
+
r.register(grepTool);
|
|
130
|
+
r.register(globTool);
|
|
131
|
+
r.register(shellTool);
|
|
132
|
+
r.register(gitTool);
|
|
133
|
+
r.register(createWebFetchTool());
|
|
134
|
+
r.register(buildSkillTool(deps.skillRegistry));
|
|
135
|
+
r.register(buildAskUserTool(askUser));
|
|
136
|
+
r.register(buildReportFindingTool(findings));
|
|
137
|
+
r.register(buildRememberTool(deps.rememberFact));
|
|
138
|
+
for (const t of contextTools(deps)) r.register(t);
|
|
139
|
+
deferMcp(r, deps.mcpTools?.() ?? []);
|
|
140
|
+
return r;
|
|
141
|
+
}
|
|
142
|
+
async function runTester(deps, workdir, tools, message, language, law = "", subject) {
|
|
143
|
+
const hints = memoryHints(deps, subject ?? message, { role: "tester", operations: true });
|
|
144
|
+
const { role: agentRole, model, fallbacks, onExhausted, onFallback } = deps.roleRegistry.fallbackOpts("tester");
|
|
145
|
+
const opts = {
|
|
146
|
+
provider: deps.provider,
|
|
147
|
+
role: agentRole,
|
|
148
|
+
model,
|
|
149
|
+
fallbacks,
|
|
150
|
+
onExhausted,
|
|
151
|
+
onFallback,
|
|
152
|
+
// `resolve` already appends the user's rules — adding them again put all 25 in twice.
|
|
153
|
+
systemPrompt: deps.roleRegistry.resolve("tester").systemPrompt + projectToolsNote(tools.list(), !!loadGraphSync(workdir)) + BATCH_TOOLS_NOTE + law + respondIn(language),
|
|
154
|
+
tools,
|
|
155
|
+
maxTurns: VERIFY_MAX_TURNS,
|
|
156
|
+
// A screenshot named in the request comes with it — the same way a mid-run note carries one.
|
|
157
|
+
messages: [
|
|
158
|
+
// What earlier runs learned about this area, ahead of the request — see runTester's own note.
|
|
159
|
+
...hints?.message ? [{ role: "user", content: hints.message }] : [],
|
|
160
|
+
{ role: "user", ...handedOver(message, workdir) }
|
|
161
|
+
],
|
|
162
|
+
permission: deps.permission,
|
|
163
|
+
approve: deps.approve,
|
|
164
|
+
cwd: workdir,
|
|
165
|
+
signal: deps.signal,
|
|
166
|
+
// A verification is the longest interactive stretch there is: the tool cards ARE the record of what was
|
|
167
|
+
// observed, and everything the tester says is addressed to the developer sitting in front of it.
|
|
168
|
+
onActivity: deps.onActivity,
|
|
169
|
+
onLiveActivity: deps.onLiveActivity,
|
|
170
|
+
/** …and a correction — "skip that one, the data is gone" — must reach it mid-run, not after the report. */
|
|
171
|
+
inbox: deps.inbox,
|
|
172
|
+
...deps.note ? { onSay: deps.note } : {}
|
|
173
|
+
};
|
|
174
|
+
const last = await runToCompletion(opts);
|
|
175
|
+
reinforceUsed(deps, hints.ids, last.content ?? "", "tester");
|
|
176
|
+
}
|
|
177
|
+
function testDocDirs(trackedFiles2) {
|
|
178
|
+
const count = /* @__PURE__ */ new Map();
|
|
179
|
+
for (const f of trackedFiles2) {
|
|
180
|
+
const p = f.replace(/\\/g, "/");
|
|
181
|
+
if (!/\.mdx?$/i.test(p)) continue;
|
|
182
|
+
if (!/(^|\/)(test-plans?|test-reports?)\//i.test(p) && !/(test-plan|test-report|-e2e)\.mdx?$/i.test(p)) continue;
|
|
183
|
+
const dir = p.slice(0, p.lastIndexOf("/"));
|
|
184
|
+
if (dir) count.set(dir, (count.get(dir) ?? 0) + 1);
|
|
185
|
+
}
|
|
186
|
+
return [...count.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).slice(0, 4).map(([d]) => d);
|
|
187
|
+
}
|
|
188
|
+
var POINTER_MARK = /<!--\s*report:\s*(.+?)\s*-->/;
|
|
189
|
+
var POINTER_HINT = "<!-- report: PATH -->";
|
|
190
|
+
function readPointer(pointerPath) {
|
|
191
|
+
try {
|
|
192
|
+
const m = POINTER_MARK.exec(readFileSync(pointerPath, "utf8"));
|
|
193
|
+
return m?.[1]?.trim() || void 0;
|
|
194
|
+
} catch {
|
|
195
|
+
return void 0;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
async function trackedFiles(cwd) {
|
|
199
|
+
try {
|
|
200
|
+
const { defaultGitRunner: defaultGitRunner2 } = await import("./git-VTSZALSR.js");
|
|
201
|
+
const r = await defaultGitRunner2(["ls-files"], cwd);
|
|
202
|
+
return r.code === 0 ? r.stdout.split("\n").filter(Boolean) : [];
|
|
203
|
+
} catch {
|
|
204
|
+
return [];
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
function planMessageFor(prompt, planRel, pointerRel, dirRel, docDirs) {
|
|
208
|
+
const where = docDirs.length ? `This project already keeps test documents in ${docDirs.map((d) => `\`${d}\``).join(", ")}. Search there FIRST \u2014 by the feature's name, by the pull request number, by the branch.` : `Search the repository for one anyway: a project can keep them anywhere.`;
|
|
209
|
+
return `${prompt}
|
|
210
|
+
|
|
211
|
+
FIRST, find out whether a test document for this work already exists. ${where} Also check "${dirRel}".
|
|
212
|
+
|
|
213
|
+
A document that already exists is CONTINUED, never replaced. It holds results someone established and evidence they gathered, and writing a second one beside it throws that away while leaving two documents that disagree. If you find one \u2014 even a partly finished one, even one that calls itself a plan \u2014 say so and record it by writing "${pointerRel}" containing one line:
|
|
214
|
+
|
|
215
|
+
${POINTER_HINT.replace("PATH", "the path you found")}
|
|
216
|
+
|
|
217
|
+
\u2026plus a sentence for a human saying which document this run continues. Then STOP.
|
|
218
|
+
|
|
219
|
+
IF THERE IS NONE, look at what you were asked. When the request already names what to check and what the right answer looks like \u2014 "confirm from the screenshot that X renders as Y" \u2014 that IS the plan: write nothing, and say so in one line. A plan document is for "run the tests for this pull request", where what to check has to be worked out first.
|
|
220
|
+
|
|
221
|
+
ONLY IF NEITHER: write a new plan to "${planRel}" \u2014 that exact path, and nowhere else. The directories above are where you LOOK for something that already exists; they are not where a new document goes. Everything a run produces belongs in its own folder, so that one piece of work has one account of itself instead of pieces in two places.
|
|
222
|
+
|
|
223
|
+
Find out what is being verified before you write a scenario: read the pull request or work item if one is named (use the tools you have for it), read the spec and plan in "${dirRel}" if they are there, and read the code that changed. A scenario invented from a title tests nothing.
|
|
224
|
+
|
|
225
|
+
The plan is a table of scenarios. Each row: an id, what the scenario does, what is expected, and WHERE the evidence for it will come from \u2014 which table and column, which log event, which screen. A row whose evidence source you cannot name is a row you cannot verify, so name it or drop the row.
|
|
226
|
+
|
|
227
|
+
Also list, before the table: what the developer must have running, and the exact commands for it. You do not run them.
|
|
228
|
+
|
|
229
|
+
Write at most one file, then stop. Do not run any scenario yet.`;
|
|
230
|
+
}
|
|
231
|
+
function directMessage(prompt, reportRel) {
|
|
232
|
+
return `${prompt}
|
|
233
|
+
|
|
234
|
+
There is no test plan for this, and none is needed \u2014 what you have been asked to check is stated above. Verify exactly that, and nothing more.
|
|
235
|
+
|
|
236
|
+
Record the result in "${reportRel}" with its evidence: what you did, what you observed, and where the evidence came from. Create the file if it is not there; if it is, append rather than replacing what is already recorded.
|
|
237
|
+
|
|
238
|
+
If you cannot run the check \u2014 the environment is not up, the data does not exist \u2014 say which command the developer should run and ask them, then wait. Never start it yourself.
|
|
239
|
+
|
|
240
|
+
When it needs an eye on the screen, describe exactly what to look at and ask what they see. Their answer is the evidence; record it as theirs.
|
|
241
|
+
|
|
242
|
+
${handOffRule}
|
|
243
|
+
|
|
244
|
+
${verdictRule}
|
|
245
|
+
|
|
246
|
+
Whatever is wrong \u2014 the check you were given, or something you noticed beside it \u2014 goes to \`report_finding\`. You never fix anything yourself.`;
|
|
247
|
+
}
|
|
248
|
+
function directResumeMessage(prompt, reportRel, done) {
|
|
249
|
+
return `The findings you reported have been dealt with:
|
|
250
|
+
${done.map((d) => `- ${d}`).join("\n")}
|
|
251
|
+
|
|
252
|
+
Anything marked FIXED is in the working tree and committed. Check the original request again against the corrected product, with fresh evidence, and record the result.
|
|
253
|
+
|
|
254
|
+
` + directMessage(prompt, reportRel);
|
|
255
|
+
}
|
|
256
|
+
function runMessage(prompt, planRel, reportRel, inPlace) {
|
|
257
|
+
const where = inPlace ? `The document is "${planRel}", and it is BOTH the plan and the report \u2014 record each result in it, in place, next to the scenario it belongs to. Do not create a second file.
|
|
258
|
+
|
|
259
|
+
Read it first: some scenarios already have results, established by someone else with evidence they gathered. Do not re-run those and do not rewrite them. Continue from the first one with no result.
|
|
260
|
+
|
|
261
|
+
` : `The test plan is "${planRel}". Work through its scenarios IN ORDER, and record each result in "${reportRel}" \u2014 with its evidence \u2014 before you start the next one.
|
|
262
|
+
|
|
263
|
+
Create the report if it is not there. If it IS there, this is a resumed session: read it first and continue from the first scenario that has no result. Do not re-run what is already recorded, and do not rewrite results someone else established.
|
|
264
|
+
|
|
265
|
+
`;
|
|
266
|
+
return `${prompt}
|
|
267
|
+
|
|
268
|
+
` + where + `Before the first scenario, check what the plan says must be running. If it is not up, tell the developer which command to run and ask them to confirm \u2014 then wait. Never start it yourself.
|
|
269
|
+
|
|
270
|
+
When a scenario needs an eye on the screen, describe exactly what to look at and ask the developer what they see. Their answer is the evidence; record it as theirs.
|
|
271
|
+
|
|
272
|
+
${verdictRule}
|
|
273
|
+
|
|
274
|
+
` + handOffRule;
|
|
275
|
+
}
|
|
276
|
+
var verdictRule = `A scenario ends in one of two ways: it PASSES, or it is still open. There is no third. Never record a scenario as failed and start the next one.
|
|
277
|
+
|
|
278
|
+
When a step does not pass, call \`report_finding\` for EACH defect behind it \u2014 one call per distinct defect, with what you saw and what must be true for it to be settled \u2014 and leave the scenario's own entry open, saying what it is waiting on. The findings are fixed while you wait, and you are handed the corrected product to run that same step again; do not go on to another scenario until it passes.
|
|
279
|
+
|
|
280
|
+
If the developer tells you to leave it and move on, that is their decision and it is fine \u2014 record it as blocked at their request, with their words, and carry on. Yours it is not.`;
|
|
281
|
+
var handOffRule = `Staging, committing and branches are this tool's business, never the developer's: never ask them to \`git add\`, commit or tidy the tree. Ask them only for what nobody else can do \u2014 carry out a scenario, look at a screen, start an environment, decide something.
|
|
282
|
+
|
|
283
|
+
The developer sees the chat and nothing else. The document you are writing is NOT on their screen, so a request that points at it \u2014 "the steps above", "the items listed" \u2014 asks them to follow something they cannot see. When you need them to carry out a scenario, pass the actions to \`ask_user\` in \`steps\`, one action per entry, and put in \`question\` what they should report back.`;
|
|
284
|
+
var MAX_FIX_ROUNDS = 2;
|
|
285
|
+
async function handleFindings(opts, found) {
|
|
286
|
+
const done = [];
|
|
287
|
+
for (const f of found) {
|
|
288
|
+
const t = await triageFinding(opts.deps, opts.workdir, f);
|
|
289
|
+
if (t.depth !== "task") {
|
|
290
|
+
const answer = await askInUserLanguage(
|
|
291
|
+
opts.deps,
|
|
292
|
+
opts.askUser,
|
|
293
|
+
opts.language,
|
|
294
|
+
`${describeEscalation(f, t)}
|
|
295
|
+
|
|
296
|
+
Start that now, or leave it in the report and carry on testing?`,
|
|
297
|
+
[
|
|
298
|
+
{ label: "Leave it \u2014 carry on testing", description: "It stays as an open finding; you can start it after the session." },
|
|
299
|
+
{ label: "Start it now", description: "The test session pauses while it is designed and built." }
|
|
300
|
+
]
|
|
301
|
+
);
|
|
302
|
+
if (!/start it now/i.test(answer.trim())) {
|
|
303
|
+
opts.note?.(`\u{1F4CC} Left open: **${f.title}** \u2014 ${t.reason}`);
|
|
304
|
+
done.push(`${f.title} \u2014 left open, to be handled separately (${t.reason})`);
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
opts.note?.(`\u{1F4CB} **${f.title}** needs ${t.depth === "brainstorm" ? "a design decision" : "a spec and a plan"}. Finish or stop this session, then ask for it as its own piece of work \u2014 the finding and its evidence are in the report.`);
|
|
308
|
+
done.push(`${f.title} \u2014 needs its own piece of work; not fixed in this session`);
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
opts.note?.(`\u{1F527} Fixing: **${f.title}** \u2014 ${t.reason}`);
|
|
312
|
+
const before = await dirtyPaths(defaultGitRunner, opts.workdir);
|
|
313
|
+
const res = await runFix(opts.deps, opts.workdir, f, `fix-${done.length + 1}`);
|
|
314
|
+
const changed = res.fixed ? await commitFix(opts.workdir, f, before) : [];
|
|
315
|
+
await refreshAfterChange({
|
|
316
|
+
cwd: opts.workdir,
|
|
317
|
+
files: changed,
|
|
318
|
+
provider: opts.deps.provider,
|
|
319
|
+
models: opts.deps.roleRegistry.chainFor("tracer", 0),
|
|
320
|
+
signal: opts.deps.signal,
|
|
321
|
+
...opts.note ? { note: opts.note } : {}
|
|
322
|
+
});
|
|
323
|
+
opts.note?.(describeFix(res));
|
|
324
|
+
done.push(res.fixed ? `${f.title} \u2014 FIXED` : `${f.title} \u2014 NOT fixed (${res.notes.join("; ") || "see above"})`);
|
|
325
|
+
}
|
|
326
|
+
return done;
|
|
327
|
+
}
|
|
328
|
+
function resumeMessage(activeRel, reportRel, inPlace, done) {
|
|
329
|
+
return `The findings you reported have been dealt with:
|
|
330
|
+
${done.map((d) => `- ${d}`).join("\n")}
|
|
331
|
+
|
|
332
|
+
Anything marked FIXED has been changed in the working tree and committed. Re-check the scenarios those findings affected \u2014 against the corrected product, with fresh evidence \u2014 and record the result. Then carry on from where you were.
|
|
333
|
+
|
|
334
|
+
Update each finding's OWN entry in the report as you re-check it: FIXED and verified, with the evidence you just gathered, or still OPEN and what you saw this time. A finding left reading OPEN after it was fixed is as wrong as one marked fixed that was not \u2014 the entry is what anyone reads later.
|
|
335
|
+
|
|
336
|
+
Anything NOT fixed stays in the report as an open finding, and the scenario it holds up stays open with it. Do not fix it yourself, and do not close a scenario as failed to get past it \u2014 report what still happens, with the evidence from this attempt, so it goes round again.
|
|
337
|
+
|
|
338
|
+
` + runMessage("", activeRel, reportRel, inPlace);
|
|
339
|
+
}
|
|
340
|
+
function fixBeforeHandOff(askUser, findings, fix, opts) {
|
|
341
|
+
return async (question, ask) => {
|
|
342
|
+
const found = findings.drain();
|
|
343
|
+
if (!found.length || opts.budget.left <= 0) {
|
|
344
|
+
if (found.length) {
|
|
345
|
+
opts.note?.(`\u26A0\uFE0F ${found.length} finding(s) left for the end of the session \u2014 the in-session fix budget is spent.`);
|
|
346
|
+
found.forEach((f) => findings.add(f));
|
|
347
|
+
}
|
|
348
|
+
return askUser(question, ask);
|
|
349
|
+
}
|
|
350
|
+
opts.budget.left--;
|
|
351
|
+
opts.note?.(`\u{1F527} ${found.length} finding(s) reported \u2014 fixing before handing over, so you test the corrected product.`);
|
|
352
|
+
const done = await fix(found);
|
|
353
|
+
const said = done.map((d) => `- ${d}`).join("\n");
|
|
354
|
+
const head = await inUserLanguage(
|
|
355
|
+
opts.deps,
|
|
356
|
+
"Before this: the finding(s) reported during the last step were dealt with.",
|
|
357
|
+
opts.language
|
|
358
|
+
);
|
|
359
|
+
const answer = await askUser(`${head}
|
|
360
|
+
${said}
|
|
361
|
+
|
|
362
|
+
${question}`, ask);
|
|
363
|
+
opts.budget.left = Math.max(opts.budget.left, opts.refill);
|
|
364
|
+
return `${answer}
|
|
365
|
+
|
|
366
|
+
[The findings you reported were handled BEFORE this hand-off:
|
|
367
|
+
${said}
|
|
368
|
+
Anything marked FIXED is in the working tree now. Re-run the step you just handed over, against the corrected product, and do not move on until it passes \u2014 or until you can say precisely what still fails, with evidence. Update each finding's own entry in the report with what you saw this time.]`;
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
async function runVerify(opts) {
|
|
372
|
+
const { deps, workdir, prompt, title, askUser } = opts;
|
|
373
|
+
const slug = featureSlugFor(workdir, title);
|
|
374
|
+
const paths = verifyPaths(workdir, slug);
|
|
375
|
+
const findings = new FindingQueue();
|
|
376
|
+
const dirRel = relative(workdir, paths.dir);
|
|
377
|
+
const planRel = relative(workdir, paths.plan);
|
|
378
|
+
const reportRel = relative(workdir, paths.report);
|
|
379
|
+
const budget = { left: MAX_FIX_ROUNDS };
|
|
380
|
+
const handOff = fixBeforeHandOff(
|
|
381
|
+
askUser,
|
|
382
|
+
findings,
|
|
383
|
+
// The RAW askUser here: handleFindings may need to ask about an escalation, and routing that back through
|
|
384
|
+
// the wrapper would be asking a question about the findings it just drained.
|
|
385
|
+
(found) => handleFindings({
|
|
386
|
+
deps,
|
|
387
|
+
workdir,
|
|
388
|
+
askUser,
|
|
389
|
+
...opts.note ? { note: opts.note } : {},
|
|
390
|
+
...opts.language ? { language: opts.language } : {}
|
|
391
|
+
}, found),
|
|
392
|
+
{
|
|
393
|
+
...opts.note ? { note: opts.note } : {},
|
|
394
|
+
budget,
|
|
395
|
+
refill: MAX_FIX_ROUNDS,
|
|
396
|
+
deps,
|
|
397
|
+
...opts.language ? { language: opts.language } : {}
|
|
398
|
+
}
|
|
399
|
+
);
|
|
400
|
+
const tools = testerTools(deps, handOff, findings);
|
|
401
|
+
const law = deps.home ? await constitutionNote({ ...deps, home: deps.home, note: opts.note }, workdir, { role: "tester", title }) : "";
|
|
402
|
+
const settled = () => {
|
|
403
|
+
const pointed = readPointer(paths.report);
|
|
404
|
+
if (pointed && existsSync(join(workdir, pointed))) return pointed;
|
|
405
|
+
return existsSync(paths.plan) ? planRel : void 0;
|
|
406
|
+
};
|
|
407
|
+
let active = settled();
|
|
408
|
+
if (!active) {
|
|
409
|
+
const docDirs = testDocDirs(await trackedFiles(workdir));
|
|
410
|
+
opts.note?.(docDirs.length ? `\u{1F9EA} Looking for an existing test document \u2014 this project keeps them in ${docDirs.map((d) => `\`${d}\``).join(", ")}.` : `\u{1F9EA} Looking for an existing test document for this work.`);
|
|
411
|
+
await runTester(deps, workdir, tools, planMessageFor(prompt, planRel, reportRel, dirRel, docDirs), opts.language, law, prompt);
|
|
412
|
+
active = settled();
|
|
413
|
+
}
|
|
414
|
+
const direct = !active;
|
|
415
|
+
if (direct) opts.note?.(`\u{1F9EA} No test document, and none needed \u2014 the request names what to check. Verifying it directly.`);
|
|
416
|
+
const inPlace = !!active && active !== planRel;
|
|
417
|
+
opts.note?.(inPlace ? `\u{1F9EA} Continuing \`${active}\` in place \u2014 it already holds results, and a second document beside it would disagree with it.` : `\u{1F9EA} Verifying against \`${active}\`.`);
|
|
418
|
+
let round = 0;
|
|
419
|
+
let message = direct ? directMessage(prompt, reportRel) : runMessage(prompt, active, reportRel, inPlace);
|
|
420
|
+
for (; ; ) {
|
|
421
|
+
await runTester(deps, workdir, tools, message, opts.language, law, prompt);
|
|
422
|
+
const found = findings.drain();
|
|
423
|
+
if (!found.length || budget.left <= 0) {
|
|
424
|
+
if (found.length) opts.note?.(`\u26A0\uFE0F ${found.length} finding(s) left unfixed \u2014 ${MAX_FIX_ROUNDS} rounds of fixing is the limit for one session.`);
|
|
425
|
+
break;
|
|
426
|
+
}
|
|
427
|
+
budget.left--;
|
|
428
|
+
round++;
|
|
429
|
+
const done = await handleFindings(opts, found);
|
|
430
|
+
message = direct ? directResumeMessage(prompt, reportRel, done) : resumeMessage(active, reportRel, inPlace, done);
|
|
431
|
+
}
|
|
432
|
+
return {
|
|
433
|
+
dir: dirRel,
|
|
434
|
+
planPath: active ?? reportRel,
|
|
435
|
+
// In-place: the results are in the document itself, and the folder holds the pointer to it.
|
|
436
|
+
reportPath: inPlace ? active : reportRel,
|
|
437
|
+
planWritten: true,
|
|
438
|
+
reportWritten: inPlace || existsSync(paths.report)
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
function describeVerify(r, branch, workdir) {
|
|
442
|
+
if (!r.planWritten) {
|
|
443
|
+
return `\u26A0\uFE0F No test plan was found or written \u2014 nothing was verified.`;
|
|
444
|
+
}
|
|
445
|
+
const head = r.reportWritten ? `\u{1F9EA} Test report: \`${r.reportPath}\`` : `\u26A0\uFE0F The plan is at \`${r.planPath}\`, but no report was written \u2014 nothing was recorded.`;
|
|
446
|
+
const inPlace = r.reportPath !== `${r.dir}/test-report.md` && r.reportWritten;
|
|
447
|
+
const where = inPlace ? `Continued in place, where its history already was. \`${r.dir}\` holds a pointer to it.` : `Everything this run produced is in \`${r.dir}\`.`;
|
|
448
|
+
const base = sessionBase(workdir !== void 0 ? resolve(workdir, r.dir) : r.dir);
|
|
449
|
+
const seat = base === void 0 ? `On branch \`${branch}\` \u2014 uncommitted, in your working tree.` : `On branch \`${branch}\`, in the worktree at \`${base}\` \u2014 review it there, then merge it in.`;
|
|
450
|
+
return `${head}
|
|
451
|
+
|
|
452
|
+
${where} ${seat}`;
|
|
453
|
+
}
|
|
454
|
+
async function currentBranchOf(cwd) {
|
|
455
|
+
const { defaultGitRunner: defaultGitRunner2 } = await import("./git-VTSZALSR.js");
|
|
456
|
+
const r = await defaultGitRunner2(["symbolic-ref", "--short", "HEAD"], cwd);
|
|
457
|
+
return r.code === 0 && r.stdout.trim() ? r.stdout.trim() : "(detached)";
|
|
458
|
+
}
|
|
459
|
+
function verifyDir(workdir, title) {
|
|
460
|
+
return relative(workdir, verifyPaths(workdir, featureSlugFor(workdir, title)).dir) || specsDir(workdir);
|
|
461
|
+
}
|
|
462
|
+
export {
|
|
463
|
+
MAX_FIX_ROUNDS,
|
|
464
|
+
POINTER_HINT,
|
|
465
|
+
POINTER_MARK,
|
|
466
|
+
VERIFY_MAX_TURNS,
|
|
467
|
+
currentBranchOf,
|
|
468
|
+
describeVerify,
|
|
469
|
+
directMessage,
|
|
470
|
+
fixBeforeHandOff,
|
|
471
|
+
planMessageFor,
|
|
472
|
+
readPointer,
|
|
473
|
+
resumeMessage,
|
|
474
|
+
runMessage,
|
|
475
|
+
runVerify,
|
|
476
|
+
testDocDirs,
|
|
477
|
+
verdictRule,
|
|
478
|
+
verifyDir
|
|
479
|
+
};
|