@redue/cli 0.1.0-alpha.14 → 0.1.0-alpha.15
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/CHANGELOG.md +17 -0
- package/README.md +4 -2
- package/docs/user/pilot.md +3 -3
- package/package.json +1 -1
- package/src/agent-integration.mjs +29 -13
- package/src/claude-hook.mjs +21 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.0-alpha.15 — 2026-10-22
|
|
4
|
+
|
|
5
|
+
No change to observation, status or `redue run` since alpha.14; the changes
|
|
6
|
+
are to the Claude Code integration.
|
|
7
|
+
|
|
8
|
+
- The CLAUDE.md block now tells agents to run the typecheck or test command
|
|
9
|
+
before saying it passes, even after an edit that seems irrelevant, and to
|
|
10
|
+
leave skipping to REDUE's hook. In the alpha.14 eval, two of six mixed-task
|
|
11
|
+
sessions skipped their final test run on their own after a README edit;
|
|
12
|
+
with the new block, none of six did (twilio and SwitchHosts, CI
|
|
13
|
+
39702429765), at the same token cost. Run `redue agent setup claude --apply`
|
|
14
|
+
again to update the block; the earlier wording is recognized and replaced.
|
|
15
|
+
- The hook also matches a typecheck or test command after a leading
|
|
16
|
+
`cd <project root>;` or `cd <project root> &&`. A skip denies the whole
|
|
17
|
+
command; a rewrite keeps the `cd`. A `cd` to any other directory matches
|
|
18
|
+
nothing.
|
|
19
|
+
|
|
3
20
|
## 0.1.0-alpha.14 — 2026-10-21
|
|
4
21
|
|
|
5
22
|
Bench numbers below are from one 800-file project on macOS arm64, Node 22.13.0.
|
package/README.md
CHANGED
|
@@ -37,7 +37,7 @@ it; relevant source edits make it stale. [Run the reproducible demo](docs/contri
|
|
|
37
37
|
|
|
38
38
|
## Install the alpha
|
|
39
39
|
|
|
40
|
-
The opt-in pilot uses **0.1.0-alpha.
|
|
40
|
+
The opt-in pilot uses **0.1.0-alpha.15**. Follow the
|
|
41
41
|
[fixed-version pilot guide](docs/user/pilot.md) for supported projects, setup,
|
|
42
42
|
updates and voluntary feedback. No external usage results are claimed yet.
|
|
43
43
|
|
|
@@ -156,7 +156,9 @@ Medians per task, summed over the three projects:
|
|
|
156
156
|
source-change task skipped nothing; its time difference is session variance.
|
|
157
157
|
In two mixed-task sessions the agent left out its final test run on its own
|
|
158
158
|
after a README-only edit, without asking REDUE, and said so; the eval report
|
|
159
|
-
lists such sessions.
|
|
159
|
+
lists such sessions. Since alpha.15 the agent instructions ask agents to run
|
|
160
|
+
the check and leave skipping to REDUE; in a 12-session rerun of that task,
|
|
161
|
+
no agent left it out. Cost was the same ($0.54 without, $0.56 with).
|
|
160
162
|
|
|
161
163
|
## What the states mean
|
|
162
164
|
|
package/docs/user/pilot.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Opt-in development pilot: alpha.
|
|
1
|
+
# Opt-in development pilot: alpha.15
|
|
2
2
|
|
|
3
3
|
Use one fixed build for several ordinary development sessions. The owner will
|
|
4
4
|
invite two or three willing testers and approve their projects; nothing is
|
|
@@ -35,7 +35,7 @@ are **NOT RUN**. The recording shortcut is macOS/direct-recipe only.
|
|
|
35
35
|
For a new default npm installation:
|
|
36
36
|
|
|
37
37
|
```sh
|
|
38
|
-
npm install --global @redue/cli@0.1.0-alpha.
|
|
38
|
+
npm install --global @redue/cli@0.1.0-alpha.15
|
|
39
39
|
redue --version
|
|
40
40
|
cd my-project
|
|
41
41
|
redue --config redue.config.json init --recipe typescript-direct --check typecheck --dry-run
|
|
@@ -89,7 +89,7 @@ using it; keep the same npm prefix, config and any explicit state selection.
|
|
|
89
89
|
|
|
90
90
|
```sh
|
|
91
91
|
redue --config redue.config.json stop
|
|
92
|
-
npm install --global @redue/cli@0.1.0-alpha.
|
|
92
|
+
npm install --global @redue/cli@0.1.0-alpha.15
|
|
93
93
|
redue --config redue.config.json agent setup codex --dry-run
|
|
94
94
|
redue --config redue.config.json agent setup codex --apply
|
|
95
95
|
redue --config redue.config.json start
|
package/package.json
CHANGED
|
@@ -88,23 +88,39 @@ function saved(root,host){const text=read(root,marker(host));if(text===null)retu
|
|
|
88
88
|
return value;
|
|
89
89
|
}
|
|
90
90
|
// The managed block is always in the agent's context, so it stays short. For
|
|
91
|
-
// Claude Code it names the typecheck and test commands the hook matches
|
|
92
|
-
//
|
|
93
|
-
//
|
|
94
|
-
//
|
|
91
|
+
// Claude Code it names the typecheck and test commands the hook matches, asks
|
|
92
|
+
// the agent to run them rather than judge for itself that a rerun is not
|
|
93
|
+
// needed, and says what a skip means. The hook does the rest, so the block
|
|
94
|
+
// does not point to the skill; the skill stays for agents that look for it.
|
|
95
95
|
const quoted=forms=>forms.map(form=>`\`${form}\``).join(' and ');
|
|
96
|
+
const commandsOf=rows=>[rows.filter(row=>row.kind!=='test').map(row=>row.form),rows.filter(row=>row.kind==='test').map(row=>row.form)];
|
|
97
|
+
const fence=(host,body)=>`\n\n<!-- REDUE:${host}:begin -->\n${body}<!-- REDUE:${host}:end -->\n`;
|
|
98
|
+
const skillPointer=host=>`Before you run, skip or report a typecheck or other configured check, read\n${hosts[host].skill}.\n`;
|
|
96
99
|
function blockFor(host,rows=[]){
|
|
97
|
-
const typechecks
|
|
98
|
-
return
|
|
99
|
-
(!
|
|
100
|
-
|
|
100
|
+
const [typechecks,tests]=commandsOf(rows);
|
|
101
|
+
if(!rows.length)return fence(host,skillPointer(host));
|
|
102
|
+
if(!tests.length)return fence(host,`Typecheck with ${quoted(typechecks)}. Run it before you say it passes, even after an\n`+
|
|
103
|
+
`edit you think cannot matter; don't skip it yourself. If its last pass still\n`+
|
|
104
|
+
`applies to these exact files, a REDUE hook skips the run and says so. That\n`+
|
|
105
|
+
`skip counts as a pass.\n`);
|
|
106
|
+
return fence(host,(typechecks.length?`Typecheck with ${quoted(typechecks)}.\n`:'')+
|
|
107
|
+
`Run the unit tests with ${quoted(tests)}.\n`+
|
|
108
|
+
`Run a check before you say it passes, even after an edit you think cannot\n`+
|
|
109
|
+
`matter; don't skip it yourself. If its last pass still applies to these exact\n`+
|
|
110
|
+
`files, a REDUE hook skips the run and says so. That skip counts as a pass.\n`);
|
|
111
|
+
}
|
|
112
|
+
// The alpha.12-alpha.14 wording, recognized so setup can replace it.
|
|
113
|
+
function earlierBlock(host,rows=[]){
|
|
114
|
+
const [typechecks,tests]=commandsOf(rows);
|
|
115
|
+
if(!rows.length)return fence(host,skillPointer(host));
|
|
116
|
+
if(!tests.length)return fence(host,`Typecheck with ${quoted(typechecks)}. If its last pass still applies to\n`+
|
|
101
117
|
`these exact files, a REDUE hook skips the run and says so. The skip counts as\n`+
|
|
102
|
-
`a pass; don't check it again.\n
|
|
103
|
-
|
|
118
|
+
`a pass; don't check it again.\n`);
|
|
119
|
+
return fence(host,(typechecks.length?`Typecheck with ${quoted(typechecks)}.\n`:'')+
|
|
104
120
|
`Run the unit tests with ${quoted(tests)}.\n`+
|
|
105
121
|
`If a check's last pass still applies to these exact files, a REDUE hook skips\n`+
|
|
106
|
-
`the run and says so. The skip counts as a pass; don't check it again.\n`)
|
|
107
|
-
|
|
122
|
+
`the run and says so. The skip counts as a pass; don't check it again.\n`);
|
|
123
|
+
}
|
|
108
124
|
// One form per check: the npm script when there is one, since that is what
|
|
109
125
|
// the project's own instructions usually name.
|
|
110
126
|
const blockForms=(host,config,relative)=>host!=='claude'?[]:hookForms(config,relative).map(row=>({kind:row.kind,
|
|
@@ -131,7 +147,7 @@ function knownBlock(host,relative,block){
|
|
|
131
147
|
const forms=pattern=>{const named=new RegExp(pattern).exec(block);return named?[...named[1].matchAll(/`([^`]+)`/g)].map(m=>m[1]):[];};
|
|
132
148
|
const rows=[...forms(`^\\n\\n<!-- REDUE:[a-z]+:begin -->\\nTypecheck with ${list}\\.`).map(form=>({kind:'typecheck',form})),
|
|
133
149
|
...forms(`Run the unit tests with ${list}\\.\\n`).map(form=>({kind:'test',form}))];
|
|
134
|
-
return block===blockFor(host,rows)||priorBlocks(host,relative).includes(block);
|
|
150
|
+
return block===blockFor(host,rows)||block===earlierBlock(host,rows)||priorBlocks(host,relative).includes(block);
|
|
135
151
|
}
|
|
136
152
|
// The policy with the exact commands for this config, so an agent never builds
|
|
137
153
|
// a --config form the hook would not recognize. Claude Code gets the typecheck
|
package/src/claude-hook.mjs
CHANGED
|
@@ -7,7 +7,7 @@ import fs from 'node:fs';
|
|
|
7
7
|
import path from 'node:path';
|
|
8
8
|
import {spawn} from 'node:child_process';
|
|
9
9
|
import {projectState} from './project-location.mjs';
|
|
10
|
-
import {realObservedPath} from './path-identity.mjs';
|
|
10
|
+
import {realObservedPath,samePath} from './path-identity.mjs';
|
|
11
11
|
|
|
12
12
|
export const hookCommand='redue agent hook claude';
|
|
13
13
|
const safe=/^[A-Za-z0-9_./:=,+-]+$/;
|
|
@@ -73,6 +73,21 @@ export function recordedCommand(command,name,configFile=defaultConfig){
|
|
|
73
73
|
return parts.base===run?null:run+parts.suffix;
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
+
// A leading `cd DIR;` or `cd DIR &&` into the project root, which agents
|
|
77
|
+
// often add before a check: {prefix, command} with the rest of the command,
|
|
78
|
+
// the whole command when there is no such prefix, or null when it enters
|
|
79
|
+
// another directory. DIR may be quoted; it may not use variables, a leading
|
|
80
|
+
// `~` or escapes. Inside double quotes a backslash before anything but
|
|
81
|
+
// $ ` " \ is literal, so a quoted Windows path matches; unquoted it is an escape.
|
|
82
|
+
const cdPrefix=/^[ \t]*cd[ \t]+(?:"((?:[^"$`\\]|\\(?![$`"\\\n]))*)"|'([^']*)'|((?!~)[^\s;&|'"$`\\]+))[ \t]*(?:;|&&)[ \t]*/;
|
|
83
|
+
export function rootCd(command,cwd,projectRoot){
|
|
84
|
+
const prefix=typeof command==='string'?cdPrefix.exec(command):null;
|
|
85
|
+
if(!prefix)return {prefix:'',command};
|
|
86
|
+
const dir=prefix[1]??prefix[2]??prefix[3];
|
|
87
|
+
try{if(!samePath(realObservedPath(path.resolve(cwd,dir)),projectRoot))return null;}catch{return null;}
|
|
88
|
+
return {prefix:prefix[0],command:command.slice(prefix[0].length)};
|
|
89
|
+
}
|
|
90
|
+
|
|
76
91
|
export function verdict(status,name,rest=null){
|
|
77
92
|
const row=status?.schema===1&&status.observation?.healthy===true&&Array.isArray(status.checks)?
|
|
78
93
|
status.checks.find(item=>item?.name===name):null;
|
|
@@ -129,7 +144,9 @@ export async function claudeHook(input,{entry,timeout}){
|
|
|
129
144
|
if(record?.schema!==1||record.host!=='claude'||typeof record.config!=='string'||
|
|
130
145
|
!/^[^/\\]+\.json$/.test(record.config))return null;
|
|
131
146
|
const config=path.join(root,record.config);
|
|
132
|
-
const parsed=JSON.parse(fs.readFileSync(config,'utf8'))
|
|
147
|
+
const parsed=JSON.parse(fs.readFileSync(config,'utf8'));
|
|
148
|
+
const entered=rootCd(command,root,realObservedPath(path.resolve(path.dirname(config),parsed.root||'.')));
|
|
149
|
+
const match=entered&&matched(entered.command,parsed,record.config);
|
|
133
150
|
if(!match)return null;
|
|
134
151
|
const name=match.check,status=await synchronizedStatus(entry,config,root,timeout??lastRunTimeout(config,parsed,name)),
|
|
135
152
|
reason=verdict(status,name,match.rest);
|
|
@@ -140,12 +157,12 @@ export async function claudeHook(input,{entry,timeout}){
|
|
|
140
157
|
// stays too: the rewrite returns "allow", which would also skip the prompt
|
|
141
158
|
// for the rest of the chain.
|
|
142
159
|
const rewritten=status?.schema===1&&status.observation?.healthy===true&&Array.isArray(status.checks)&&
|
|
143
|
-
status.checks.some(item=>item?.name===name)?recordedCommand(command,name,record.config):null;
|
|
160
|
+
status.checks.some(item=>item?.name===name)?recordedCommand(entered.command,name,record.config):null;
|
|
144
161
|
// updatedInput replaces the whole tool input. Deny and ask rules still apply
|
|
145
162
|
// to the rewritten command; "allow" skips only the prompt.
|
|
146
163
|
return rewritten?{hookSpecificOutput:{hookEventName:'PreToolUse',permissionDecision:'allow',
|
|
147
164
|
permissionDecisionReason:`REDUE: check ${JSON.stringify(name)} is not CURRENT; running it through redue run so the result is recorded.`,
|
|
148
|
-
updatedInput:{...input.tool_input,command:rewritten}}}:null;
|
|
165
|
+
updatedInput:{...input.tool_input,command:entered.prefix+rewritten}}}:null;
|
|
149
166
|
}
|
|
150
167
|
|
|
151
168
|
export async function runClaudeHook(entry){
|