@haus-tech/haus-workflow 1.0.0 → 1.1.1
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 +8 -0
- package/dist/cli.js +119 -54
- package/library/catalog/manifest.json +1108 -108
- package/library/catalog/validation-rules.json +5 -1
- package/library/global/settings-fragments/hooks.json +7 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.1.1](https://github.com/WeAreHausTech/haus-workflow/compare/v1.1.0...v1.1.1) (2026-07-03)
|
|
4
|
+
|
|
5
|
+
## [1.1.0](https://github.com/WeAreHausTech/haus-workflow/compare/v1.0.0...v1.1.0) (2026-07-02)
|
|
6
|
+
|
|
7
|
+
### Features
|
|
8
|
+
|
|
9
|
+
- **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))
|
|
10
|
+
|
|
3
11
|
## [1.0.0](https://github.com/WeAreHausTech/haus-workflow/compare/v0.32.1...v1.0.0) (2026-07-02)
|
|
4
12
|
|
|
5
13
|
### ⚠ BREAKING CHANGES
|
package/dist/cli.js
CHANGED
|
@@ -361,7 +361,11 @@ var validation_rules_default = {
|
|
|
361
361
|
"stripe",
|
|
362
362
|
"qliro",
|
|
363
363
|
"supabase",
|
|
364
|
-
"payments"
|
|
364
|
+
"payments",
|
|
365
|
+
"myid",
|
|
366
|
+
"cgi",
|
|
367
|
+
"collection2",
|
|
368
|
+
"deployer-php"
|
|
365
369
|
],
|
|
366
370
|
alwaysAllowedTags: [
|
|
367
371
|
"haus",
|
|
@@ -1538,11 +1542,11 @@ function reconcileManagedRules(existing, prevTracked, newRules) {
|
|
|
1538
1542
|
function mergeDenyRules(settings, rules) {
|
|
1539
1543
|
const existingDeny = settings.permissions?.deny ?? [];
|
|
1540
1544
|
const trackedDeny = settings._haus?.denyRules ?? [];
|
|
1541
|
-
const { rules:
|
|
1545
|
+
const { rules: deny, tracked, added } = reconcileManagedRules(existingDeny, trackedDeny, rules);
|
|
1542
1546
|
const updated = { ...settings };
|
|
1543
1547
|
updated.permissions = {
|
|
1544
1548
|
...settings.permissions ?? {},
|
|
1545
|
-
deny
|
|
1549
|
+
deny
|
|
1546
1550
|
};
|
|
1547
1551
|
updated._haus = {
|
|
1548
1552
|
hooks: settings._haus?.hooks ?? [],
|
|
@@ -1805,6 +1809,13 @@ var PROJECT_HOOK_FRAGMENTS = [
|
|
|
1805
1809
|
matcher: "Bash",
|
|
1806
1810
|
command: "haus guard bash --from-hook"
|
|
1807
1811
|
},
|
|
1812
|
+
{
|
|
1813
|
+
id: "haus.decisions-guard",
|
|
1814
|
+
gate: "keep",
|
|
1815
|
+
event: "PreToolUse",
|
|
1816
|
+
matcher: "Bash",
|
|
1817
|
+
command: "haus decisions guard --from-hook"
|
|
1818
|
+
},
|
|
1808
1819
|
{
|
|
1809
1820
|
id: "haus.decisions-suggest",
|
|
1810
1821
|
gate: "keep",
|
|
@@ -1910,6 +1921,10 @@ var CANONICAL_HOOKS = {
|
|
|
1910
1921
|
{
|
|
1911
1922
|
matcher: "Bash",
|
|
1912
1923
|
hooks: [{ type: "command", command: "haus guard bash --from-hook" }]
|
|
1924
|
+
},
|
|
1925
|
+
{
|
|
1926
|
+
matcher: "Bash",
|
|
1927
|
+
hooks: [{ type: "command", command: "haus decisions guard --from-hook" }]
|
|
1913
1928
|
}
|
|
1914
1929
|
],
|
|
1915
1930
|
Stop: [
|
|
@@ -3053,8 +3068,59 @@ async function runClone(url, opts = {}) {
|
|
|
3053
3068
|
log(`\u2713 cloned ${url} \u2192 ${target}`);
|
|
3054
3069
|
}
|
|
3055
3070
|
|
|
3056
|
-
// src/
|
|
3057
|
-
import
|
|
3071
|
+
// src/claude/hook-io.ts
|
|
3072
|
+
import { readFileSync as readFileSync2 } from "node:fs";
|
|
3073
|
+
|
|
3074
|
+
// src/utils/audit-checks.ts
|
|
3075
|
+
function isRecord(v) {
|
|
3076
|
+
return typeof v === "object" && v !== null;
|
|
3077
|
+
}
|
|
3078
|
+
|
|
3079
|
+
// src/claude/hook-io.ts
|
|
3080
|
+
function stdin() {
|
|
3081
|
+
try {
|
|
3082
|
+
if (process.stdin.isTTY) return "";
|
|
3083
|
+
return readFileSync2(0, "utf8");
|
|
3084
|
+
} catch {
|
|
3085
|
+
return "";
|
|
3086
|
+
}
|
|
3087
|
+
}
|
|
3088
|
+
function readHookPayload() {
|
|
3089
|
+
const raw = stdin();
|
|
3090
|
+
if (!raw) return { payload: {}, malformed: false };
|
|
3091
|
+
try {
|
|
3092
|
+
const parsed = JSON.parse(raw);
|
|
3093
|
+
return { payload: isRecord(parsed) ? parsed : {}, malformed: false };
|
|
3094
|
+
} catch {
|
|
3095
|
+
return { payload: {}, malformed: true };
|
|
3096
|
+
}
|
|
3097
|
+
}
|
|
3098
|
+
function emitPreToolUseDeny(reason) {
|
|
3099
|
+
log(
|
|
3100
|
+
JSON.stringify({
|
|
3101
|
+
hookSpecificOutput: {
|
|
3102
|
+
hookEventName: "PreToolUse",
|
|
3103
|
+
permissionDecision: "deny",
|
|
3104
|
+
permissionDecisionReason: reason
|
|
3105
|
+
}
|
|
3106
|
+
})
|
|
3107
|
+
);
|
|
3108
|
+
}
|
|
3109
|
+
|
|
3110
|
+
// src/decisions/base-ref.ts
|
|
3111
|
+
init_exec();
|
|
3112
|
+
async function resolveBaseRef(root) {
|
|
3113
|
+
const symbolic = await runGit(["symbolic-ref", "refs/remotes/origin/HEAD"], { cwd: root });
|
|
3114
|
+
if (symbolic.exitCode === 0) {
|
|
3115
|
+
const ref = symbolic.stdout.trim().replace(/^refs\/remotes\//, "");
|
|
3116
|
+
if (ref) return ref;
|
|
3117
|
+
}
|
|
3118
|
+
for (const candidate of ["origin/main", "origin/master", "main", "master"]) {
|
|
3119
|
+
const check = await runGit(["rev-parse", "--verify", "--quiet", candidate], { cwd: root });
|
|
3120
|
+
if (check.exitCode === 0) return candidate;
|
|
3121
|
+
}
|
|
3122
|
+
return void 0;
|
|
3123
|
+
}
|
|
3058
3124
|
|
|
3059
3125
|
// src/decisions/check.ts
|
|
3060
3126
|
import path21 from "node:path";
|
|
@@ -3303,6 +3369,46 @@ async function listDecisionNumbers(decisionsDir) {
|
|
|
3303
3369
|
return entries.map((name) => decisionNumberFromFile(name)).filter((n) => n != null).map((n) => Number(n));
|
|
3304
3370
|
}
|
|
3305
3371
|
|
|
3372
|
+
// src/commands/decisions-guard.ts
|
|
3373
|
+
init_exec();
|
|
3374
|
+
var PR_CREATE_PATTERN = /\bgh\s+pr\s+create\b/;
|
|
3375
|
+
function readPayload(options) {
|
|
3376
|
+
if (options.stdinPayload != null) {
|
|
3377
|
+
try {
|
|
3378
|
+
const parsed = JSON.parse(options.stdinPayload);
|
|
3379
|
+
return { payload: isRecord(parsed) ? parsed : {}, malformed: false };
|
|
3380
|
+
} catch {
|
|
3381
|
+
return { payload: {}, malformed: true };
|
|
3382
|
+
}
|
|
3383
|
+
}
|
|
3384
|
+
return readHookPayload();
|
|
3385
|
+
}
|
|
3386
|
+
async function runDecisionsGuard(options = {}) {
|
|
3387
|
+
const { payload, malformed } = readPayload(options);
|
|
3388
|
+
if (malformed) {
|
|
3389
|
+
emitPreToolUseDeny("Malformed hook payload");
|
|
3390
|
+
return;
|
|
3391
|
+
}
|
|
3392
|
+
const toolInput = isRecord(payload.tool_input) ? payload.tool_input : {};
|
|
3393
|
+
const command = String(toolInput.command ?? "");
|
|
3394
|
+
if (!PR_CREATE_PATTERN.test(command)) return;
|
|
3395
|
+
const root = process.cwd();
|
|
3396
|
+
const baseRef = await resolveBaseRef(root);
|
|
3397
|
+
if (!baseRef) return;
|
|
3398
|
+
const range = `${baseRef}...HEAD`;
|
|
3399
|
+
const logRange = `${baseRef}..HEAD`;
|
|
3400
|
+
const commitMessages = (await runGit(["log", "--format=%B", logRange], { cwd: root })).stdout;
|
|
3401
|
+
const result = await runDecisionsCheck(root, { range, commitMessages });
|
|
3402
|
+
if (result.triggered && !result.satisfied) {
|
|
3403
|
+
emitPreToolUseDeny(
|
|
3404
|
+
`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.`
|
|
3405
|
+
);
|
|
3406
|
+
}
|
|
3407
|
+
}
|
|
3408
|
+
|
|
3409
|
+
// src/commands/decisions.ts
|
|
3410
|
+
import path22 from "node:path";
|
|
3411
|
+
|
|
3306
3412
|
// src/decisions/next-number.ts
|
|
3307
3413
|
async function nextDecisionNumber(root) {
|
|
3308
3414
|
const decisionsDir = await resolveDecisionsDir(root);
|
|
@@ -3511,11 +3617,6 @@ async function auditDecisionsLayout(root) {
|
|
|
3511
3617
|
// src/scanner/scan-project.ts
|
|
3512
3618
|
import path27 from "node:path";
|
|
3513
3619
|
|
|
3514
|
-
// src/utils/audit-checks.ts
|
|
3515
|
-
function isRecord(v) {
|
|
3516
|
-
return typeof v === "object" && v !== null;
|
|
3517
|
-
}
|
|
3518
|
-
|
|
3519
3620
|
// src/utils/versions.ts
|
|
3520
3621
|
import semver from "semver";
|
|
3521
3622
|
function normalizeVersion(version) {
|
|
@@ -4411,9 +4512,6 @@ async function runFetchRefs(options) {
|
|
|
4411
4512
|
}
|
|
4412
4513
|
}
|
|
4413
4514
|
|
|
4414
|
-
// src/commands/guard.ts
|
|
4415
|
-
import { readFileSync as readFileSync2 } from "node:fs";
|
|
4416
|
-
|
|
4417
4515
|
// src/security/guard-bash.ts
|
|
4418
4516
|
function matchesDenyToken(command, denyPhrase) {
|
|
4419
4517
|
if (denyPhrase === "sudo") {
|
|
@@ -4438,50 +4536,25 @@ function guardFileAccess(candidate) {
|
|
|
4438
4536
|
}
|
|
4439
4537
|
|
|
4440
4538
|
// 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
4539
|
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
|
-
}
|
|
4540
|
+
const { payload, malformed } = readHookPayload();
|
|
4541
|
+
if (malformed) {
|
|
4542
|
+
emitPreToolUseDeny("Malformed hook payload");
|
|
4543
|
+
return;
|
|
4471
4544
|
}
|
|
4472
4545
|
const toolInput = isRecord(payload.tool_input) ? payload.tool_input : {};
|
|
4473
4546
|
if (kind === "file-access") {
|
|
4474
4547
|
const candidate = String(toolInput.path ?? toolInput.file_path ?? "");
|
|
4475
4548
|
const reason2 = guardFileAccess(candidate);
|
|
4476
4549
|
if (reason2) {
|
|
4477
|
-
|
|
4550
|
+
emitPreToolUseDeny(reason2);
|
|
4478
4551
|
}
|
|
4479
4552
|
return;
|
|
4480
4553
|
}
|
|
4481
4554
|
const command = String(toolInput.command ?? "");
|
|
4482
4555
|
const reason = guardBash(command);
|
|
4483
4556
|
if (reason) {
|
|
4484
|
-
|
|
4557
|
+
emitPreToolUseDeny(reason);
|
|
4485
4558
|
}
|
|
4486
4559
|
}
|
|
4487
4560
|
|
|
@@ -4652,16 +4725,7 @@ async function recommend(root, context, opts = {}) {
|
|
|
4652
4725
|
skip(item.id, "source-approval", "Source not approved", `source:${normSource}`);
|
|
4653
4726
|
continue;
|
|
4654
4727
|
}
|
|
4655
|
-
if (item.id === "haus.
|
|
4656
|
-
skip(
|
|
4657
|
-
item.id,
|
|
4658
|
-
"required-role-missing",
|
|
4659
|
-
"Required role missing: nx-monorepo",
|
|
4660
|
-
"role:nx-monorepo"
|
|
4661
|
-
);
|
|
4662
|
-
continue;
|
|
4663
|
-
}
|
|
4664
|
-
if (item.id === "haus.turbo-monorepo-patterns" && !roleSet.has("turbo-monorepo")) {
|
|
4728
|
+
if (item.id === "haus.turborepo-turborepo" && !roleSet.has("turbo-monorepo")) {
|
|
4665
4729
|
skip(
|
|
4666
4730
|
item.id,
|
|
4667
4731
|
"required-role-missing",
|
|
@@ -7101,6 +7165,7 @@ function registerDecisionsCommand(name) {
|
|
|
7101
7165
|
(opts) => runDecisions("suggest", void 0, opts)
|
|
7102
7166
|
);
|
|
7103
7167
|
cmd.command("next-number").action(() => runDecisions("next-number"));
|
|
7168
|
+
cmd.command("guard").option("--from-hook", "Emit JSON for Claude Code PreToolUse hook").action((opts) => runDecisionsGuard(opts));
|
|
7104
7169
|
cmd.command("validate").argument("[path]", "Decision markdown file").action((filePath) => runDecisions("validate", filePath));
|
|
7105
7170
|
}
|
|
7106
7171
|
registerDecisionsCommand("decisions");
|