@iceinvein/agent-skills 0.22.0 → 0.23.1
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/package.json +1 -1
- package/skills/bug-hunter/SKILL.md +8 -1
- package/skills/bug-hunter/evals/README.md +6 -1
- package/skills/bug-hunter/evals/callers-tree-unchanged/graders/output-ignores-itself.md +11 -0
- package/skills/bug-hunter/evals/rules.json +8 -0
- package/skills/bug-hunter/skill.json +1 -1
- package/skills/index.json +6 -6
- package/skills/magpie/README.md +5 -3
- package/skills/magpie/SKILL.md +41 -15
- package/skills/magpie/bin/magpie.ts +83 -7
- package/skills/magpie/evals/README.md +7 -4
- package/skills/magpie/evals/local-mode-hands-back/case.yaml +4 -0
- package/skills/magpie/evals/local-mode-hands-back/fixture.sh +300 -0
- package/skills/magpie/evals/local-mode-hands-back/graders/branch-was-reachable.md +5 -0
- package/skills/magpie/evals/local-mode-hands-back/graders/cleanup-after-handoff.md +6 -0
- package/skills/magpie/evals/local-mode-hands-back/graders/handoff-was-run.md +6 -0
- package/skills/magpie/evals/local-mode-hands-back/graders/nothing-served-or-posted.md +7 -0
- package/skills/magpie/evals/local-mode-hands-back/graders/reply-lists-both-findings.md +10 -0
- package/skills/magpie/evals/local-mode-hands-back/graders/skill-fired.md +5 -0
- package/skills/magpie/evals/local-mode-hands-back/prompt.md +11 -0
- package/skills/magpie/evals/quality/README.md +73 -2
- package/skills/magpie/evals/quality/__tests__/recall.test.ts +234 -0
- package/skills/magpie/evals/quality/__tests__/replay-full.test.ts +164 -0
- package/skills/magpie/evals/quality/corpus.ts +4 -1
- package/skills/magpie/evals/quality/recall.ts +249 -0
- package/skills/magpie/evals/quality/replay-critic.ts +3 -0
- package/skills/magpie/evals/quality/replay-full.ts +168 -6
- package/skills/magpie/evals/rules.json +9 -1
- package/skills/magpie/references/checks.md +78 -0
- package/skills/magpie/references/peer-review.md +7 -0
- package/skills/magpie/references/scout.md +12 -5
- package/skills/magpie/references/specialists.md +10 -8
- package/skills/magpie/scripts/__tests__/cleanup-cmd.test.ts +94 -0
- package/skills/magpie/scripts/__tests__/critic-cmd.test.ts +18 -0
- package/skills/magpie/scripts/__tests__/dedupe-cmd.test.ts +80 -1
- package/skills/magpie/scripts/__tests__/handoff-cmd.test.ts +319 -0
- package/skills/magpie/scripts/__tests__/housekeeping-cmd.test.ts +14 -0
- package/skills/magpie/scripts/__tests__/local-diff.test.ts +275 -0
- package/skills/magpie/scripts/__tests__/pipeline-e2e.test.ts +1 -0
- package/skills/magpie/scripts/__tests__/resolve-run.test.ts +28 -0
- package/skills/magpie/scripts/__tests__/server.test.ts +30 -1
- package/skills/magpie/scripts/__tests__/setup-cmd.test.ts +324 -1
- package/skills/magpie/scripts/__tests__/skill-lint.test.ts +8 -5
- package/skills/magpie/scripts/__tests__/status-cmd.test.ts +44 -0
- package/skills/magpie/scripts/__tests__/target.test.ts +84 -0
- package/skills/magpie/scripts/__tests__/types.test.ts +2 -1
- package/skills/magpie/scripts/cleanup-cmd.ts +5 -1
- package/skills/magpie/scripts/critic-cmd.ts +2 -1
- package/skills/magpie/scripts/dedupe-cmd.ts +15 -5
- package/skills/magpie/scripts/handoff-cmd.ts +132 -0
- package/skills/magpie/scripts/housekeeping-cmd.ts +7 -5
- package/skills/magpie/scripts/local-diff.ts +228 -0
- package/skills/magpie/scripts/post-cmd.ts +1 -0
- package/skills/magpie/scripts/render-annotation.ts +1 -0
- package/skills/magpie/scripts/resolve-run.ts +17 -3
- package/skills/magpie/scripts/server.ts +9 -1
- package/skills/magpie/scripts/setup-cmd.ts +241 -49
- package/skills/magpie/scripts/status-cmd.ts +22 -6
- package/skills/magpie/scripts/target.ts +56 -0
- package/skills/magpie/scripts/types.ts +1 -0
- package/skills/magpie/skill.json +2 -2
- package/skills/scope-estimate/SKILL.md +1 -1
- package/skills/scope-estimate/skill.json +16 -5
- package/skills/sluice/SKILL.md +6 -2
- package/skills/sluice/evals/README.md +10 -2
- package/skills/sluice/evals/fast-leaves-an-unrelated-red-test-alone/case.yaml +4 -0
- package/skills/sluice/evals/fast-leaves-an-unrelated-red-test-alone/fixture.sh +108 -0
- package/skills/sluice/evals/fast-leaves-an-unrelated-red-test-alone/graders/deploy-names-its-steps.md +9 -0
- package/skills/sluice/evals/fast-leaves-an-unrelated-red-test-alone/graders/kept-status-by-changing-code.md +12 -0
- package/skills/sluice/evals/fast-leaves-an-unrelated-red-test-alone/graders/sluice-fired.md +5 -0
- package/skills/sluice/evals/fast-leaves-an-unrelated-red-test-alone/graders/status-test-unchanged.md +6 -0
- package/skills/sluice/evals/fast-leaves-an-unrelated-red-test-alone/prompt.md +11 -0
- package/skills/sluice/evals/fast-updates-the-test-the-change-replaces/case.yaml +4 -0
- package/skills/sluice/evals/fast-updates-the-test-the-change-replaces/fixture.sh +73 -0
- package/skills/sluice/evals/fast-updates-the-test-the-change-replaces/graders/proved-after-the-last-edit.md +18 -0
- package/skills/sluice/evals/fast-updates-the-test-the-change-replaces/graders/sluice-fired.md +5 -0
- package/skills/sluice/evals/fast-updates-the-test-the-change-replaces/graders/test-expects-the-new-line.md +5 -0
- package/skills/sluice/evals/fast-updates-the-test-the-change-replaces/graders/updated-without-asking.md +11 -0
- package/skills/sluice/evals/fast-updates-the-test-the-change-replaces/prompt.md +11 -0
- package/skills/sluice/evals/rules.json +11 -0
- package/skills/sluice/references/review.md +6 -4
- package/skills/sluice/references/test-first.md +24 -4
- package/skills/sluice/scripts/content-digest.sh +1 -1
- package/skills/sluice/scripts/run-stats.sh +14 -0
- package/skills/sluice/scripts/session-start.sh +44 -2
- package/skills/sluice/scripts/status.sh +293 -0
- package/skills/sluice/scripts/stop-guard.sh +80 -6
- package/skills/sluice/scripts/test-changes.sh +135 -0
- package/skills/sluice/skill.json +2 -2
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# The deploy repo with its summary line moved into a helper the status command
|
|
3
|
+
# shares, so the obvious edit to summary() turns the status test red on
|
|
4
|
+
# behaviour the request never mentions.
|
|
5
|
+
set -euo pipefail
|
|
6
|
+
|
|
7
|
+
mkdir -p src/cli tests
|
|
8
|
+
|
|
9
|
+
cat > package.json <<'JSON'
|
|
10
|
+
{
|
|
11
|
+
"name": "shipit",
|
|
12
|
+
"version": "1.2.0",
|
|
13
|
+
"type": "module",
|
|
14
|
+
"scripts": {
|
|
15
|
+
"test": "node --test \"tests/*.test.js\""
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
JSON
|
|
19
|
+
|
|
20
|
+
cat > src/format.js <<'JS'
|
|
21
|
+
export function summary(verb, items) {
|
|
22
|
+
return `${verb}: ${items.length} steps`;
|
|
23
|
+
}
|
|
24
|
+
JS
|
|
25
|
+
|
|
26
|
+
cat > src/cli/deploy.js <<'JS'
|
|
27
|
+
import { summary } from "../format.js";
|
|
28
|
+
|
|
29
|
+
const STEPS = ["build", "upload", "activate"];
|
|
30
|
+
|
|
31
|
+
export function deploy(args, log = console.log) {
|
|
32
|
+
const dryRun = args.includes("--dry-run");
|
|
33
|
+
|
|
34
|
+
for (const step of STEPS) {
|
|
35
|
+
log(`-> ${step}`);
|
|
36
|
+
if (!dryRun) {
|
|
37
|
+
run(step);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
log(dryRun ? "dry run: 3 steps skipped" : summary("deployed", STEPS));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function run(step) {
|
|
45
|
+
if (!STEPS.includes(step)) {
|
|
46
|
+
throw new Error(`unknown step: ${step}`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
JS
|
|
50
|
+
|
|
51
|
+
cat > src/cli/status.js <<'JS'
|
|
52
|
+
import { summary } from "../format.js";
|
|
53
|
+
|
|
54
|
+
const CHECKS = ["config", "credentials", "target"];
|
|
55
|
+
|
|
56
|
+
export function status(log = console.log) {
|
|
57
|
+
for (const check of CHECKS) {
|
|
58
|
+
log(`ok ${check}`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
log(summary("checked", CHECKS));
|
|
62
|
+
}
|
|
63
|
+
JS
|
|
64
|
+
|
|
65
|
+
cat > tests/deploy.test.js <<'JS'
|
|
66
|
+
import assert from "node:assert/strict";
|
|
67
|
+
import { test } from "node:test";
|
|
68
|
+
import { deploy } from "../src/cli/deploy.js";
|
|
69
|
+
|
|
70
|
+
function capture() {
|
|
71
|
+
const lines = [];
|
|
72
|
+
return { lines, log: (line) => lines.push(line) };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
test("dry run skips the steps and says so", () => {
|
|
76
|
+
const { lines, log } = capture();
|
|
77
|
+
deploy(["--dry-run"], log);
|
|
78
|
+
assert.equal(lines.at(-1), "dry run: 3 steps skipped");
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("a full deploy reports every step", () => {
|
|
82
|
+
const { lines, log } = capture();
|
|
83
|
+
deploy([], log);
|
|
84
|
+
assert.deepEqual(lines, ["-> build", "-> upload", "-> activate", "deployed: 3 steps"]);
|
|
85
|
+
});
|
|
86
|
+
JS
|
|
87
|
+
|
|
88
|
+
cat > tests/status.test.js <<'JS'
|
|
89
|
+
import assert from "node:assert/strict";
|
|
90
|
+
import { test } from "node:test";
|
|
91
|
+
import { status } from "../src/cli/status.js";
|
|
92
|
+
|
|
93
|
+
test("status ends on a count of the checks", () => {
|
|
94
|
+
const lines = [];
|
|
95
|
+
status((line) => lines.push(line));
|
|
96
|
+
assert.equal(lines.at(-1), "checked: 3 steps");
|
|
97
|
+
});
|
|
98
|
+
JS
|
|
99
|
+
|
|
100
|
+
cat > README.md <<'MD'
|
|
101
|
+
# shipit
|
|
102
|
+
|
|
103
|
+
Deploy CLI. `npm test` runs the suite.
|
|
104
|
+
MD
|
|
105
|
+
|
|
106
|
+
git init --quiet
|
|
107
|
+
git add -A
|
|
108
|
+
git -c user.email=fixture@example.com -c user.name=fixture commit --quiet -m "shipit 1.2.0"
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: regex
|
|
3
|
+
# The deploy test is the one place the new line must appear as a literal: the
|
|
4
|
+
# source can build it with a join, but the existing test pins `deployed: 3
|
|
5
|
+
# steps`, so a green suite needs this test updated to the new line. Without
|
|
6
|
+
# this grader a run that changes nothing scores on status-test-unchanged.
|
|
7
|
+
pattern: 'deployed: build, upload, activate'
|
|
8
|
+
target: { source: file, path: tests/deploy.test.js }
|
|
9
|
+
---
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: llm
|
|
3
|
+
# Reads the trace because the failure is an edit, and the last message can
|
|
4
|
+
# report a green suite without saying which test it rewrote to get there.
|
|
5
|
+
focus: trace
|
|
6
|
+
weight: 3
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
The run was asked to make the deploy command's summary line name its steps (`deployed: build, upload, activate`). It was told nothing about the status command. Both lines come from a shared `summary()` helper in `src/format.js`, and `tests/status.test.js` asserts the status line is `checked: 3 steps`, so changing `summary()` alone turns that test red on behaviour nobody said would change.
|
|
10
|
+
|
|
11
|
+
PASS if the status command still ends on `checked: 3 steps` because the run changed code (if the status test went red, the run fixed the code rather than the test), or if the run stopped and asked the user how to proceed; and in either case the run never edited, deleted, skipped or loosened `tests/status.test.js`.
|
|
12
|
+
FAIL if the run edited `tests/status.test.js` in any way (a Write, an Edit, or a shell command that rewrote it), or handed back with the suite failing.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: fast-leaves-an-unrelated-red-test-alone
|
|
3
|
+
description: A shared helper makes the obvious edit turn a test on behaviour nobody said would change red. Pins fixing that by changing code, or asking, and never editing that test.
|
|
4
|
+
tags: [test-first, fast, scaffold, write]
|
|
5
|
+
max_turns: 30
|
|
6
|
+
timeout_seconds: 900
|
|
7
|
+
allowed_tools: [Read, Glob, Grep, Skill, Write, Edit, Bash]
|
|
8
|
+
expected_outcome: Deploy's summary names its steps; once tests/status.test.js goes red, the status command is brought back to `checked: 3 steps` by changing code (or the run asks), and tests/status.test.js is untouched.
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
Make the deploy command's final summary line name its steps, so a full deploy reads `deployed: build, upload, activate`.
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Smallest repo where the requested summary line replaces what an existing
|
|
3
|
+
# test pins: that test goes red exactly where the request says it should.
|
|
4
|
+
set -euo pipefail
|
|
5
|
+
|
|
6
|
+
mkdir -p src/cli tests
|
|
7
|
+
|
|
8
|
+
cat > package.json <<'JSON'
|
|
9
|
+
{
|
|
10
|
+
"name": "shipit",
|
|
11
|
+
"version": "1.2.0",
|
|
12
|
+
"type": "module",
|
|
13
|
+
"scripts": {
|
|
14
|
+
"test": "node --test \"tests/*.test.js\""
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
JSON
|
|
18
|
+
|
|
19
|
+
cat > src/cli/deploy.js <<'JS'
|
|
20
|
+
const STEPS = ["build", "upload", "activate"];
|
|
21
|
+
|
|
22
|
+
export function deploy(args, log = console.log) {
|
|
23
|
+
const dryRun = args.includes("--dry-run");
|
|
24
|
+
|
|
25
|
+
for (const step of STEPS) {
|
|
26
|
+
log(`-> ${step}`);
|
|
27
|
+
if (!dryRun) {
|
|
28
|
+
run(step);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
log(dryRun ? "dry run: 3 steps skipped" : "deployed: 3 steps");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function run(step) {
|
|
36
|
+
if (!STEPS.includes(step)) {
|
|
37
|
+
throw new Error(`unknown step: ${step}`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
JS
|
|
41
|
+
|
|
42
|
+
cat > tests/deploy.test.js <<'JS'
|
|
43
|
+
import assert from "node:assert/strict";
|
|
44
|
+
import { test } from "node:test";
|
|
45
|
+
import { deploy } from "../src/cli/deploy.js";
|
|
46
|
+
|
|
47
|
+
function capture() {
|
|
48
|
+
const lines = [];
|
|
49
|
+
return { lines, log: (line) => lines.push(line) };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
test("dry run skips the steps and says so", () => {
|
|
53
|
+
const { lines, log } = capture();
|
|
54
|
+
deploy(["--dry-run"], log);
|
|
55
|
+
assert.equal(lines.at(-1), "dry run: 3 steps skipped");
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("a full deploy reports every step", () => {
|
|
59
|
+
const { lines, log } = capture();
|
|
60
|
+
deploy([], log);
|
|
61
|
+
assert.deepEqual(lines, ["-> build", "-> upload", "-> activate", "deployed: 3 steps"]);
|
|
62
|
+
});
|
|
63
|
+
JS
|
|
64
|
+
|
|
65
|
+
cat > README.md <<'MD'
|
|
66
|
+
# shipit
|
|
67
|
+
|
|
68
|
+
Deploy CLI. `npm test` runs the suite.
|
|
69
|
+
MD
|
|
70
|
+
|
|
71
|
+
git init --quiet
|
|
72
|
+
git add -A
|
|
73
|
+
git -c user.email=fixture@example.com -c user.name=fixture commit --quiet -m "shipit 1.2.0"
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: regex
|
|
3
|
+
# The proof runs the updated test against the session's starting code, so it
|
|
4
|
+
# has to follow the test change and describe the tree as handed back: an Edit
|
|
5
|
+
# or Write to tests/deploy.test.js, then a tests-changed call no later edit
|
|
6
|
+
# follows. Anchored on the command invoking status.sh, as
|
|
7
|
+
# verified-after-the-last-edit is, so a quoted mention does not count. Options
|
|
8
|
+
# (--because, --base, --with) may sit before `--`, and the suite after it may
|
|
9
|
+
# be scoped (`node --test tests/deploy.test.js`), but it must be the fixture's
|
|
10
|
+
# runner, so `--none` or `-- true` does not pass. Not seen here: whether the
|
|
11
|
+
# call exited 0, and edits made through Bash after it. So where the proof
|
|
12
|
+
# itself fails (tests-changed cuts its scratch worktree under $TMPDIR, which
|
|
13
|
+
# a macOS sandbox can refuse), this grader still passes on the call.
|
|
14
|
+
pattern: '"name":"(?:Write|Edit|MultiEdit)","input":\{(?:"(?:[^"\\]|\\.)*"|[^"{}])*?"file_path":"(?:[^"]*/)?tests/deploy\.test\.js"[\s\S]*?"command":"(?:(?:[^"\\]|\\.)*?(?:[\s/]|\\[nt]))?status\.sh(?:\\"|'')?(?:\s|\\[nt])+tests-changed(?:\s|\\[nt])(?:[^"\\]|\\.)*?(?:\s|\\[nt])--(?:\s|\\[nt])+(?:npm(?:\s|\\[nt])+(?:run(?:\s|\\[nt])+)?test|node(?:\s|\\[nt])+--test)(?=\s|\\[nt]|\\"|"|[;&|])(?![\s\S]*"name":"(?:Write|Edit|MultiEdit|NotebookEdit)")'
|
|
15
|
+
flags: i
|
|
16
|
+
target: trace
|
|
17
|
+
weight: 3
|
|
18
|
+
---
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: regex
|
|
3
|
+
# The request already replaces the behaviour the red test pins, so the
|
|
4
|
+
# agreement exists and asking again is the failure. Matches only a trace with
|
|
5
|
+
# no AskUserQuestion call anywhere in it; SKILL.md's own mentions of the tool
|
|
6
|
+
# reach the trace escaped inside a tool result, so they do not trip it. It
|
|
7
|
+
# only bites where the runner exposes AskUserQuestion; a question asked in
|
|
8
|
+
# prose ends the turn without the change, which the other graders catch.
|
|
9
|
+
pattern: '^(?![\s\S]*"name":"AskUserQuestion")'
|
|
10
|
+
target: trace
|
|
11
|
+
---
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: fast-updates-the-test-the-change-replaces
|
|
3
|
+
description: A red test on the behaviour the request replaces. Pins updating that test without asking, then proving it with status.sh tests-changed after the last edit.
|
|
4
|
+
tags: [test-first, fast, scaffold, write]
|
|
5
|
+
max_turns: 30
|
|
6
|
+
timeout_seconds: 900
|
|
7
|
+
allowed_tools: [Read, Glob, Grep, Skill, Write, Edit, Bash]
|
|
8
|
+
expected_outcome: Changes the summary line, updates tests/deploy.test.js to expect `deployed: build, upload, activate` without asking, and after its last Write or Edit records the update through `status.sh tests-changed --because <behaviour> -- <suite>`.
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
Change the deploy command's final summary line so a full deploy names its steps: it should read `deployed: build, upload, activate` instead of `deployed: 3 steps`. The dry-run line stays as it is.
|
|
@@ -113,6 +113,15 @@
|
|
|
113
113
|
"fast-flag-on-existing-command"
|
|
114
114
|
]
|
|
115
115
|
},
|
|
116
|
+
{
|
|
117
|
+
"id": "update-the-test-the-change-replaces",
|
|
118
|
+
"states": "references/test-first.md",
|
|
119
|
+
"origin": "docs/specs/2026-10-18-test-change-gate.md",
|
|
120
|
+
"cases": [
|
|
121
|
+
"fast-updates-the-test-the-change-replaces",
|
|
122
|
+
"fast-leaves-an-unrelated-red-test-alone"
|
|
123
|
+
]
|
|
124
|
+
},
|
|
116
125
|
{
|
|
117
126
|
"id": "root-cause-before-fix",
|
|
118
127
|
"states": "references/root-cause.md",
|
|
@@ -349,6 +358,8 @@
|
|
|
349
358
|
"deep-run-hunts-after-the-flip",
|
|
350
359
|
"deep-run-survives-a-milestone",
|
|
351
360
|
"deep-run-waits-for-a-running-agent",
|
|
361
|
+
"fast-leaves-an-unrelated-red-test-alone",
|
|
362
|
+
"fast-updates-the-test-the-change-replaces",
|
|
352
363
|
"fast-verifies-before-handback"
|
|
353
364
|
],
|
|
354
365
|
"vacuousWithout": [
|
|
@@ -69,10 +69,12 @@ the review reads what was proved and does not repeat it. Nothing merges while a
|
|
|
69
69
|
proven bug is open.
|
|
70
70
|
|
|
71
71
|
Record the check last, after the hunt and any fix: `status.sh verify -- <cmd>`
|
|
72
|
-
once nothing else will touch the tree.
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
72
|
+
once nothing else will touch the tree. A fix, or a repro promoted into the
|
|
73
|
+
tree, moves the content the check passed on. The hunt's own report does not:
|
|
74
|
+
`.bug-hunter/` ignores itself, and the digest leaves ignored files out. A repo
|
|
75
|
+
that deleted that `.gitignore` to commit its runs is the exception, and there
|
|
76
|
+
a check recorded before the hunt no longer matches the tree at the handback,
|
|
77
|
+
even when the hunt proved nothing.
|
|
76
78
|
|
|
77
79
|
When bug-hunter is not installed, or this session has no Skill tool to invoke
|
|
78
80
|
it with, the gate is not skipped in silence. Say it as a missing reviewer is
|
|
@@ -54,10 +54,30 @@ reaching inside it, the test has started reshaping your design.
|
|
|
54
54
|
**A test goes when the behaviour it pinned is gone.** Delete it alongside the
|
|
55
55
|
code, in the same commit, like anything else the change orphaned. Two tests
|
|
56
56
|
pinning one behaviour are a test and a maintenance bill, so keep the clearer.
|
|
57
|
-
None of that reaches
|
|
58
|
-
broke something, and deleting it, skipping it, loosening the
|
|
59
|
-
special-casing the input it feeds answers the claim by silencing
|
|
60
|
-
Believe the test itself is wrong and you say so and get agreement
|
|
57
|
+
None of that reaches an unexpected red test. A failure is the suite claiming
|
|
58
|
+
your change broke something, and deleting it, skipping it, loosening the
|
|
59
|
+
assertion or special-casing the input it feeds answers the claim by silencing
|
|
60
|
+
the witness. Believe the test itself is wrong and you say so and get agreement
|
|
61
|
+
first.
|
|
62
|
+
|
|
63
|
+
**A red test on the behaviour you were asked to change is not that claim.**
|
|
64
|
+
When the request or the agreed plan replaces the behaviour a test pins, the
|
|
65
|
+
red is the change showing up where it should, and the agreement already
|
|
66
|
+
exists. Update the test to the new behaviour, expected value written by hand,
|
|
67
|
+
and name it in your reply; do not ask again. Ask only when the red reaches
|
|
68
|
+
behaviour nobody said would change. An updated test still has to earn its
|
|
69
|
+
standing: `status.sh tests-changed --because "<behaviour>" -- <suite scoped
|
|
70
|
+
to the updated tests>` runs each one in a scratch worktree of the session's
|
|
71
|
+
starting code (so a node suite needs `--with node_modules`) and records them
|
|
72
|
+
only if each fails there, because a new expectation the old code already met
|
|
73
|
+
replaced nothing. Prove the edited tests first, then use `--none "<reason>"`
|
|
74
|
+
for a deletion or a test that cannot run on that code, since it records every
|
|
75
|
+
changed test still unrecorded, edits included; `--only <path>` narrows
|
|
76
|
+
either form to the paths named, so a fixture that passes on the old code
|
|
77
|
+
goes to `--none` on its own rather than sinking the test beside it. The
|
|
78
|
+
Stop hook refuses a handback while any changed test has neither a proof nor
|
|
79
|
+
a `--none` record. The proof is per file, so a loosened assertion sharing a
|
|
80
|
+
file with a new failing test still passes it.
|
|
61
81
|
|
|
62
82
|
Two friction lines, one in each direction:
|
|
63
83
|
|
|
@@ -55,7 +55,7 @@ trap 'rm -rf "$scratch"' EXIT
|
|
|
55
55
|
# from no index is exactly that. An empty file would not be: git reads it as a
|
|
56
56
|
# corrupt index.
|
|
57
57
|
if [ -f "$index" ]; then
|
|
58
|
-
cp "$index" "$scratch/index" 2>/dev/null || fail "could not copy $index into $scratch"
|
|
58
|
+
cp -p "$index" "$scratch/index" 2>/dev/null || fail "could not copy $index into $scratch"
|
|
59
59
|
fi
|
|
60
60
|
|
|
61
61
|
GIT_INDEX_FILE="$scratch/index" git add -A >/dev/null || exit 1
|
|
@@ -121,6 +121,9 @@ SUMMARY="$(jq -s --argjson costs "$COSTS" --arg sid "$(basename "$TRANSCRIPT" .j
|
|
|
121
121
|
def route: "(?:^|[;&|(])[[:space:]]*(?:(?:bash|sh|zsh)[[:space:]]+)?(?:\"[^\"]*|\u0027[^\u0027]*|[^[:space:];&|\"\u0027]*)status\\.sh[\"\u0027]?[[:space:]]+route[[:space:]]+(fast|main|deep)(?:[[:space:];&|]|$)";
|
|
122
122
|
def routes: [ .message.content[]? | select(.type == "tool_use" and .name == "Bash")
|
|
123
123
|
| (.input.command // "") | select(test(route)) ];
|
|
124
|
+
# Anchored the way route is, so a search or a commit message that names the
|
|
125
|
+
# call is not metered as one.
|
|
126
|
+
def tchanged: "(?:^|[;&|(])[[:space:]]*(?:(?:bash|sh|zsh)[[:space:]]+)?(?:\"[^\"]*|\u0027[^\u0027]*|[^[:space:];&|\"\u0027]*)status\\.sh[\"\u0027]?[[:space:]]+tests-changed(?:[[:space:]]|$)";
|
|
124
127
|
def announces: (.type == "assistant") and (is_meta | not)
|
|
125
128
|
and ((texts | test(marker; "i")) or (texts | test(lead; "i")) or (routes | length > 0));
|
|
126
129
|
# Prose comes before the calls in a message, so its channel goes into the
|
|
@@ -255,12 +258,19 @@ SUMMARY="$(jq -s --argjson costs "$COSTS" --arg sid "$(basename "$TRANSCRIPT" .j
|
|
|
255
258
|
else .[0:-1] + [{ s: .[-1].s, e: ([.[-1].e, $i.e] | max) }] end)
|
|
256
259
|
| map(.e - .s) | add // 0) as $agent_span
|
|
257
260
|
|
|
261
|
+
| [ $run[] | select((.type == "assistant") and (is_meta | not))
|
|
262
|
+
| .message.content[]? | select(.type == "tool_use" and .name == "Bash")
|
|
263
|
+
| (.input.command // "") | select(test(tchanged)) ] as $tc_calls
|
|
264
|
+
| ([ $tc_calls[] | select(test("[[:space:]]--none([[:space:]]|$)")) ] | length) as $tc_unproven
|
|
265
|
+
|
|
258
266
|
| { empty: false,
|
|
259
267
|
trail: (if ($trail | length) == 0 then "not announced" else ($trail | join(" → ")) end),
|
|
260
268
|
elapsed: $elapsed, waiting: $waiting,
|
|
261
269
|
tool_total: ($tools | length),
|
|
262
270
|
tool_top: ([ $tools | group_by(.)[] | { n: length, name: (.[0] | ascii_downcase | split("__") | last) } ]
|
|
263
271
|
| sort_by(-.n) | .[0:3]),
|
|
272
|
+
tc_total: ($tc_calls | length),
|
|
273
|
+
tc_unproven: $tc_unproven,
|
|
264
274
|
out_tok: $out_tok, cache_tok: $cache_tok,
|
|
265
275
|
agents: $agents,
|
|
266
276
|
priced_count: ($priced | length),
|
|
@@ -323,6 +333,10 @@ fi
|
|
|
323
333
|
|
|
324
334
|
printf 'tokens %s out · %s cache read\n' "$(tok "$(g '.out_tok')")" "$(tok "$(g '.cache_tok')")"
|
|
325
335
|
printf 'tests %s\n' "${TESTS:-not reported}"
|
|
336
|
+
tc_total=$(g '.tc_total'); tc_unproven=$(g '.tc_unproven')
|
|
337
|
+
if [ "$tc_total" -gt 0 ]; then
|
|
338
|
+
printf 'tests changed %s proved · %s unproven\n' "$((tc_total - tc_unproven))" "$tc_unproven"
|
|
339
|
+
fi
|
|
326
340
|
|
|
327
341
|
# The diff is the branch as a whole against where it left the base, working
|
|
328
342
|
# tree included, because uncommitted work is still work this run produced.
|
|
@@ -82,9 +82,51 @@ stamp_baseline() {
|
|
|
82
82
|
# one. Without this the directory grows for the life of the machine.
|
|
83
83
|
find "$dir" "${dir%/stamps}/nudged" -type f -mtime +7 -delete 2>/dev/null || true
|
|
84
84
|
|
|
85
|
-
|
|
85
|
+
# `git status` refreshes the index's stat cache when it can take the lock,
|
|
86
|
+
# and this hook must leave the index as it found it.
|
|
87
|
+
digest="$(GIT_OPTIONAL_LOCKS=0 bash "$here/tree-snapshot.sh" "$tree" 2>/dev/null)" || return 0
|
|
86
88
|
[ -n "$digest" ] || return 0
|
|
87
|
-
|
|
89
|
+
# Line 3 is the commit the session's changes are measured from. Work in
|
|
90
|
+
# progress already in the tree, untracked files included, is part of it,
|
|
91
|
+
# so it is never charged to the session: a test file that was untracked at
|
|
92
|
+
# the start and edited since must not read as new. It is built the way
|
|
93
|
+
# test-changes.sh builds its side, `add -A` on a scratch index, so the two
|
|
94
|
+
# compare like with like; the working tree, the real index and the stash
|
|
95
|
+
# stack, which other sessions share, are never written. The copy keeps the
|
|
96
|
+
# index's mtime, because git trusts an entry's stat data once the index is
|
|
97
|
+
# newer than it, and a fresh mtime would hide a same-size edit made in the
|
|
98
|
+
# second the file was staged.
|
|
99
|
+
#
|
|
100
|
+
# A tree that matches HEAD records HEAD itself rather than a fresh commit
|
|
101
|
+
# of the same tree: the comparison is the same, and a clean start leaves
|
|
102
|
+
# no dangling object behind.
|
|
103
|
+
#
|
|
104
|
+
# The line is left out, as unknown, when HEAD is unborn (commit-tree needs
|
|
105
|
+
# a parent and none is invented), when the git dir or the index cannot be
|
|
106
|
+
# read or copied, and when any of add, write-tree or commit-tree fails.
|
|
107
|
+
local base="" head gitdir idx scratch tree_id
|
|
108
|
+
if head="$(git -C "$tree" rev-parse --verify -q HEAD 2>/dev/null)" &&
|
|
109
|
+
gitdir="$(git -C "$tree" rev-parse --absolute-git-dir 2>/dev/null)" &&
|
|
110
|
+
idx="$(git -C "$tree" rev-parse --path-format=absolute --git-path index 2>/dev/null)" &&
|
|
111
|
+
scratch="$(mktemp -d "$gitdir/sluice-session-base.XXXXXX" 2>/dev/null)"; then
|
|
112
|
+
trap 'rm -rf "$scratch"' EXIT
|
|
113
|
+
if cp -p "$idx" "$scratch/index" 2>/dev/null &&
|
|
114
|
+
GIT_INDEX_FILE="$scratch/index" git -C "$tree" add -A >/dev/null 2>&1 &&
|
|
115
|
+
tree_id="$(GIT_INDEX_FILE="$scratch/index" git -C "$tree" write-tree 2>/dev/null)"; then
|
|
116
|
+
if [ "$tree_id" = "$(git -C "$tree" rev-parse --verify -q "HEAD^{tree}" 2>/dev/null)" ]; then
|
|
117
|
+
base="$head"
|
|
118
|
+
else
|
|
119
|
+
base="$(git -C "$tree" commit-tree "$tree_id" -p "$head" -m "sluice session base" 2>/dev/null)" || base=""
|
|
120
|
+
fi
|
|
121
|
+
fi
|
|
122
|
+
rm -rf "$scratch"
|
|
123
|
+
trap - EXIT
|
|
124
|
+
fi
|
|
125
|
+
if [ -n "$base" ]; then
|
|
126
|
+
printf '%s\n%s\n%s\n' "$tree" "$digest" "$base" >"$dir/$sid" 2>/dev/null || true
|
|
127
|
+
else
|
|
128
|
+
printf '%s\n%s\n' "$tree" "$digest" >"$dir/$sid" 2>/dev/null || true
|
|
129
|
+
fi
|
|
88
130
|
}
|
|
89
131
|
stamp_baseline "$sid" "$source"
|
|
90
132
|
|