@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 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: deny2, tracked, added } = reconcileManagedRules(existingDeny, trackedDeny, 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: deny2
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/commands/decisions.ts
3057
- import path22 from "node:path";
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 raw = stdin();
4462
- let payload = {};
4463
- if (raw) {
4464
- try {
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
- deny(reason2);
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
- deny(reason);
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.nx21-monorepo-patterns" && !roleSet.has("nx-monorepo")) {
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");