@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 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.14**. Follow the
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. Cost was the same ($0.54 without, $0.56 with).
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
 
@@ -1,4 +1,4 @@
1
- # Opt-in development pilot: alpha.14
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.14
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.14
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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@redue/cli",
3
3
  "private": false,
4
- "version": "0.1.0-alpha.14",
4
+ "version": "0.1.0-alpha.15",
5
5
  "publishConfig": {
6
6
  "access": "public",
7
7
  "tag": "alpha"
@@ -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 and
92
- // what a skip means. The hook does the rest, so the block does not point to
93
- // the skill; the skill stays for agents that look for it. A typecheck-only
94
- // block keeps its earlier wording.
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=rows.filter(row=>row.kind!=='test').map(row=>row.form),tests=rows.filter(row=>row.kind==='test').map(row=>row.form);
98
- return `\n\n<!-- REDUE:${host}:begin -->\n`+
99
- (!rows.length?`Before you run, skip or report a typecheck or other configured check, read\n`+
100
- `${hosts[host].skill}.\n`:!tests.length?`Typecheck with ${quoted(typechecks)}. If its last pass still applies to\n`+
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
- (typechecks.length?`Typecheck with ${quoted(typechecks)}.\n`:'')+
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
- `<!-- REDUE:${host}:end -->\n`;}
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
@@ -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')),match=matched(command,parsed,record.config);
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){