@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 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: deny2, tracked, added } = reconcileManagedRules(existingDeny, trackedDeny, 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: deny2
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/commands/decisions.ts
3057
- import path22 from "node:path";
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 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
- }
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
- deny(reason2);
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
- deny(reason);
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.1",
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": false,
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": false,
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",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@haus-tech/haus-workflow",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Haus AI workflow CLI for Claude Code.",
5
5
  "type": "module",
6
6
  "bin": {