@brunosps00/dev-workflow 2.5.0 → 2.6.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/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +29 -0
- package/README.md +1 -1
- package/lib/doctor.js +46 -9
- package/lib/hooks.js +112 -27
- package/lib/init.js +11 -5
- package/lib/uninstall.js +2 -2
- package/package.json +1 -1
- package/scaffold/en/references/invariants.md +3 -2
- package/scaffold/pt-br/references/invariants.md +3 -2
- package/scaffold/scripts/hooks/git-guardrails.mjs +3 -2
- package/scaffold/scripts/hooks/statusline.mjs +14 -11
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dev-workflow",
|
|
3
3
|
"description": "Standalone, à-la-carte skills from the dev-workflow pipeline: minimalism, search-first, simplification, verification, security review, and more.",
|
|
4
|
-
"version": "2.
|
|
4
|
+
"version": "2.6.0",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Bruno Santos"
|
|
7
7
|
},
|
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,35 @@ project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
10
10
|
> those versions were released, so they are summaries of what shipped, not
|
|
11
11
|
> contemporaneous release notes. `git log` remains the authoritative record.
|
|
12
12
|
|
|
13
|
+
## [2.6.0] — 2026-10-07
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
|
|
17
|
+
- **Git guardrails under Codex.** `init` and `update` also write the git-guardrails
|
|
18
|
+
`PreToolUse` hook to `.codex/hooks.json` — same payload and deny contract as Claude Code,
|
|
19
|
+
matcher `Bash`. It is the only hook Codex gets: the session-cost tracker parses Claude Code
|
|
20
|
+
transcripts and Codex has no statusLine command. Codex runs project hooks only after the
|
|
21
|
+
project and the hook are trusted. Merge-aware like the Claude side: a malformed
|
|
22
|
+
`.codex/hooks.json` is left untouched, and `uninstall` removes only our entry, deleting the
|
|
23
|
+
file when nothing else is left. `doctor` warns when the Codex guardrail is missing or
|
|
24
|
+
outdated.
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
|
|
28
|
+
- **Hooks stopped running whenever the shell left the project root.** The hook and statusLine
|
|
29
|
+
commands were cwd-relative (`node .dw/scripts/hooks/…`). Claude Code runs hooks in the
|
|
30
|
+
session's current directory, which follows every `cd`, so from any subdirectory they failed
|
|
31
|
+
with `Cannot find module` — and a failing `PreToolUse` hook is non-blocking, so Bash ran with
|
|
32
|
+
no git guardrail at all. In one real session, 843 of 1,243 Bash calls ran unguarded this way.
|
|
33
|
+
Commands now resolve `${CLAUDE_PROJECT_DIR:-$(git rev-parse --show-toplevel || pwd)}`:
|
|
34
|
+
Claude Code exports the variable to hooks, and Codex, which exports none, gets the git root.
|
|
35
|
+
`update` rewrites the old entries in place, and `doctor` reports a cwd-relative guardrail as
|
|
36
|
+
an issue.
|
|
37
|
+
- The statusline read `.dw/` state (active spec, minimalism mode, today's spend) from the
|
|
38
|
+
current directory. It now reads it from the payload's `workspace.project_dir`.
|
|
39
|
+
- The guardrail's deny message pointed only at `.claude/settings.json`; it now names
|
|
40
|
+
`.codex/hooks.json` too, as does the invariants floor.
|
|
41
|
+
|
|
13
42
|
## [2.5.0] — 2026-09-28
|
|
14
43
|
|
|
15
44
|
### Added
|
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@ This will:
|
|
|
16
16
|
5. Install bundled skills (`dw-verify`, `dw-memory`, `dw-review-rigor`, `dw-ui-discipline`, `dw-testing-discipline`, `security-review`, etc.) to `.agents/skills/`
|
|
17
17
|
6. Generate skill wrappers and project agents for Claude Code, Codex, Copilot, and OpenCode
|
|
18
18
|
7. Configure MCP servers (Context7 + Playwright)
|
|
19
|
-
8. Install enforcement hooks (git guardrails + a `SessionEnd` token-cost tracker → `.dw/metrics/costs.jsonl`) + a statusline (branch · active spec · minimalism mode · today's estimated spend) into `.claude/settings.json` (merge-aware — your own hooks/statusline are never overwritten)
|
|
19
|
+
8. Install enforcement hooks (git guardrails + a `SessionEnd` token-cost tracker → `.dw/metrics/costs.jsonl`) + a statusline (branch · active spec · minimalism mode · today's estimated spend) into `.claude/settings.json`, and the git guardrails into `.codex/hooks.json` for Codex (merge-aware — your own hooks/statusline are never overwritten; Codex runs project hooks only after you trust them)
|
|
20
20
|
|
|
21
21
|
> **Compozy-inspired disciplines.** Since 0.5.0, dev-workflow bundles three primitives — `dw-verify`, `dw-memory`, `dw-review-rigor` — adapted from the [Compozy](https://github.com/compozy/compozy) project and invoked internally by existing commands. See [docs/compozy-integration.md](docs/compozy-integration.md) for what was ported and what was not.
|
|
22
22
|
|
package/lib/doctor.js
CHANGED
|
@@ -10,7 +10,7 @@ const {
|
|
|
10
10
|
} = require('./agents');
|
|
11
11
|
const { readInstallState, STATE_RELATIVE_PATH } = require('./install-state');
|
|
12
12
|
const { validateSkillRegistry, readSkillRegistry, validateSkillStructuredReturn } = require('./skill-registry');
|
|
13
|
-
const { GIT_GUARDRAILS_CMD, STATUSLINE_CMD, HOOKS_DIR_MARKER } = require('./hooks');
|
|
13
|
+
const { GIT_GUARDRAILS_SCRIPT, GIT_GUARDRAILS_CMD, STATUSLINE_CMD, HOOKS_DIR_MARKER, CODEX_HOOKS_FILE } = require('./hooks');
|
|
14
14
|
const removedCommands = require('./removed-commands');
|
|
15
15
|
|
|
16
16
|
function run() {
|
|
@@ -212,6 +212,8 @@ function checkHooks(projectRoot, issues, warnings) {
|
|
|
212
212
|
if (!fs.existsSync(file)) issues.push(`Missing hook script: ${path.relative(projectRoot, file)}`);
|
|
213
213
|
}
|
|
214
214
|
|
|
215
|
+
checkCodexHooks(projectRoot, warnings);
|
|
216
|
+
|
|
215
217
|
const settingsPath = path.join(projectRoot, '.claude', 'settings.json');
|
|
216
218
|
if (!fs.existsSync(settingsPath)) return; // checkMcp already reports this
|
|
217
219
|
|
|
@@ -222,17 +224,52 @@ function checkHooks(projectRoot, issues, warnings) {
|
|
|
222
224
|
return; // checkMcp already reports malformed settings
|
|
223
225
|
}
|
|
224
226
|
|
|
225
|
-
const
|
|
226
|
-
|
|
227
|
-
(
|
|
228
|
-
)
|
|
229
|
-
|
|
227
|
+
const guardrailCommands = guardrailCommandsOf(settings);
|
|
228
|
+
if (guardrailCommands.length === 0) {
|
|
229
|
+
issues.push('Missing git-guardrails PreToolUse hook in .claude/settings.json');
|
|
230
|
+
} else if (!guardrailCommands.includes(GIT_GUARDRAILS_CMD)) {
|
|
231
|
+
// Pre-2.5.1 installs wired a cwd-relative path: it fails (non-blocking) and stops
|
|
232
|
+
// guarding as soon as the shell leaves the project root.
|
|
233
|
+
issues.push('Outdated git-guardrails PreToolUse hook in .claude/settings.json. Run `dev-workflow update`.');
|
|
234
|
+
}
|
|
230
235
|
|
|
231
236
|
const statusLine = settings.statusLine;
|
|
237
|
+
const statusCommand = String((statusLine && statusLine.command) || '');
|
|
232
238
|
if (!statusLine) {
|
|
233
239
|
issues.push('Missing statusLine in .claude/settings.json');
|
|
234
|
-
} else if (
|
|
235
|
-
warnings.push(
|
|
240
|
+
} else if (!statusCommand.includes(HOOKS_DIR_MARKER)) {
|
|
241
|
+
warnings.push(`Custom statusLine set; dev-workflow statusline is not active (add \`${STATUSLINE_CMD}\`).`);
|
|
242
|
+
} else if (statusCommand !== STATUSLINE_CMD) {
|
|
243
|
+
warnings.push('Outdated dev-workflow statusLine in .claude/settings.json. Run `dev-workflow update`.');
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function guardrailCommandsOf(config) {
|
|
248
|
+
const preToolUse = (config && config.hooks && config.hooks.PreToolUse) || [];
|
|
249
|
+
return preToolUse.flatMap((g) =>
|
|
250
|
+
g && Array.isArray(g.hooks)
|
|
251
|
+
? g.hooks.map((h) => h && h.command).filter((c) => typeof c === 'string' && c.includes(GIT_GUARDRAILS_SCRIPT))
|
|
252
|
+
: [],
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Codex is one of several supported agents, so a missing Codex guardrail is a
|
|
257
|
+
// warning rather than an issue.
|
|
258
|
+
function checkCodexHooks(projectRoot, warnings) {
|
|
259
|
+
const file = path.join(projectRoot, CODEX_HOOKS_FILE);
|
|
260
|
+
let commands = [];
|
|
261
|
+
if (fs.existsSync(file)) {
|
|
262
|
+
try {
|
|
263
|
+
commands = guardrailCommandsOf(JSON.parse(fs.readFileSync(file, 'utf-8')));
|
|
264
|
+
} catch {
|
|
265
|
+
warnings.push(`Malformed ${CODEX_HOOKS_FILE}; Codex cannot load the git guardrail.`);
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
if (commands.length === 0) {
|
|
270
|
+
warnings.push(`Missing git-guardrails hook in ${CODEX_HOOKS_FILE} (Bash under Codex is unguarded). Run \`dev-workflow update\`.`);
|
|
271
|
+
} else if (!commands.includes(GIT_GUARDRAILS_CMD)) {
|
|
272
|
+
warnings.push(`Outdated git-guardrails hook in ${CODEX_HOOKS_FILE}. Run \`dev-workflow update\`.`);
|
|
236
273
|
}
|
|
237
274
|
}
|
|
238
275
|
|
|
@@ -245,4 +282,4 @@ function checkLegacyCommands(projectRoot, warnings) {
|
|
|
245
282
|
}
|
|
246
283
|
}
|
|
247
284
|
|
|
248
|
-
module.exports = { run };
|
|
285
|
+
module.exports = { run, checkHooks };
|
package/lib/hooks.js
CHANGED
|
@@ -1,14 +1,33 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
1
3
|
const { readSettings, writeSettings } = require('./mcp');
|
|
2
4
|
|
|
3
5
|
// dev-workflow-owned hook/statusline scripts live under this path. The substring
|
|
4
|
-
// is the marker used to identify OUR entries in .claude/settings.json
|
|
5
|
-
// add/refresh them on update without ever touching
|
|
6
|
+
// is the marker used to identify OUR entries in .claude/settings.json and
|
|
7
|
+
// .codex/hooks.json so we can add/refresh them on update without ever touching
|
|
8
|
+
// the user's own hooks.
|
|
6
9
|
const HOOKS_DIR_MARKER = '.dw/scripts/hooks/';
|
|
7
|
-
|
|
8
|
-
|
|
10
|
+
|
|
11
|
+
// Hooks run in the session's *current* directory, which in Claude Code follows
|
|
12
|
+
// every `cd` in the Bash tool. A cwd-relative `node .dw/...` breaks with "Cannot
|
|
13
|
+
// find module" as soon as the shell sits in a subdirectory, and a failing
|
|
14
|
+
// PreToolUse hook is non-blocking: the command runs unguarded. Claude Code exports
|
|
15
|
+
// $CLAUDE_PROJECT_DIR to hooks; Codex exports no project variable, so fall back to
|
|
16
|
+
// the git root (a worktree's own root), then to the cwd. The fallback is only
|
|
17
|
+
// evaluated when the variable is unset.
|
|
18
|
+
const PROJECT_ROOT = '${CLAUDE_PROJECT_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}';
|
|
19
|
+
const hookCommand = (script) => `node "${PROJECT_ROOT}/${HOOKS_DIR_MARKER}${script}"`;
|
|
20
|
+
|
|
21
|
+
// Codex reads project hooks from .codex/hooks.json (same shape as Claude Code's
|
|
22
|
+
// "hooks" block; PreToolUse matches tool_name "Bash"). It only runs them once the
|
|
23
|
+
// project's .codex/ layer and the hook itself are trusted in Codex.
|
|
24
|
+
const CODEX_HOOKS_FILE = path.join('.codex', 'hooks.json');
|
|
25
|
+
|
|
9
26
|
const GIT_GUARDRAILS_SCRIPT = 'git-guardrails.mjs';
|
|
10
|
-
const
|
|
27
|
+
const GIT_GUARDRAILS_CMD = hookCommand(GIT_GUARDRAILS_SCRIPT);
|
|
28
|
+
const STATUSLINE_CMD = hookCommand('statusline.mjs');
|
|
11
29
|
const SESSION_COST_SCRIPT = 'session-cost.mjs';
|
|
30
|
+
const SESSION_COST_CMD = hookCommand(SESSION_COST_SCRIPT);
|
|
12
31
|
|
|
13
32
|
function isOurs(command) {
|
|
14
33
|
return typeof command === 'string' && command.includes(HOOKS_DIR_MARKER);
|
|
@@ -25,8 +44,11 @@ function isOurs(command) {
|
|
|
25
44
|
* - leaves a user-defined statusLine untouched (reports it so the caller can
|
|
26
45
|
* surface a post-update note).
|
|
27
46
|
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
47
|
+
* Also reconciles the git guardrail into .codex/hooks.json (see installCodexHooks).
|
|
48
|
+
*
|
|
49
|
+
* Returns { hooks, sessionCost, statusLine, codex } where each is 'added' |
|
|
50
|
+
* 'updated' | 'unchanged' (statusLine may also be 'skipped-user', codex
|
|
51
|
+
* 'skipped-malformed').
|
|
30
52
|
*/
|
|
31
53
|
function installHooks(projectRoot) {
|
|
32
54
|
const settings = readSettings(projectRoot);
|
|
@@ -40,7 +62,44 @@ function installHooks(projectRoot) {
|
|
|
40
62
|
writeSettings(projectRoot, settings);
|
|
41
63
|
}
|
|
42
64
|
|
|
43
|
-
return {
|
|
65
|
+
return {
|
|
66
|
+
hooks: hooksResult,
|
|
67
|
+
sessionCost: sessionCostResult,
|
|
68
|
+
statusLine: statusLineResult,
|
|
69
|
+
codex: installCodexHooks(projectRoot),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function readCodexHooks(projectRoot) {
|
|
74
|
+
const file = path.join(projectRoot, CODEX_HOOKS_FILE);
|
|
75
|
+
if (!fs.existsSync(file)) return {};
|
|
76
|
+
try {
|
|
77
|
+
const parsed = JSON.parse(fs.readFileSync(file, 'utf-8'));
|
|
78
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : null;
|
|
79
|
+
} catch {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function writeCodexHooks(projectRoot, config) {
|
|
85
|
+
const file = path.join(projectRoot, CODEX_HOOKS_FILE);
|
|
86
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
87
|
+
fs.writeFileSync(file, JSON.stringify(config, null, 2) + '\n', 'utf-8');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Reconcile the git guardrail into .codex/hooks.json — the only dev-workflow hook
|
|
92
|
+
* Codex gets: session-cost parses Claude Code transcripts and Codex has no
|
|
93
|
+
* statusLine command. Same marker-based contract as the Claude side.
|
|
94
|
+
* Returns 'added' | 'updated' | 'unchanged' | 'skipped-malformed'.
|
|
95
|
+
*/
|
|
96
|
+
function installCodexHooks(projectRoot) {
|
|
97
|
+
const config = readCodexHooks(projectRoot);
|
|
98
|
+
if (config === null) return 'skipped-malformed';
|
|
99
|
+
const before = JSON.stringify(config);
|
|
100
|
+
const result = reconcilePreToolUseGitGuardrails(config);
|
|
101
|
+
if (JSON.stringify(config) !== before) writeCodexHooks(projectRoot, config);
|
|
102
|
+
return result;
|
|
44
103
|
}
|
|
45
104
|
|
|
46
105
|
/**
|
|
@@ -135,30 +194,14 @@ function reconcileStatusLine(settings) {
|
|
|
135
194
|
}
|
|
136
195
|
|
|
137
196
|
/**
|
|
138
|
-
* Remove ONLY dev-workflow-owned hook + statusLine entries
|
|
139
|
-
* Returns the number of entries removed.
|
|
197
|
+
* Remove ONLY dev-workflow-owned hook + statusLine entries from .claude/settings.json
|
|
198
|
+
* and .codex/hooks.json (used by uninstall). Returns the number of entries removed.
|
|
140
199
|
*/
|
|
141
200
|
function removeHooks(projectRoot) {
|
|
142
201
|
const settings = readSettings(projectRoot);
|
|
143
|
-
let removed = 0;
|
|
144
|
-
|
|
145
202
|
// Remove OUR hook entries from every event (PreToolUse, SessionEnd, PostToolUse, …),
|
|
146
203
|
// identified by the .dw/scripts/hooks/ marker; never touch user-added hooks.
|
|
147
|
-
|
|
148
|
-
for (const event of Object.keys(settings.hooks)) {
|
|
149
|
-
const groups = settings.hooks[event];
|
|
150
|
-
if (!Array.isArray(groups)) continue;
|
|
151
|
-
for (const group of groups) {
|
|
152
|
-
if (!group || !Array.isArray(group.hooks)) continue;
|
|
153
|
-
const kept = group.hooks.filter((h) => !(h && isOurs(h.command)));
|
|
154
|
-
removed += group.hooks.length - kept.length;
|
|
155
|
-
group.hooks = kept;
|
|
156
|
-
}
|
|
157
|
-
settings.hooks[event] = groups.filter((g) => g && Array.isArray(g.hooks) && g.hooks.length > 0);
|
|
158
|
-
if (settings.hooks[event].length === 0) delete settings.hooks[event];
|
|
159
|
-
}
|
|
160
|
-
if (Object.keys(settings.hooks).length === 0) delete settings.hooks;
|
|
161
|
-
}
|
|
204
|
+
let removed = removeOurHookEntries(settings);
|
|
162
205
|
|
|
163
206
|
if (settings.statusLine && isOurs(settings.statusLine.command)) {
|
|
164
207
|
delete settings.statusLine;
|
|
@@ -166,6 +209,46 @@ function removeHooks(projectRoot) {
|
|
|
166
209
|
}
|
|
167
210
|
|
|
168
211
|
if (removed > 0) writeSettings(projectRoot, settings);
|
|
212
|
+
return removed + removeCodexHooks(projectRoot);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function removeOurHookEntries(config) {
|
|
216
|
+
let removed = 0;
|
|
217
|
+
if (!config.hooks || typeof config.hooks !== 'object') return 0;
|
|
218
|
+
for (const event of Object.keys(config.hooks)) {
|
|
219
|
+
const groups = config.hooks[event];
|
|
220
|
+
if (!Array.isArray(groups)) continue;
|
|
221
|
+
for (const group of groups) {
|
|
222
|
+
if (!group || !Array.isArray(group.hooks)) continue;
|
|
223
|
+
const kept = group.hooks.filter((h) => !(h && isOurs(h.command)));
|
|
224
|
+
removed += group.hooks.length - kept.length;
|
|
225
|
+
group.hooks = kept;
|
|
226
|
+
}
|
|
227
|
+
config.hooks[event] = groups.filter((g) => g && Array.isArray(g.hooks) && g.hooks.length > 0);
|
|
228
|
+
if (config.hooks[event].length === 0) delete config.hooks[event];
|
|
229
|
+
}
|
|
230
|
+
if (Object.keys(config.hooks).length === 0) delete config.hooks;
|
|
231
|
+
return removed;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Remove OUR entries from .codex/hooks.json; delete the file only when nothing
|
|
235
|
+
// of the user's is left in it.
|
|
236
|
+
function removeCodexHooks(projectRoot) {
|
|
237
|
+
const config = readCodexHooks(projectRoot);
|
|
238
|
+
if (!config) return 0;
|
|
239
|
+
const removed = removeOurHookEntries(config);
|
|
240
|
+
if (removed === 0) return 0;
|
|
241
|
+
const file = path.join(projectRoot, CODEX_HOOKS_FILE);
|
|
242
|
+
if (Object.keys(config).length === 0) {
|
|
243
|
+
fs.rmSync(file);
|
|
244
|
+
try {
|
|
245
|
+
fs.rmdirSync(path.dirname(file));
|
|
246
|
+
} catch {
|
|
247
|
+
/* .codex/ still holds other files */
|
|
248
|
+
}
|
|
249
|
+
} else {
|
|
250
|
+
writeCodexHooks(projectRoot, config);
|
|
251
|
+
}
|
|
169
252
|
return removed;
|
|
170
253
|
}
|
|
171
254
|
|
|
@@ -173,6 +256,8 @@ module.exports = {
|
|
|
173
256
|
installHooks,
|
|
174
257
|
removeHooks,
|
|
175
258
|
HOOKS_DIR_MARKER,
|
|
259
|
+
CODEX_HOOKS_FILE,
|
|
260
|
+
GIT_GUARDRAILS_SCRIPT,
|
|
176
261
|
GIT_GUARDRAILS_CMD,
|
|
177
262
|
STATUSLINE_CMD,
|
|
178
263
|
SESSION_COST_CMD,
|
package/lib/init.js
CHANGED
|
@@ -4,7 +4,7 @@ const { ensureDir, copyDir, writeFile, upsertDelimitedBlock, log, listFilesRecur
|
|
|
4
4
|
const { selectLanguage } = require('./prompts');
|
|
5
5
|
const { generateWrappers } = require('./wrappers');
|
|
6
6
|
const { installMCPs } = require('./mcp');
|
|
7
|
-
const { installHooks } = require('./hooks');
|
|
7
|
+
const { installHooks, STATUSLINE_CMD } = require('./hooks');
|
|
8
8
|
const { generateAgents } = require('./agents');
|
|
9
9
|
const { migrate: migrateGsd } = require('./migrate-gsd');
|
|
10
10
|
const { migrate: migrateSkills } = require('./migrate-skills');
|
|
@@ -408,13 +408,19 @@ async function run({ force = false, lang = null, mode = 'init', profile = null,
|
|
|
408
408
|
const mcpResults = installMCPs(projectRoot);
|
|
409
409
|
console.log(` ${mcpResults.added} configured, ${mcpResults.skipped} already present\n`);
|
|
410
410
|
|
|
411
|
-
// 9.2. Reconcile dev-workflow hooks + statusline
|
|
412
|
-
//
|
|
411
|
+
// 9.2. Reconcile dev-workflow hooks + statusline into .claude/settings.json and the
|
|
412
|
+
// git guardrail into .codex/hooks.json (marker-based, merge-aware: refreshes our
|
|
413
|
+
// entries, never touches user-defined hooks/statusLine).
|
|
413
414
|
console.log(' Hooks & statusline:');
|
|
414
415
|
const hookResults = installHooks(projectRoot);
|
|
415
|
-
console.log(` git-guardrails: ${hookResults.hooks}, session-cost: ${hookResults.sessionCost}, statusline: ${hookResults.statusLine}\n`);
|
|
416
|
+
console.log(` git-guardrails: ${hookResults.hooks}, session-cost: ${hookResults.sessionCost}, statusline: ${hookResults.statusLine}, codex git-guardrails: ${hookResults.codex}\n`);
|
|
417
|
+
if (hookResults.codex === 'skipped-malformed') {
|
|
418
|
+
console.log(' Note: .codex/hooks.json is not valid JSON; the Codex git guardrail was not installed.\n');
|
|
419
|
+
} else if (hookResults.codex !== 'unchanged') {
|
|
420
|
+
console.log(' Note: Codex runs .codex/hooks.json only after you trust the project and review the hook in Codex.\n');
|
|
421
|
+
}
|
|
416
422
|
if (hookResults.statusLine === 'skipped-user') {
|
|
417
|
-
console.log(
|
|
423
|
+
console.log(` Note: a custom statusLine is set; to show dev-workflow state add \`${STATUSLINE_CMD}\`.\n`);
|
|
418
424
|
}
|
|
419
425
|
|
|
420
426
|
// 9.5. Persist install state for diagnostics and repair.
|
package/lib/uninstall.js
CHANGED
|
@@ -104,8 +104,8 @@ function run() {
|
|
|
104
104
|
}
|
|
105
105
|
console.log();
|
|
106
106
|
|
|
107
|
-
// 4.5. Remove dev-workflow hooks + statusline from .claude/settings.json
|
|
108
|
-
// (marker-based; user-defined hooks/statusLine are left intact). The hook
|
|
107
|
+
// 4.5. Remove dev-workflow hooks + statusline from .claude/settings.json and
|
|
108
|
+
// .codex/hooks.json (marker-based; user-defined hooks/statusLine are left intact). The hook
|
|
109
109
|
// scripts themselves were removed with .dw/scripts/ above.
|
|
110
110
|
console.log(' Hooks & statusline:');
|
|
111
111
|
try {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@brunosps00/dev-workflow",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.6.0",
|
|
4
4
|
"description": "AI-driven development workflow commands for any project. Scaffolds a complete PRD-to-PR pipeline with multi-platform AI assistant support.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"dev-workflow": "./bin/dev-workflow.js"
|
|
@@ -50,8 +50,9 @@ it is the boundary working. An agent that cannot destroy work cannot be talked i
|
|
|
50
50
|
|
|
51
51
|
**How this is enforced:** partly by `.dw/scripts/hooks/git-guardrails.mjs`, a `PreToolUse` hook that denies
|
|
52
52
|
these patterns on Bash. The hook is **one implementation, not the source of the rule**, and it has two
|
|
53
|
-
known limits: it only covers Bash under Claude Code
|
|
54
|
-
|
|
53
|
+
known limits: it only covers Bash under Claude Code and Codex (Codex runs it only once the project and the
|
|
54
|
+
hook are trusted), and it **fails open** — a parse error or a runtime fault allows the command through. So
|
|
55
|
+
the rule binds even where the hook does not run.
|
|
55
56
|
|
|
56
57
|
Two operations are deliberately left out of the hook and live only here: `rebase` and `commit --amend`.
|
|
57
58
|
Both are routine on unpushed work, and a command line cannot tell pushed from unpushed. Denying them would
|
|
@@ -49,8 +49,9 @@ funcionando. Um agente que não consegue destruir trabalho não pode ser convenc
|
|
|
49
49
|
|
|
50
50
|
**Como isso é verificado:** em parte pelo `.dw/scripts/hooks/git-guardrails.mjs`, um hook `PreToolUse` que
|
|
51
51
|
nega esses padrões no Bash. O hook é **uma implementação, não a fonte da regra**, e tem dois limites
|
|
52
|
-
conhecidos: só cobre Bash no Claude Code
|
|
53
|
-
|
|
52
|
+
conhecidos: só cobre Bash no Claude Code e no Codex (o Codex só o roda depois que o projeto e o hook são
|
|
53
|
+
marcados como confiáveis), e **falha aberto** — erro de parse ou exceção em runtime deixa o comando passar.
|
|
54
|
+
Então a regra obriga mesmo onde o hook não roda.
|
|
54
55
|
|
|
55
56
|
Duas operações ficam de fora do hook de propósito e vivem só aqui: `rebase` e `commit --amend`. As duas são
|
|
56
57
|
rotina em trabalho não pushado, e uma linha de comando não distingue pushado de não pushado. Negá-las
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* dev-workflow git guardrails —
|
|
3
|
+
* dev-workflow git guardrails — PreToolUse hook (matcher: Bash) for Claude Code
|
|
4
|
+
* (.claude/settings.json) and Codex (.codex/hooks.json); both send the same payload.
|
|
4
5
|
*
|
|
5
6
|
* Blocks irreversible git commands (force push, hard reset, clean -f, branch
|
|
6
7
|
* deletion, remote branch deletion). Everything else is allowed.
|
|
@@ -76,7 +77,7 @@ export function evaluatePayload(input) {
|
|
|
76
77
|
permissionDecision: 'deny',
|
|
77
78
|
permissionDecisionReason:
|
|
78
79
|
`Blocked by dev-workflow git guardrails: ${rule.why}. ` +
|
|
79
|
-
`If this is intentional, run it yourself outside the agent, or adjust .claude/settings.json hooks.`,
|
|
80
|
+
`If this is intentional, run it yourself outside the agent, or adjust the hook in .claude/settings.json or .codex/hooks.json.`,
|
|
80
81
|
},
|
|
81
82
|
};
|
|
82
83
|
}
|
|
@@ -3,9 +3,11 @@
|
|
|
3
3
|
* dev-workflow statusline — Claude Code statusLine command.
|
|
4
4
|
*
|
|
5
5
|
* Prints one line: git branch · dev-workflow active spec (if any) · minimalism
|
|
6
|
-
* mode. Reads the session payload (JSON on stdin)
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* mode. Reads the session payload (JSON on stdin): the branch comes from the
|
|
7
|
+
* current dir, the .dw/ state from the project dir, since the shell may sit in a
|
|
8
|
+
* subdirectory. Falls back to $CLAUDE_PROJECT_DIR, then process.cwd(). Fails
|
|
9
|
+
* SAFE — any error prints a minimal line so the statusline never breaks the
|
|
10
|
+
* session.
|
|
9
11
|
*
|
|
10
12
|
* Minimalism mode is read from .dw/minimalism.json (see the dw-minimalism skill).
|
|
11
13
|
*/
|
|
@@ -93,23 +95,24 @@ function activeSpec(cwd) {
|
|
|
93
95
|
|
|
94
96
|
async function main() {
|
|
95
97
|
let cwd = process.cwd();
|
|
98
|
+
let root = process.env.CLAUDE_PROJECT_DIR || '';
|
|
96
99
|
try {
|
|
97
100
|
const payload = JSON.parse(await readStdin());
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
cwd;
|
|
101
|
+
const workspace = (payload && payload.workspace) || {};
|
|
102
|
+
cwd = workspace.current_dir || (payload && payload.cwd) || cwd;
|
|
103
|
+
root = workspace.project_dir || root;
|
|
102
104
|
} catch {
|
|
103
|
-
/* use process.cwd() */
|
|
105
|
+
/* use $CLAUDE_PROJECT_DIR / process.cwd() */
|
|
104
106
|
}
|
|
107
|
+
root = root || cwd;
|
|
105
108
|
|
|
106
109
|
const parts = [];
|
|
107
110
|
const branch = gitBranch(cwd);
|
|
108
111
|
if (branch) parts.push(`⎇ ${branch}`);
|
|
109
|
-
const spec = activeSpec(
|
|
112
|
+
const spec = activeSpec(root);
|
|
110
113
|
if (spec) parts.push(spec);
|
|
111
|
-
parts.push(`min:${minimalismMode(
|
|
112
|
-
const cost = costToday(
|
|
114
|
+
parts.push(`min:${minimalismMode(root)}`);
|
|
115
|
+
const cost = costToday(root);
|
|
113
116
|
if (cost) parts.push(cost);
|
|
114
117
|
|
|
115
118
|
process.stdout.write(`dw · ${parts.join(' · ')}`);
|