tewip-ai 0.24.0 → 0.25.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/README.md
CHANGED
|
@@ -444,6 +444,80 @@ Every change is small (1 to 16 lines, median 4) and most are what the developers
|
|
|
444
444
|
**No wrong fix was offered**: anything that could not be proved was thrown away and the file put
|
|
445
445
|
back byte for byte.
|
|
446
446
|
|
|
447
|
+
## One bundle a release manager can read (Phase 15)
|
|
448
|
+
|
|
449
|
+
```bash
|
|
450
|
+
tewip attest --out attestation.md
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
Everything Tewip knows about a build, in one file: which tests ran, what each failure was, which
|
|
454
|
+
rule or which model decided it, **what it refused to decide**, and what it never looked at. The
|
|
455
|
+
refusals are the point - a tool that never says "I could not tell" has nothing worth attesting,
|
|
456
|
+
because every line it writes might be a guess.
|
|
457
|
+
|
|
458
|
+
The bundle carries a digest, and an Ed25519 signature when you give it a key:
|
|
459
|
+
|
|
460
|
+
```bash
|
|
461
|
+
TEWIP_ATTEST_KEY=./private.pem tewip attest # sign it
|
|
462
|
+
tewip attest --verify attestation.json --key public.pem # check it
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
A digest is not a signature and Tewip says so: without a key it tells you the bundle is undamaged,
|
|
466
|
+
not that nobody edited it. Editing a claim out of a signed bundle makes verification fail and exit
|
|
467
|
+
non-zero. Every CI run writes one of these automatically.
|
|
468
|
+
|
|
469
|
+
## Is this incident covered by a test? (Phase 16)
|
|
470
|
+
|
|
471
|
+
```bash
|
|
472
|
+
tewip incident sentry-issue.json
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
Reads a Sentry payload, an OpenTelemetry span or a plain stack trace, finds the test that should
|
|
476
|
+
have caught it, and answers with one of four verdicts: **reproduced**, **passed** (the test ran and
|
|
477
|
+
did not catch it, which is worse news), **failed differently**, or **never ran**. It does not say
|
|
478
|
+
"probably covered".
|
|
479
|
+
|
|
480
|
+
## What did we decide about this last time? (Phase 17)
|
|
481
|
+
|
|
482
|
+
```bash
|
|
483
|
+
tewip recall "the login test that keeps flaking"
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
Every correction your team ever gave Tewip is kept. Ask, and it answers with your own decision from
|
|
487
|
+
then, and says it was yours - not its. It never acts on a correction by itself.
|
|
488
|
+
|
|
489
|
+
## From the ticket to the run (Phase 18)
|
|
490
|
+
|
|
491
|
+
```bash
|
|
492
|
+
tewip trace --only "SHOP-12"
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
Joins the test plans Tewip wrote to the tests that cover them and the runs that attempted them. A
|
|
496
|
+
scenario counts as covered only when a real test can be named, and passing only when a real run
|
|
497
|
+
attempted it; everything else says what is missing. No model, no cost.
|
|
498
|
+
|
|
499
|
+
## A goal instead of a verb (Phases 19 and 21)
|
|
500
|
+
|
|
501
|
+
```bash
|
|
502
|
+
tewip pursue "keep the checkout flow green"
|
|
503
|
+
tewip pursue "keep the checkout flow green" --act
|
|
504
|
+
```
|
|
505
|
+
|
|
506
|
+
Give Tewip a goal and it says what it would do next, and why, from what it can actually see. The
|
|
507
|
+
order is fixed, published and testable - **not chosen by a model**: a real bug first, then the
|
|
508
|
+
environment, then flakiness, then anything failing it cannot explain, then a coverage gap, and only
|
|
509
|
+
then is the goal worth attesting. Deciding the same state twice gives the same answer.
|
|
510
|
+
|
|
511
|
+
With `--act` it carries the decision out, and this is where the refusals matter most:
|
|
512
|
+
|
|
513
|
+
- It **never does the work itself.** Each action is an existing command that already proves what it
|
|
514
|
+
changes - `write` keeps a test only when it passes *and* fails when broken.
|
|
515
|
+
- **Two actions are on the allowlist**: write a missing test, and attest a green goal. Everything
|
|
516
|
+
else is refused by name. It will not work around a real bug.
|
|
517
|
+
- **One action, then stop.** There is no unattended loop.
|
|
518
|
+
- Everything it did or refused goes to `.tewip/actions.jsonl`, with the run that justified it.
|
|
519
|
+
- It **will not repeat itself**: the same attempt is refused until a new run gives it new evidence.
|
|
520
|
+
|
|
447
521
|
## Where it has been tried
|
|
448
522
|
|
|
449
523
|
| Project | How | Result |
|
package/dist/cli/help.js
CHANGED
|
@@ -105,8 +105,11 @@ Usage:
|
|
|
105
105
|
green" - and it says what it would do next and why, from what
|
|
106
106
|
it can actually see. The order is fixed and written down: a
|
|
107
107
|
real bug first, then the environment, then flakiness, then a
|
|
108
|
-
coverage gap. It decides; it does not act unless you ask
|
|
109
|
-
|
|
108
|
+
coverage gap. It decides; it does not act unless you ask. With
|
|
109
|
+
--act it carries out the two safe ones - writing a missing test,
|
|
110
|
+
or attesting a green goal - by running the command that already
|
|
111
|
+
proves them. It will not touch a real bug, and it writes what it
|
|
112
|
+
did or refused to .tewip/actions.jsonl.
|
|
110
113
|
tewip trace [--only "<ticket or title>"] [--out trace.md]
|
|
111
114
|
Is this requirement covered, and is it passing right now? Joins
|
|
112
115
|
the plans Tewip wrote to the tests that cover them and the runs
|
|
@@ -46,9 +46,18 @@ function history(root, only) {
|
|
|
46
46
|
function whatWasChecked(records) {
|
|
47
47
|
const checked = [];
|
|
48
48
|
const notChecked = [];
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
const
|
|
49
|
+
// Decided means settled: a rule that looked and could not say is a refusal, and counting it here
|
|
50
|
+
// would have a bundle claim more decisions than it lists claims.
|
|
51
|
+
const settledBy = (source) => records.filter((r) => {
|
|
52
|
+
const d = r.diagnosis;
|
|
53
|
+
return d?.source === source && d.category && d.category !== 'UNKNOWN';
|
|
54
|
+
}).length;
|
|
55
|
+
if (records.length === 0)
|
|
56
|
+
checked.push('every test in the run passed, so there was nothing to diagnose');
|
|
57
|
+
else
|
|
58
|
+
checked.push('every failing test in the run was read, and each was given a cause or left undecided');
|
|
59
|
+
const byRules = settledBy('rules');
|
|
60
|
+
const byModel = settledBy('llm');
|
|
52
61
|
if (byRules > 0)
|
|
53
62
|
checked.push(`${byRules} decided by deterministic rules (${ENGINE_VERSION}), which call no model and give the same answer every time`);
|
|
54
63
|
if (byModel > 0)
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What Tewip may carry out, and with what. Anything absent is refused: reporting a real bug, for
|
|
3
|
+
* instance, is deliberately not an action - it is for a person to read.
|
|
4
|
+
*/
|
|
5
|
+
export function planFor(decision) {
|
|
6
|
+
switch (decision.act) {
|
|
7
|
+
case 'cover-gap': {
|
|
8
|
+
// `write` drafts the test, runs it, breaks it on purpose to see it fail, and keeps it only
|
|
9
|
+
// if both hold. The scenario id is in the decision's own next step.
|
|
10
|
+
const id = /write a test for ([\w-]+)/.exec(decision.next)?.[1];
|
|
11
|
+
return id ? { command: ['write', '--only', id], target: `scenario ${id}` } : null;
|
|
12
|
+
}
|
|
13
|
+
case 'attest':
|
|
14
|
+
return { command: ['attest'], target: 'the evidence bundle' };
|
|
15
|
+
default:
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
/** Why an action was not carried out, in words a person can act on. */
|
|
20
|
+
export const cannotAct = {
|
|
21
|
+
'look-first': 'there is nothing to act on until a report has been read',
|
|
22
|
+
'triage-real-bug': 'a real bug is for a person to see, and Tewip will not work around one on its own',
|
|
23
|
+
'rerun-environment': 'a re-run belongs to whatever runs the tests, not to Tewip',
|
|
24
|
+
'settle-flaky': 'settling a flaky test changes a test file, which Tewip does only when asked directly with `tewip fix`',
|
|
25
|
+
'triage-undecided': 'Tewip cannot say why these failed, so it has nothing it could safely do about them',
|
|
26
|
+
'cover-gap': '',
|
|
27
|
+
attest: '',
|
|
28
|
+
'stand-down': 'there is nothing here that matches the goal',
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Has this exact action already been tried against this exact evidence? A scheduled run that keeps
|
|
32
|
+
* attempting an impossible task is the failure mode that makes people turn an agent off. A new run
|
|
33
|
+
* is new evidence and unblocks it; running again over the same report does not.
|
|
34
|
+
*
|
|
35
|
+
* Refusals are not counted: refusing costs nothing and says nothing about whether it would work.
|
|
36
|
+
*/
|
|
37
|
+
export function alreadyTried(records, act, target, runId) {
|
|
38
|
+
return records.find((r) => r.act === act && r.target === target && r.runId === runId && r.outcome !== 'refused') ?? null;
|
|
39
|
+
}
|
|
@@ -4,9 +4,11 @@
|
|
|
4
4
|
// it chose, and only through the gates that already prove a change before keeping it. There is no
|
|
5
5
|
// mode in which it loops unattended: an agent that keeps going until it decides to stop is the
|
|
6
6
|
// thing teams roll back, and nothing here needs it.
|
|
7
|
-
import {
|
|
7
|
+
import { spawnSync } from 'node:child_process';
|
|
8
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
|
|
8
9
|
import { dirname, join, resolve } from 'node:path';
|
|
9
10
|
import { datasetFiles, readJsonl, readTriageRecords } from '../../reporters/store.js';
|
|
11
|
+
import { alreadyTried, cannotAct, planFor } from './act.js';
|
|
10
12
|
import { decide } from './policy.js';
|
|
11
13
|
const words = (text) => (text.toLowerCase().match(/[a-z][a-z0-9]{2,}/g) ?? []).filter((w) => !['the', 'and', 'for', 'this', 'that', 'with', 'keep', 'green', 'sprint', 'flow', 'test', 'tests'].includes(w));
|
|
12
14
|
/** Does this test or scenario belong to the goal? Deterministic, so a person can predict it. */
|
|
@@ -53,6 +55,7 @@ export function observe(root, goal, only) {
|
|
|
53
55
|
uncovered: uncoveredScenarios(root, wanted, attempted),
|
|
54
56
|
passing: attempted.length - failing.length,
|
|
55
57
|
noHistory: runs.length === 0,
|
|
58
|
+
runId: newest?.runId ?? '',
|
|
56
59
|
};
|
|
57
60
|
}
|
|
58
61
|
/** Scenarios of a kept plan that no attempted test can be matched to, worst priority first. */
|
|
@@ -108,15 +111,100 @@ export function renderPursuit(goal, seen, decision, acting) {
|
|
|
108
111
|
: 'Tewip has not done anything: this is a decision, and the action is yours.', '', 'The order of these decisions is fixed and written down, not chosen by a model: a real bug first,', 'then the environment, then flakiness, then a coverage gap, and only then is a goal worth attesting.');
|
|
109
112
|
return lines.join('\n');
|
|
110
113
|
}
|
|
114
|
+
/** The audit file the team owns: every action Tewip took on its own, and every one it refused. */
|
|
115
|
+
const auditPath = (root) => join(root, '.tewip', 'actions.jsonl');
|
|
116
|
+
export function actionsTaken(root) {
|
|
117
|
+
try {
|
|
118
|
+
return readFileSync(auditPath(root), 'utf8')
|
|
119
|
+
.split(String.fromCharCode(10))
|
|
120
|
+
.filter(Boolean)
|
|
121
|
+
.map((l) => JSON.parse(l));
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
return [];
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
function record(root, entry) {
|
|
128
|
+
mkdirSync(dirname(auditPath(root)), { recursive: true });
|
|
129
|
+
appendFileSync(auditPath(root), JSON.stringify(entry) + String.fromCharCode(10), 'utf8');
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* The Tewip command line belonging to *this* build, found from this file rather than from how the
|
|
133
|
+
* process was started.
|
|
134
|
+
*
|
|
135
|
+
* This is not a detail. The first version used process.argv[1], which is the command line only when
|
|
136
|
+
* a person typed it: under a test runner argv[1] is the test file, so acting re-ran the test file,
|
|
137
|
+
* which acted again, which re-ran it - a fork bomb that froze the machine it was written on. What
|
|
138
|
+
* re-enters Tewip has to be Tewip, established here, or nothing is carried out at all.
|
|
139
|
+
*/
|
|
140
|
+
export function cliEntry() {
|
|
141
|
+
const here = import.meta.dirname;
|
|
142
|
+
for (const candidate of [join(here, '..', '..', 'cli', 'tewip.ts'), join(here, '..', '..', 'cli', 'tewip.js')]) {
|
|
143
|
+
if (existsSync(candidate))
|
|
144
|
+
return candidate;
|
|
145
|
+
}
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Carries out the one action that was decided, by running the command that already proves it.
|
|
150
|
+
* Tewip does not do the work here; it asks the gate to.
|
|
151
|
+
*/
|
|
152
|
+
function carryOut(cmd, decision, observed) {
|
|
153
|
+
const plan = planFor(decision);
|
|
154
|
+
const runId = observed.runId ?? '';
|
|
155
|
+
const note = (outcome, detail, target = plan?.target ?? '') => ({
|
|
156
|
+
at: new Date().toISOString(),
|
|
157
|
+
goal: cmd.goal,
|
|
158
|
+
act: decision.act,
|
|
159
|
+
target,
|
|
160
|
+
command: plan ? 'tewip ' + plan.command.join(' ') : '',
|
|
161
|
+
runId,
|
|
162
|
+
outcome,
|
|
163
|
+
detail,
|
|
164
|
+
});
|
|
165
|
+
if (!plan) {
|
|
166
|
+
const why = cannotAct[decision.act] || 'this is not something Tewip carries out on its own';
|
|
167
|
+
record(cmd.root, note('refused', why, decision.act));
|
|
168
|
+
return 'Not carried out: ' + why + '.';
|
|
169
|
+
}
|
|
170
|
+
const tried = alreadyTried(actionsTaken(cmd.root), decision.act, plan.target, runId);
|
|
171
|
+
if (tried) {
|
|
172
|
+
record(cmd.root, note('refused', 'already attempted at ' + tried.at + ', on the same evidence'));
|
|
173
|
+
return ['Not carried out: Tewip already attempted this at ' + tried.at.slice(0, 19).replace('T', ' ') + ' UTC, and nothing has happened since that would make the outcome different.', '', 'That attempt ' + (tried.outcome === 'done' ? 'succeeded' : 'did not finish') + ': ' + tried.detail, '', 'Run ' + tried.command + ' yourself if you want it tried again.'].join(String.fromCharCode(10));
|
|
174
|
+
}
|
|
175
|
+
// Re-enter this same build, so an installed Tewip runs the installed gates and a checkout runs
|
|
176
|
+
// the checkout's - never a different version from the one that decided, and never whatever
|
|
177
|
+
// happens to have started this process.
|
|
178
|
+
const entry = cliEntry();
|
|
179
|
+
if (!entry) {
|
|
180
|
+
record(cmd.root, note('refused', 'the Tewip command line could not be found next to this build, so there is nothing safe to re-enter'));
|
|
181
|
+
return 'Not carried out: Tewip could not find its own command line beside this build, and it will not guess at one. Run ' + 'tewip ' + plan.command.join(' ') + ' yourself.';
|
|
182
|
+
}
|
|
183
|
+
const ran = spawnSync(process.execPath, [entry, ...plan.command], { cwd: cmd.root, encoding: 'utf8', timeout: 20 * 60_000, maxBuffer: 32 * 1024 * 1024 });
|
|
184
|
+
const output = ((ran.stdout ?? '') + (ran.stderr ?? '')).trim();
|
|
185
|
+
const ok = ran.status === 0;
|
|
186
|
+
// What the audit file remembers: the headline of a success, the tail of a failure. A failure's
|
|
187
|
+
// reason is at the end of what it printed; a success's is at the start.
|
|
188
|
+
const lines = output.split(String.fromCharCode(10)).map((l) => l.trim()).filter(Boolean);
|
|
189
|
+
const detail = (ok ? lines[0] ?? 'it printed nothing' : lines.slice(-3).join(' ')).slice(0, 300);
|
|
190
|
+
record(cmd.root, note(ok ? 'done' : 'failed', detail));
|
|
191
|
+
return [
|
|
192
|
+
ok ? 'Carried out: tewip ' + plan.command.join(' ') : 'Tried and did not finish: tewip ' + plan.command.join(' '),
|
|
193
|
+
'',
|
|
194
|
+
output.slice(-2000),
|
|
195
|
+
'',
|
|
196
|
+
ok
|
|
197
|
+
? 'Anything kept by that command was proved by it; anything unproved was thrown away. Written to .tewip/actions.jsonl.'
|
|
198
|
+
: 'Nothing was kept. Written to .tewip/actions.jsonl.',
|
|
199
|
+
].join(String.fromCharCode(10));
|
|
200
|
+
}
|
|
111
201
|
export function runPursue(cmd) {
|
|
112
202
|
const observed = observe(cmd.root, cmd.goal, cmd.name);
|
|
113
203
|
const decision = decide(cmd.goal, observed);
|
|
114
204
|
const report = renderPursuit(cmd.goal, observed, decision, Boolean(cmd.act));
|
|
115
205
|
console.log(report);
|
|
116
|
-
if (cmd.act
|
|
117
|
-
console.
|
|
118
|
-
console.error(`Run: ${decision.next}`);
|
|
119
|
-
}
|
|
206
|
+
if (cmd.act)
|
|
207
|
+
console.log(String.fromCharCode(10) + '---' + String.fromCharCode(10) + carryOut(cmd, decision, observed));
|
|
120
208
|
if (cmd.out) {
|
|
121
209
|
mkdirSync(dirname(resolve(cmd.out)), { recursive: true });
|
|
122
210
|
writeFileSync(resolve(cmd.out), `${report}\n`, 'utf8');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tewip-ai",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.25.0",
|
|
4
4
|
"description": "Tewip finds why each failing test failed - real bug, flaky test, environment, test data or changed selector - for any stack, from the command line, CI, or your editor.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"test-failures",
|