Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion .supertool.example.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,16 @@
"compact": false,
"rtk": false,
"parallel": 0,
"adviseForNewTest": false,
"_comment_advice": "Non-blocking [advice] lines emitted after a mutating op. Each rule: hooks_into (ops, default all mutating), match (path glob), when (new-file|existing-file|always), contains (regex over ADDED content), resolve/resolveFromValidator (subprocess emitting a would-be target via exit 3 + stderr), message ({target}/{path}/{op} interpolate). See docs/validators.md#advice.",
"advice": {
"newTest": {
"hooks_into": ["paste"],
"match": "*.php",
"when": "new-file",
"resolveFromValidator": true,
"message": "new class without test"
}
},
"validator_cache": true,
"validator_cache_ttl_hours": 24,
"_comment_defaults": "Default project/repo for payload ops that would otherwise require one. gl-issue-create reads 'gitlab_project' when the payload omits 'project'; gh-issue-create reads 'github_repo' when it omits 'repo'. Resolution order: explicit payload field > this config > the 'origin' git remote (host-matched). Omit this block to rely on git-remote auto-detect alone.",
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **`gl-job` / `gh-job` gained a `:fail` suffix** — `gl-job:ID:fail` (alias `:errors`) prints only the matched failure blocks with no tail, the tight triage view. It applies the same per-job pattern table as the default mode (rector → diff lines, phpstan → 🪪/type markers, unit → `FAILURES!`/`Failed asserting`), so a red job shows just its actionable failures instead of the default's blocks-plus-80-line-tail. This names the discoverable front door for a behavior that previously only existed as the undocumented `:errors` mode on `gl-job` (and was entirely absent on `gh-job`); `:errors` stays as a back-compat alias. The default (no suffix), `:grep:PATTERN`, and `:raw` are unchanged. Closes [#326](https://github.com/Digital-Process-Tools/claude-supertool/issues/326).
- **`grep` gained a `:no-auto-read` flag** — `grep:PATTERN:PATH:no-auto-read` suppresses the single-small-file auto-read so only the matching line(s) are emitted, mirroring `glob`'s existing flag. Default behavior (auto-read a concrete file < 20KB on a match) is unchanged. The flag is order-independent with `:count` and any `LIMIT`/`CONTEXT` args. Avoids silently dumping 10-18KB of unrequested file content when the caller only wants the hit. Closes [#320](https://github.com/Digital-Process-Tools/claude-supertool/issues/320).
- **`git-commit` gained an `@file` payload route for multi-line messages** — `git-commit:@-` (stdin) or `git-commit:@msg.toml` reads a `message` field (subject + blank line + body) and an optional `paths` list, so a real subject+body commit no longer forces a drop to raw `git commit -F file` — which skipped the op's auto `Co-Authored-By` trailer and needed a follow-up `--amend`. The trailer is still appended on the payload route. The colon-CLI one-liner (`git-commit:::MESSAGE`) is unchanged. Mechanically this generalizes the `@file` field parser: a syntax token in a trailing `[...]` group is now optional, and a `...` token is variadic (a payload list expands into multiple positional args) — previously the parser leaked the `[`/`...]` brackets into the field names (`msg[`, `path...]`), so the route 404'd with `missing required field 'msg['`. Closes [#340](https://github.com/Digital-Process-Tools/claude-supertool/issues/340).
- **Generalized the new-class-without-test advisory into a config-driven `advice` block** — the hardcoded `adviseForNewTest` bool is replaced by a top-level `advice` map of named rules, each gated (all optional) by `hooks_into` (ops, default all mutating), `match` (path glob), `when` (`new-file`|`existing-file`|`always`), `contains` (regex over the content the op *added* — lines present after but not before, so it fires only when the op introduces the pattern), and `resolve`/`resolveFromValidator` (a subprocess emitting a would-be target via the same exit-3 + stderr contract). `message` is the rendered line, with `{target}`/`{path}`/`{op}` interpolation. The old single-purpose advisory was a dead end the moment a second nudge was wanted (e.g. "regenerate the XSD/cache after adding a component class or public attribute" — exactly the easy-to-forget step the XML editor depends on); the generic block lets a project add that as data instead of code. **Breaking:** the `adviseForNewTest: true` bool is removed — express it as an `advice.newTest` rule (`{"hooks_into": ["paste"], "match": "*.php", "when": "new-file", "resolveFromValidator": true, "message": "new class without test"}`); see `.supertool.example.json`. Closes [#347](https://github.com/Digital-Process-Tools/claude-supertool/issues/347).

### Fixed

Expand Down
18 changes: 15 additions & 3 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -176,13 +176,25 @@ Set `"compact": true` in `.supertool.json` to enable compact reads. When enabled

Compact is disabled when using `grep=` filter or `offset` (editing needs exact lines).

## Advise on new class without test
## Advice — config-driven post-edit hints

```json
{ "adviseForNewTest": true }
{
"advice": {
"newTest": {
"hooks_into": ["paste"], "match": "*.php", "when": "new-file",
"resolveFromValidator": true, "message": "new class without test"
},
"newComponent": {
"hooks_into": ["edit", "paste"], "match": "*.php",
"contains": "extends \\w*ComponentBase|implements \\w*IComponent",
"message": "XSD/cache regen likely (dvsi_xsd + dvsi_clearcache)"
}
}
}
```

Opt-in (default false). When set, a `paste` that creates a new `*.php` file with no resolvable sibling test gets a non-blocking `[advice]` line appended to the op output. It reuses a validator's `resolve` cmd to find the would-be test path. Full contract in [validators.md → resolve](validators.md#resolve--map-a-source-file-to-its-real-target).
Each rule appends a non-blocking `[advice]` line after a mutating op when it matches. Gates: `hooks_into` (ops, default all mutating), `match` (path glob), `when` (`new-file`|`existing-file`|`always`), `contains` (regex over the content the op *added*), and `resolve`/`resolveFromValidator` (a subprocess emitting a would-be target). Full field reference and the resolve contract in [validators.md → advice](validators.md#advice--config-driven-post-op-hints).

## Parallel execution

Expand Down
48 changes: 39 additions & 9 deletions docs/validators.md
Original file line number Diff line number Diff line change
Expand Up @@ -241,30 +241,60 @@ By default a validator runs against the file the op touched. The optional `resol

The exit code is **ignored** by the validator path. That is deliberate, and it's what makes the advisory below possible.

### adviseForNewTestnag on a new class with no test
### adviceconfig-driven post-op hints

Autonomous flows that write only through supertool bypass the editor/git-hook reminders to write a test. With the top-level flag enabled:
Autonomous flows that write only through supertool bypass the editor/git-hook reminders to do follow-up work (write a test, regenerate the XSD, …). The top-level `advice` block emits non-blocking `[advice]` lines after a mutating op when a rule matches:

```json
// .supertool.json — opt-in, default false
{ "adviseForNewTest": true }
// .supertool.json
{
"advice": {
"newTest": {
"hooks_into": ["paste"],
"match": "*.php",
"when": "new-file",
"resolveFromValidator": true,
"message": "new class without test"
},
"newComponent": {
"hooks_into": ["edit", "paste"],
"match": "*.php",
"contains": "extends \\w*ComponentBase|implements \\w*IComponent",
"message": "XSD/cache regen likely (dvsi_xsd + dvsi_clearcache)"
}
}
}
```

a `paste` that **creates a new `*.php` file** (it did not exist before) with no resolvable sibling test gets a non-blocking line appended to the op output:
Each rule is gated by (all optional):

| field | meaning | default |
|-------|---------|---------|
| `hooks_into` | ops that trigger the rule | all mutating (`edit`, `paste`, `replace`, `replace_lines`, `vim`) |
| `match` | path glob | `*` |
| `when` | `new-file` \\| `existing-file` \\| `always` — gated on whether the file existed before the op | `always` |
| `contains` | regex tested against the **content the op added** (lines present after but not before) — fires only when the op *introduces* the pattern, not when the file already held it | — (no content gate) |
| `resolve` | a subprocess (a source→target resolver) emitting a would-be target | — |
| `resolveFromValidator` | reuse the first `resolve` cmd declared on a validator instead of duplicating it | `false` |
| `message` | the line shown; `{target}`/`{path}`/`{op}` interpolate. A message with no `{target}` gets ` — consider <target>` appended when a resolver produced one | `""` |

A `paste` that **creates a new `*.php` file** with no resolvable sibling test (the `newTest` rule above) appends:

```
[advice]
ℹ new class without test — consider tests/unit/SiX/FooTest.php
```

It reuses the same `resolve` cmd declared on a validator, so the source→test path logic lives in exactly one place. Because the advisory needs the *would-be* target (which doesn't exist yet) while the validator path needs stdout to stay empty on a miss, the resolver signals the two cases on different channels:
#### The resolve contract

A `resolve`/`resolveFromValidator` rule reuses the same `resolve` cmd declared on a validator, so the source→target path logic lives in exactly one place. Because the advisory needs the *would-be* target (which doesn't exist yet) while the validator path needs stdout to stay empty on a miss, the resolver signals the two cases on different channels:

| case | stdout | stderr | exit |
|------|--------|--------|------|
| test exists | test path | — | 0 |
| no test | *(empty)* | would-be target | 3 |
| target exists | target path | — | 0 |
| no target | *(empty)* | would-be target | 3 |

stdout stays empty on a miss → the validator still skips, unchanged. The miss target rides on **stderr**, flagged by **exit 3** → only the advisory reads it. Advisory only: it never blocks the write, and never fires on a rewrite of an existing file.
stdout stays empty on a miss → the validator still skips, unchanged. The miss target rides on **stderr**, flagged by **exit 3** → only the advisory reads it. Advisory only: it never blocks the write.

## Format-on-save

Expand Down
Loading
Loading