@haus-tech/haus-workflow 1.0.0 → 1.1.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/CHANGELOG.md +6 -0
- package/dist/cli.js +113 -43
- package/library/catalog/manifest.json +3 -3
- package/library/global/settings-fragments/hooks.json +7 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.1.0](https://github.com/WeAreHausTech/haus-workflow/compare/v1.0.0...v1.1.0) (2026-07-02)
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
|
|
7
|
+
- **decisions:** add local pre-PR ADR gate (decisions guard hook) ([#163](https://github.com/WeAreHausTech/haus-workflow/issues/163)) ([2f2d8fb](https://github.com/WeAreHausTech/haus-workflow/commit/2f2d8fb0840389a9b4d1631b686da996064fc8e6))
|
|
8
|
+
|
|
3
9
|
## [1.0.0](https://github.com/WeAreHausTech/haus-workflow/compare/v0.32.1...v1.0.0) (2026-07-02)
|
|
4
10
|
|
|
5
11
|
### ⚠ BREAKING CHANGES
|
package/dist/cli.js
CHANGED
|
@@ -1538,11 +1538,11 @@ function reconcileManagedRules(existing, prevTracked, newRules) {
|
|
|
1538
1538
|
function mergeDenyRules(settings, rules) {
|
|
1539
1539
|
const existingDeny = settings.permissions?.deny ?? [];
|
|
1540
1540
|
const trackedDeny = settings._haus?.denyRules ?? [];
|
|
1541
|
-
const { rules:
|
|
1541
|
+
const { rules: deny, tracked, added } = reconcileManagedRules(existingDeny, trackedDeny, rules);
|
|
1542
1542
|
const updated = { ...settings };
|
|
1543
1543
|
updated.permissions = {
|
|
1544
1544
|
...settings.permissions ?? {},
|
|
1545
|
-
deny
|
|
1545
|
+
deny
|
|
1546
1546
|
};
|
|
1547
1547
|
updated._haus = {
|
|
1548
1548
|
hooks: settings._haus?.hooks ?? [],
|
|
@@ -1805,6 +1805,13 @@ var PROJECT_HOOK_FRAGMENTS = [
|
|
|
1805
1805
|
matcher: "Bash",
|
|
1806
1806
|
command: "haus guard bash --from-hook"
|
|
1807
1807
|
},
|
|
1808
|
+
{
|
|
1809
|
+
id: "haus.decisions-guard",
|
|
1810
|
+
gate: "keep",
|
|
1811
|
+
event: "PreToolUse",
|
|
1812
|
+
matcher: "Bash",
|
|
1813
|
+
command: "haus decisions guard --from-hook"
|
|
1814
|
+
},
|
|
1808
1815
|
{
|
|
1809
1816
|
id: "haus.decisions-suggest",
|
|
1810
1817
|
gate: "keep",
|
|
@@ -1910,6 +1917,10 @@ var CANONICAL_HOOKS = {
|
|
|
1910
1917
|
{
|
|
1911
1918
|
matcher: "Bash",
|
|
1912
1919
|
hooks: [{ type: "command", command: "haus guard bash --from-hook" }]
|
|
1920
|
+
},
|
|
1921
|
+
{
|
|
1922
|
+
matcher: "Bash",
|
|
1923
|
+
hooks: [{ type: "command", command: "haus decisions guard --from-hook" }]
|
|
1913
1924
|
}
|
|
1914
1925
|
],
|
|
1915
1926
|
Stop: [
|
|
@@ -3053,8 +3064,59 @@ async function runClone(url, opts = {}) {
|
|
|
3053
3064
|
log(`\u2713 cloned ${url} \u2192 ${target}`);
|
|
3054
3065
|
}
|
|
3055
3066
|
|
|
3056
|
-
// src/
|
|
3057
|
-
import
|
|
3067
|
+
// src/claude/hook-io.ts
|
|
3068
|
+
import { readFileSync as readFileSync2 } from "node:fs";
|
|
3069
|
+
|
|
3070
|
+
// src/utils/audit-checks.ts
|
|
3071
|
+
function isRecord(v) {
|
|
3072
|
+
return typeof v === "object" && v !== null;
|
|
3073
|
+
}
|
|
3074
|
+
|
|
3075
|
+
// src/claude/hook-io.ts
|
|
3076
|
+
function stdin() {
|
|
3077
|
+
try {
|
|
3078
|
+
if (process.stdin.isTTY) return "";
|
|
3079
|
+
return readFileSync2(0, "utf8");
|
|
3080
|
+
} catch {
|
|
3081
|
+
return "";
|
|
3082
|
+
}
|
|
3083
|
+
}
|
|
3084
|
+
function readHookPayload() {
|
|
3085
|
+
const raw = stdin();
|
|
3086
|
+
if (!raw) return { payload: {}, malformed: false };
|
|
3087
|
+
try {
|
|
3088
|
+
const parsed = JSON.parse(raw);
|
|
3089
|
+
return { payload: isRecord(parsed) ? parsed : {}, malformed: false };
|
|
3090
|
+
} catch {
|
|
3091
|
+
return { payload: {}, malformed: true };
|
|
3092
|
+
}
|
|
3093
|
+
}
|
|
3094
|
+
function emitPreToolUseDeny(reason) {
|
|
3095
|
+
log(
|
|
3096
|
+
JSON.stringify({
|
|
3097
|
+
hookSpecificOutput: {
|
|
3098
|
+
hookEventName: "PreToolUse",
|
|
3099
|
+
permissionDecision: "deny",
|
|
3100
|
+
permissionDecisionReason: reason
|
|
3101
|
+
}
|
|
3102
|
+
})
|
|
3103
|
+
);
|
|
3104
|
+
}
|
|
3105
|
+
|
|
3106
|
+
// src/decisions/base-ref.ts
|
|
3107
|
+
init_exec();
|
|
3108
|
+
async function resolveBaseRef(root) {
|
|
3109
|
+
const symbolic = await runGit(["symbolic-ref", "refs/remotes/origin/HEAD"], { cwd: root });
|
|
3110
|
+
if (symbolic.exitCode === 0) {
|
|
3111
|
+
const ref = symbolic.stdout.trim().replace(/^refs\/remotes\//, "");
|
|
3112
|
+
if (ref) return ref;
|
|
3113
|
+
}
|
|
3114
|
+
for (const candidate of ["origin/main", "origin/master", "main", "master"]) {
|
|
3115
|
+
const check = await runGit(["rev-parse", "--verify", "--quiet", candidate], { cwd: root });
|
|
3116
|
+
if (check.exitCode === 0) return candidate;
|
|
3117
|
+
}
|
|
3118
|
+
return void 0;
|
|
3119
|
+
}
|
|
3058
3120
|
|
|
3059
3121
|
// src/decisions/check.ts
|
|
3060
3122
|
import path21 from "node:path";
|
|
@@ -3303,6 +3365,46 @@ async function listDecisionNumbers(decisionsDir) {
|
|
|
3303
3365
|
return entries.map((name) => decisionNumberFromFile(name)).filter((n) => n != null).map((n) => Number(n));
|
|
3304
3366
|
}
|
|
3305
3367
|
|
|
3368
|
+
// src/commands/decisions-guard.ts
|
|
3369
|
+
init_exec();
|
|
3370
|
+
var PR_CREATE_PATTERN = /\bgh\s+pr\s+create\b/;
|
|
3371
|
+
function readPayload(options) {
|
|
3372
|
+
if (options.stdinPayload != null) {
|
|
3373
|
+
try {
|
|
3374
|
+
const parsed = JSON.parse(options.stdinPayload);
|
|
3375
|
+
return { payload: isRecord(parsed) ? parsed : {}, malformed: false };
|
|
3376
|
+
} catch {
|
|
3377
|
+
return { payload: {}, malformed: true };
|
|
3378
|
+
}
|
|
3379
|
+
}
|
|
3380
|
+
return readHookPayload();
|
|
3381
|
+
}
|
|
3382
|
+
async function runDecisionsGuard(options = {}) {
|
|
3383
|
+
const { payload, malformed } = readPayload(options);
|
|
3384
|
+
if (malformed) {
|
|
3385
|
+
emitPreToolUseDeny("Malformed hook payload");
|
|
3386
|
+
return;
|
|
3387
|
+
}
|
|
3388
|
+
const toolInput = isRecord(payload.tool_input) ? payload.tool_input : {};
|
|
3389
|
+
const command = String(toolInput.command ?? "");
|
|
3390
|
+
if (!PR_CREATE_PATTERN.test(command)) return;
|
|
3391
|
+
const root = process.cwd();
|
|
3392
|
+
const baseRef = await resolveBaseRef(root);
|
|
3393
|
+
if (!baseRef) return;
|
|
3394
|
+
const range = `${baseRef}...HEAD`;
|
|
3395
|
+
const logRange = `${baseRef}..HEAD`;
|
|
3396
|
+
const commitMessages = (await runGit(["log", "--format=%B", logRange], { cwd: root })).stdout;
|
|
3397
|
+
const result = await runDecisionsCheck(root, { range, commitMessages });
|
|
3398
|
+
if (result.triggered && !result.satisfied) {
|
|
3399
|
+
emitPreToolUseDeny(
|
|
3400
|
+
`I didn't run that \u2014 this change needs an ADR before opening a PR (${result.reasons.join("; ")}). Run haus decisions suggest, add the decision under docs/decisions/, then retry.`
|
|
3401
|
+
);
|
|
3402
|
+
}
|
|
3403
|
+
}
|
|
3404
|
+
|
|
3405
|
+
// src/commands/decisions.ts
|
|
3406
|
+
import path22 from "node:path";
|
|
3407
|
+
|
|
3306
3408
|
// src/decisions/next-number.ts
|
|
3307
3409
|
async function nextDecisionNumber(root) {
|
|
3308
3410
|
const decisionsDir = await resolveDecisionsDir(root);
|
|
@@ -3511,11 +3613,6 @@ async function auditDecisionsLayout(root) {
|
|
|
3511
3613
|
// src/scanner/scan-project.ts
|
|
3512
3614
|
import path27 from "node:path";
|
|
3513
3615
|
|
|
3514
|
-
// src/utils/audit-checks.ts
|
|
3515
|
-
function isRecord(v) {
|
|
3516
|
-
return typeof v === "object" && v !== null;
|
|
3517
|
-
}
|
|
3518
|
-
|
|
3519
3616
|
// src/utils/versions.ts
|
|
3520
3617
|
import semver from "semver";
|
|
3521
3618
|
function normalizeVersion(version) {
|
|
@@ -4411,9 +4508,6 @@ async function runFetchRefs(options) {
|
|
|
4411
4508
|
}
|
|
4412
4509
|
}
|
|
4413
4510
|
|
|
4414
|
-
// src/commands/guard.ts
|
|
4415
|
-
import { readFileSync as readFileSync2 } from "node:fs";
|
|
4416
|
-
|
|
4417
4511
|
// src/security/guard-bash.ts
|
|
4418
4512
|
function matchesDenyToken(command, denyPhrase) {
|
|
4419
4513
|
if (denyPhrase === "sudo") {
|
|
@@ -4438,50 +4532,25 @@ function guardFileAccess(candidate) {
|
|
|
4438
4532
|
}
|
|
4439
4533
|
|
|
4440
4534
|
// src/commands/guard.ts
|
|
4441
|
-
function stdin() {
|
|
4442
|
-
try {
|
|
4443
|
-
if (process.stdin.isTTY) return "";
|
|
4444
|
-
return readFileSync2(0, "utf8");
|
|
4445
|
-
} catch {
|
|
4446
|
-
return "";
|
|
4447
|
-
}
|
|
4448
|
-
}
|
|
4449
|
-
function deny(reason) {
|
|
4450
|
-
log(
|
|
4451
|
-
JSON.stringify({
|
|
4452
|
-
hookSpecificOutput: {
|
|
4453
|
-
hookEventName: "PreToolUse",
|
|
4454
|
-
permissionDecision: "deny",
|
|
4455
|
-
permissionDecisionReason: reason
|
|
4456
|
-
}
|
|
4457
|
-
})
|
|
4458
|
-
);
|
|
4459
|
-
}
|
|
4460
4535
|
async function runGuard(kind, _options) {
|
|
4461
|
-
const
|
|
4462
|
-
|
|
4463
|
-
|
|
4464
|
-
|
|
4465
|
-
const parsed = JSON.parse(raw);
|
|
4466
|
-
if (isRecord(parsed)) payload = parsed;
|
|
4467
|
-
} catch {
|
|
4468
|
-
deny("Malformed hook payload");
|
|
4469
|
-
return;
|
|
4470
|
-
}
|
|
4536
|
+
const { payload, malformed } = readHookPayload();
|
|
4537
|
+
if (malformed) {
|
|
4538
|
+
emitPreToolUseDeny("Malformed hook payload");
|
|
4539
|
+
return;
|
|
4471
4540
|
}
|
|
4472
4541
|
const toolInput = isRecord(payload.tool_input) ? payload.tool_input : {};
|
|
4473
4542
|
if (kind === "file-access") {
|
|
4474
4543
|
const candidate = String(toolInput.path ?? toolInput.file_path ?? "");
|
|
4475
4544
|
const reason2 = guardFileAccess(candidate);
|
|
4476
4545
|
if (reason2) {
|
|
4477
|
-
|
|
4546
|
+
emitPreToolUseDeny(reason2);
|
|
4478
4547
|
}
|
|
4479
4548
|
return;
|
|
4480
4549
|
}
|
|
4481
4550
|
const command = String(toolInput.command ?? "");
|
|
4482
4551
|
const reason = guardBash(command);
|
|
4483
4552
|
if (reason) {
|
|
4484
|
-
|
|
4553
|
+
emitPreToolUseDeny(reason);
|
|
4485
4554
|
}
|
|
4486
4555
|
}
|
|
4487
4556
|
|
|
@@ -7101,6 +7170,7 @@ function registerDecisionsCommand(name) {
|
|
|
7101
7170
|
(opts) => runDecisions("suggest", void 0, opts)
|
|
7102
7171
|
);
|
|
7103
7172
|
cmd.command("next-number").action(() => runDecisions("next-number"));
|
|
7173
|
+
cmd.command("guard").option("--from-hook", "Emit JSON for Claude Code PreToolUse hook").action((opts) => runDecisionsGuard(opts));
|
|
7104
7174
|
cmd.command("validate").argument("[path]", "Decision markdown file").action((filePath) => runDecisions("validate", filePath));
|
|
7105
7175
|
}
|
|
7106
7176
|
registerDecisionsCommand("decisions");
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "3.
|
|
2
|
+
"version": "3.3.0",
|
|
3
3
|
"items": [
|
|
4
4
|
{
|
|
5
5
|
"id": "haus.agentic-workflow-standard",
|
|
@@ -1334,7 +1334,7 @@
|
|
|
1334
1334
|
"ecosystem": "general",
|
|
1335
1335
|
"installMode": "copy-selected",
|
|
1336
1336
|
"tokenEstimate": 120,
|
|
1337
|
-
"default":
|
|
1337
|
+
"default": true,
|
|
1338
1338
|
"reviewStatus": "approved",
|
|
1339
1339
|
"riskLevel": "low"
|
|
1340
1340
|
},
|
|
@@ -1353,7 +1353,7 @@
|
|
|
1353
1353
|
"ecosystem": "general",
|
|
1354
1354
|
"tokenEstimate": 650,
|
|
1355
1355
|
"tokenBudget": 900,
|
|
1356
|
-
"default":
|
|
1356
|
+
"default": true,
|
|
1357
1357
|
"reviewStatus": "approved",
|
|
1358
1358
|
"riskLevel": "low"
|
|
1359
1359
|
},
|
|
@@ -15,6 +15,13 @@
|
|
|
15
15
|
"matcher": "Bash",
|
|
16
16
|
"command": "haus guard bash --from-hook"
|
|
17
17
|
},
|
|
18
|
+
{
|
|
19
|
+
"id": "hook.decisions.guard",
|
|
20
|
+
"gate": "keep",
|
|
21
|
+
"event": "PreToolUse",
|
|
22
|
+
"matcher": "Bash",
|
|
23
|
+
"command": "haus decisions guard --from-hook"
|
|
24
|
+
},
|
|
18
25
|
{
|
|
19
26
|
"id": "hook.decisions.suggest",
|
|
20
27
|
"gate": "keep",
|