@haus-tech/haus-workflow 5.5.0 → 5.6.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,17 @@
1
1
  # Changelog
2
2
 
3
+ ## [5.6.1](https://github.com/WeAreHausTech/haus-workflow/compare/v5.6.0...v5.6.1) (2026-09-16)
4
+
5
+ ### Bug Fixes
6
+
7
+ - **security:** allow gh pr merge and read-only gh verbs, deny the --admin bypass ([#562](https://github.com/WeAreHausTech/haus-workflow/issues/562)) ([00a8c95](https://github.com/WeAreHausTech/haus-workflow/commit/00a8c95dbc49dea5863d15b9812f038dfde036bb)), closes [#545](https://github.com/WeAreHausTech/haus-workflow/issues/545)
8
+
9
+ ## [5.6.0](https://github.com/WeAreHausTech/haus-workflow/compare/v5.5.0...v5.6.0) (2026-09-13)
10
+
11
+ ### Bug Fixes
12
+
13
+ - **poc:** what the second PoC run rubbed against ([#557](https://github.com/WeAreHausTech/haus-workflow/issues/557)) ([8b37668](https://github.com/WeAreHausTech/haus-workflow/commit/8b37668bfd0a9a99f7be32cd4863ead8bcf96b27))
14
+
3
15
  ## [5.5.0](https://github.com/WeAreHausTech/haus-workflow/compare/v5.4.0...v5.5.0) (2026-09-12)
4
16
 
5
17
  ### Features
package/dist/cli.js CHANGED
@@ -2942,6 +2942,7 @@ var PROFILES = {
2942
2942
  workflowTemplateRel: WORKFLOW_TEMPLATE_REL,
2943
2943
  writesCiGate: true,
2944
2944
  writesDecisionsSeed: true,
2945
+ runsDocsPass: true,
2945
2946
  droppedHookCommands: [],
2946
2947
  workflowConfig: "standard"
2947
2948
  },
@@ -2950,6 +2951,7 @@ var PROFILES = {
2950
2951
  workflowTemplateRel: "templates/agentic-workflow-poc.md",
2951
2952
  writesCiGate: false,
2952
2953
  writesDecisionsSeed: false,
2954
+ runsDocsPass: false,
2953
2955
  droppedHookCommands: ["haus decisions guard --from-hook", "haus decisions suggest --from-hook"],
2954
2956
  workflowConfig: "poc"
2955
2957
  }
@@ -3381,8 +3383,42 @@ var ALLOWED_SUBCOMMANDS = [
3381
3383
  "workspace worktree list",
3382
3384
  "workspace worktree doctor"
3383
3385
  ];
3386
+ var ALLOWED_COMMANDS = [
3387
+ // Merging the PR is the last step of the house loop, and where a deploy builds `main`'s
3388
+ // SHA it is a precondition of deploying at all. The auto mode classifier blocked it
3389
+ // mid-run with no rule available to pre-authorize it (#545). Its dangerous form —
3390
+ // `--admin`, the only way past branch protection and required reviews — is deny-tier in
3391
+ // `DENY_COMMANDS` and caught in any spelling by the runtime guard, which outranks this
3392
+ // rule. The repo's own branch protection stays the review gate.
3393
+ "gh pr merge",
3394
+ // Read-only verbs. `gh` is absent from Claude Code's built-in read-only command set
3395
+ // (`ls`, `cat`, `grep`, read-only `git`, …), so each of these otherwise costs a
3396
+ // classifier round-trip mid-run — on `gh run watch`, once per CI wait. Nothing under
3397
+ // these prefixes writes, so there is no dangerous form left to gate.
3398
+ "gh pr view",
3399
+ "gh pr list",
3400
+ "gh pr diff",
3401
+ "gh pr checks",
3402
+ "gh run list",
3403
+ "gh run view",
3404
+ "gh run watch",
3405
+ "gh issue view",
3406
+ "gh issue list",
3407
+ "gh repo view",
3408
+ "gh label list",
3409
+ // Prints who you are authenticated as; `gh auth token`, which prints the credential
3410
+ // itself, is a different leaf and stays out.
3411
+ "gh auth status",
3412
+ "gh search prs",
3413
+ "gh search issues"
3414
+ ];
3384
3415
  function buildAllowRules() {
3385
- return [...new Set(ALLOWED_SUBCOMMANDS.map((sub) => `Bash(haus ${sub}:*)`))];
3416
+ return [
3417
+ .../* @__PURE__ */ new Set([
3418
+ ...ALLOWED_SUBCOMMANDS.map((sub) => `Bash(haus ${sub}:*)`),
3419
+ ...ALLOWED_COMMANDS.map((command) => `Bash(${command}:*)`)
3420
+ ])
3421
+ ];
3386
3422
  }
3387
3423
 
3388
3424
  // src/install/settings-merge.ts
@@ -3720,6 +3756,9 @@ var DENY_COMMANDS = [
3720
3756
  // Skipping the hooks is a NEVER rule in WORKFLOW.md and was missing from every tier.
3721
3757
  "git commit --no-verify",
3722
3758
  "git push --no-verify",
3759
+ // `gh pr merge` is allow-tier so an unattended run can land its own PR; this is the
3760
+ // one form that merges past branch protection and required reviews (#545, ADR-0126).
3761
+ "gh pr merge --admin",
3723
3762
  ...RUNTIME_ONLY_DENY_COMMANDS,
3724
3763
  "npm publish",
3725
3764
  // Classic v1 syntax alongside the Berry `yarn npm publish` form.
@@ -7695,9 +7734,9 @@ async function readScannedMarkdown(root) {
7695
7734
  }
7696
7735
 
7697
7736
  // src/decisions/doctor.ts
7698
- init_fs();
7699
7737
  import path40 from "node:path";
7700
7738
  import fs38 from "fs-extra";
7739
+ init_fs();
7701
7740
 
7702
7741
  // src/decisions/check.ts
7703
7742
  init_fs();
@@ -8165,19 +8204,21 @@ async function listDecisionNumbers(decisionsDir) {
8165
8204
  }
8166
8205
 
8167
8206
  // src/decisions/doctor.ts
8168
- async function auditDecisionsLayout(root) {
8207
+ async function auditDecisionsLayout(root, profile) {
8169
8208
  const findings = [];
8170
8209
  const primary = path40.join(root, DECISIONS_TRIGGERS.decisionsDir);
8171
8210
  const legacy = path40.join(root, DECISIONS_TRIGGERS.legacyDecisionsDir ?? "docs/adr");
8172
8211
  const hasPrimary = await fs38.pathExists(primary);
8173
8212
  const hasLegacy = await fs38.pathExists(legacy);
8174
8213
  if (!hasPrimary && hasLegacy) {
8175
- findings.push({
8176
- level: "advisory",
8177
- line: `- DECISIONS: legacy ${DECISIONS_TRIGGERS.legacyDecisionsDir}/ present \u2014 migrate to docs/decisions/`,
8178
- sentence: "Decision records still live under the legacy docs/adr/ path",
8179
- fix: `git mv ${DECISIONS_TRIGGERS.legacyDecisionsDir} ${DECISIONS_TRIGGERS.decisionsDir}`
8180
- });
8214
+ if (PROFILES[profile].writesDecisionsSeed) {
8215
+ findings.push({
8216
+ level: "advisory",
8217
+ line: `- DECISIONS: legacy ${DECISIONS_TRIGGERS.legacyDecisionsDir}/ present \u2014 migrate to docs/decisions/`,
8218
+ sentence: "Decision records still live under the legacy docs/adr/ path",
8219
+ fix: `git mv ${DECISIONS_TRIGGERS.legacyDecisionsDir} ${DECISIONS_TRIGGERS.decisionsDir}`
8220
+ });
8221
+ }
8181
8222
  return findings;
8182
8223
  }
8183
8224
  if (!hasPrimary) return findings;
@@ -9535,7 +9576,7 @@ async function runDoctor(options) {
9535
9576
  if (docsSkillInstalled) {
9536
9577
  if (await fs41.pathExists(path49.join(root, "docs", "SUMMARY.md"))) {
9537
9578
  ok("- PROJECT DOCS: OK (docs/SUMMARY.md present)");
9538
- } else {
9579
+ } else if (PROFILES[profile].runsDocsPass) {
9539
9580
  suggest(
9540
9581
  "- PROJECT DOCS: docs/SUMMARY.md missing \u2014 run the writing-documentation skill (`/docs` in Claude Code) to generate the project docs",
9541
9582
  "The project docs index (docs/SUMMARY.md) has not been generated yet",
@@ -9599,7 +9640,7 @@ async function runDoctor(options) {
9599
9640
  } else {
9600
9641
  ok("- GITIGNORE (haus content): OK (.claude/ and .haus-workflow/ are tracked)");
9601
9642
  }
9602
- for (const finding of await auditDecisionsLayout(root)) {
9643
+ for (const finding of await auditDecisionsLayout(root, profile)) {
9603
9644
  if (finding.level === "ok") ok(finding.line);
9604
9645
  else suggest(finding.line, finding.sentence, finding.fix);
9605
9646
  }
@@ -11864,6 +11905,32 @@ function isPackagePublish(words) {
11864
11905
  if (RUN_SCRIPT_VERBS.some((verb) => hasWord(words, verb))) return false;
11865
11906
  return !words.some((word) => word === "--dry-run" || word === "--dry-run=true");
11866
11907
  }
11908
+ var GH_TEXT_FLAGS = /* @__PURE__ */ new Set([
11909
+ "-b",
11910
+ "--body",
11911
+ "-F",
11912
+ "--body-file",
11913
+ "-t",
11914
+ "--subject",
11915
+ "-R",
11916
+ "--repo"
11917
+ ]);
11918
+ var ADMIN_MERGE_FLAGS = /* @__PURE__ */ new Set(["--admin", "--admin=true", "--admin=1"]);
11919
+ function isProtectionBypassMerge(words) {
11920
+ if (commandName(words) !== "gh") return false;
11921
+ const args = [];
11922
+ for (let i = 1; i < words.length; i += 1) {
11923
+ const word = words[i];
11924
+ if (GH_TEXT_FLAGS.has(word)) {
11925
+ i += 1;
11926
+ continue;
11927
+ }
11928
+ args.push(word);
11929
+ }
11930
+ const operands = args.filter((word) => !word.startsWith("-")).map((word) => word.toLowerCase());
11931
+ if (operands[0] !== "pr" || operands[1] !== "merge") return false;
11932
+ return args.some((word) => ADMIN_MERGE_FLAGS.has(word.toLowerCase()));
11933
+ }
11867
11934
  var PRIVILEGE_ESCALATORS = /* @__PURE__ */ new Set(["sudo", "doas", "pkexec"]);
11868
11935
  function isPrivilegeEscalation(words) {
11869
11936
  return PRIVILEGE_ESCALATORS.has(commandName(words));
@@ -11883,7 +11950,8 @@ var SEGMENT_MATCHERS = [
11883
11950
  { name: "a force push", matches: isForcePush },
11884
11951
  { name: "a commit or push that skips the hooks", matches: isHookBypass },
11885
11952
  { name: "a recursive permission change", matches: isRecursiveChmod },
11886
- { name: "a package publish", matches: isPackagePublish }
11953
+ { name: "a package publish", matches: isPackagePublish },
11954
+ { name: "a merge that bypasses branch protection", matches: isProtectionBypassMerge }
11887
11955
  ];
11888
11956
  function matchDangerousCommandShape(command) {
11889
11957
  for (const segment of parseShellSegments(command)) {
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "6.5.0",
2
+ "version": "6.5.1",
3
3
  "manifestFormat": 2,
4
4
  "globalPlugins": [
5
5
  {
@@ -56,7 +56,7 @@
56
56
  },
57
57
  {
58
58
  "id": "haus.agentic-workflow-poc",
59
- "version": "1.0.0",
59
+ "version": "1.0.1",
60
60
  "source": "haus",
61
61
  "type": "template",
62
62
  "path": "templates/agentic-workflow-poc.md",
@@ -68,8 +68,8 @@
68
68
  "repoRoles": [],
69
69
  "ecosystem": "general",
70
70
  "installMode": "copy-selected",
71
- "tokenEstimate": 2094,
72
- "tokenBudget": 2723,
71
+ "tokenEstimate": 2187,
72
+ "tokenBudget": 2844,
73
73
  "default": false,
74
74
  "reviewStatus": "approved",
75
75
  "riskLevel": "low"
@@ -67,13 +67,15 @@ A description argument ("a frontend app for shopping") goes straight to Phase 1.
67
67
  No-argument invocation: ask "What do you want to build?" as free text. Do not present
68
68
  a stack menu.
69
69
 
70
- **PoC:** the mode is already settled by now, either the trigger phrase in the request or
71
- the question asked in `## PoC mode` above, so decide the way in next: `git rev-parse
72
- --is-inside-work-tree` and `ls -A`: an empty directory or a git repo means the cwd IS the
73
- project (manual rails in Phase 3); anything else means the cwd is a parent folder (fast
74
- path). A git repo with a remote (`git remote -v`) keeps it; one without gets a new
75
- private repo in Phase 3, unless local-only is chosen in Phase 1. The workspace question
76
- above applies unchanged.
70
+ **PoC:** upgrading the haus package rewrites this very file, so if the CLI gets upgraded
71
+ mid-flow, re-read `references/new.md` from disk before continuing rather than carrying on
72
+ from what is already in context. The mode itself is already settled by now, either the
73
+ trigger phrase in the request or the question asked in `## PoC mode` above, so decide the
74
+ way in next: `git rev-parse --is-inside-work-tree` and `ls -A`: an empty directory or a
75
+ git repo means the cwd IS the project (manual rails in Phase 3); anything else means the
76
+ cwd is a parent folder (fast path). A git repo with a remote (`git remote -v`) keeps it;
77
+ one without gets a new private repo in Phase 3, unless local-only is chosen in Phase 1.
78
+ The workspace question above applies unchanged.
77
79
 
78
80
  ## Phase 1 — Elicit (at most 5 AskUserQuestion rounds)
79
81
 
@@ -149,7 +151,10 @@ Check the registry first: `haus new --list --json`.
149
151
  3. Manual rails: `git init` (if the scaffolder didn't), `git branch -M main`, initial
150
152
  commit; then `gh repo create` (confirm first) unless the user declined a repo.
151
153
 
152
- **PoC:** two ways in, decided already in Phase 0 by what the cwd was.
154
+ **PoC:** two ways in, decided already in Phase 0 by what the cwd was. Before either
155
+ path's own first commit below, check whatever commit hook a scaffold's files already
156
+ ship (husky, commitlint, lefthook): reading its message-format rules now means finding
157
+ out what it wants before the first commit, not after it rejects one.
153
158
 
154
159
  - **cwd is the project:** the registry is not used, because `haus new` creates
155
160
  `<parent>/<name>` and refuses an existing directory. Run the official scaffolder into
@@ -166,6 +171,16 @@ repo create`. Carry `--dir` through from Phase 0 for a workspace member; without
166
171
  PoC still lands in the cwd, exactly like production. The chained init records the
167
172
  profile and writes the PoC standard; nothing more to do for the profile in Phase 4.
168
173
 
174
+ Whichever way in, the profile governs what haus installs and nothing else: a vendor
175
+ starter's own ceremony arrives untouched and no profile can subtract it. Right after the
176
+ first commit, list what came with the scaffold, naming at least its `test` script and
177
+ test directories, any `playwright.config.ts`, commit hooks (husky, commitlint, lefthook),
178
+ CI workflows, and a decisions or `docs/adr/` folder; tell the user in one line what
179
+ stays. Nothing gets deleted: removing files a vendor starter shipped is a surprise in
180
+ someone else's repo, not this flow's call to make. What stays is inert, not authority:
181
+ never run or extend a starter's test suite, and never read its presence as permission to
182
+ add tests.
183
+
169
184
  ## Phase 4 — Context install
170
185
 
171
186
  Everything from here on runs inside the new project directory — `cd` into it first and
@@ -199,23 +214,49 @@ stay there.
199
214
  directory).
200
215
  2. Read: `haus init --json --dry-run --profile poc`; on the zero-signal refusal, confirm
201
216
  and re-run with `--force` as above.
202
- 3. Write: `haus apply --write`. It reads the profile: `.haus-workflow/WORKFLOW.md` now
217
+ 3. Add a `"manus"` key to package.json scripts now, before the write step, even as a
218
+ placeholder (`"manus": "playwright test"`). `haus apply --write` derives the PoC's
219
+ only gate command in `.haus-workflow/workflow-config.md` from whether that key
220
+ exists, not from its final body, and with no key at all it falls back to a Playwright
221
+ invocation that resolves against whatever default config the scaffold shipped and
222
+ finds no tests at all. The skeleton below fills in the real body once the runner
223
+ config is decided; what matters here is that the key exists first.
224
+ 4. Write: `haus apply --write`. It reads the profile: `.haus-workflow/WORKFLOW.md` now
203
225
  starts with `# PoC Workflow Standard`, and there is no `.github/workflows/haus-gate.yml`.
204
226
 
205
- Fast path: the chained init already did this; check that `.haus-workflow/haus.config.json`
206
- says `"profile": "poc"`.
227
+ Fast path: the chained init already ran `haus apply --write` inside Phase 3, before this
228
+ phase's `"manus"` script exists, so `.haus-workflow/workflow-config.md`'s "Run the manus"
229
+ line was derived from the fallback command. That file is project-owned and never
230
+ rewritten after its first write, exactly as its own header says, so adding the script
231
+ afterward will not fix it. Once the skeleton below has added the `manus` script and
232
+ decided the runner config, open `.haus-workflow/workflow-config.md` yourself and correct
233
+ that one line to `yarn run manus` / `npm run manus` / `pnpm run manus` (the project's
234
+ package manager) either way, whether the script ended up `"playwright test"` or
235
+ `"playwright test -c playwright.manus.config.ts"`: the line only needs to delegate to
236
+ the script, which already points at the right config. Also check that
237
+ `.haus-workflow/haus.config.json` says `"profile": "poc"`.
207
238
 
208
239
  Then the demo skeleton, instead of SPEC.md and the walking-skeleton plan:
209
240
 
210
241
  - `yarn add -D @playwright/test` (or the project's package manager), then
211
242
  `npx playwright install chromium` (a download: ask first).
212
- - `playwright.config.ts` with a `webServer` that starts the dev server and takes its port
213
- from `PORT` (default 3000), `testDir: 'demo'`, `reporter: 'line'`, `use.baseURL`
214
- from the same port.
243
+ - The runner config. If the repo has no `playwright.config.ts`, write one with a
244
+ `webServer` that starts the dev server and takes its port from `PORT` (default 3000),
245
+ `testDir: 'demo'`, `reporter: 'line'`, `use.baseURL` from the same port. If the
246
+ scaffold already shipped a `playwright.config.ts`, which a vendor starter usually has
247
+ and points at its own e2e suite, leave that file alone and write
248
+ `playwright.manus.config.ts` beside it with the same contents instead: two configs is
249
+ the correct outcome, the starter's suite tests the starter and the manus tests the
250
+ demo. Either way, the config's own `webServer` owns the dev server: never start the
251
+ dev server yourself in the background and then run the manus against it, or every
252
+ round starts by hunting the process holding the port.
215
253
  - `demo/manus.spec.ts` with the header comment `// The manus. One test per point in
216
254
  docs/MANUS.md, same titles, same order. test.fixme() for points decided but not built.`
217
255
  and no tests yet.
218
- - `"manus": "playwright test"` in package.json scripts.
256
+ - The `"manus"` script: on the manual path fill in the key added in step 3, and on the fast
257
+ path add it here. Either way its body is `"playwright test"` when the repo had no
258
+ config of its own, or `"playwright test -c playwright.manus.config.ts"` when the
259
+ starter's config forced the second file.
219
260
  - `docs/MANUS.md`:
220
261
 
221
262
  ```markdown
@@ -262,7 +303,19 @@ Commit the skeleton on `main` (`chore(poc): demo skeleton`).
262
303
  **PoC:** now, with the PoC standard loaded, invoke superpowers:brainstorming. A new
263
304
  project is architectural, so it ends in a written design: tell the skill the spec
264
305
  location is `docs/MANUS.md` (numbered points in show order, each with what the customer
265
- sees and what proves it; keep the `Demo:` header). Then superpowers:writing-plans into
306
+ sees and what proves it; keep the `Demo:` header).
307
+
308
+ Before the plan, probe. For every drafted point, run the smallest real query that
309
+ produces that point's proof against the system the demo will use: one GraphQL query, one
310
+ API call, one page load. Do it before any code is written, not when the feature is half
311
+ built. A point whose proof the system cannot produce today goes straight to Parked in
312
+ `docs/MANUS.md` with the one sentence that says what is missing, and the user hears it
313
+ before the plan exists. If the probe kills the system chosen in Phase 1, stop and ask:
314
+ the manus is the scope ceiling, and what it runs against is part of the manus. Do not
315
+ commit a backend URL or token until the probe has confirmed the system can produce the
316
+ points that depend on it.
317
+
318
+ Then superpowers:writing-plans into
266
319
  `docs/plans/round-1.md`, one task per manus point with "the manus point is green" as its
267
320
  verification. Build: `demo/manus.spec.ts` gets one `test()` per point as it is built and
268
321
  `test.fixme()` for the rest; independent points may go to one subagent each in its own
@@ -290,9 +343,19 @@ why.
290
343
  project directory.
291
344
 
292
345
  **PoC:** the gate is the manus: `yarn manus` (or the package manager's equivalent) with
293
- every point green or `fixme`, nothing red. Then: README with the start command, the port
294
- and the manus command; commit `docs/MANUS.md` and `docs/plans/round-1.md`; `haus doctor`
295
- shows `Profile: poc` and nothing needing attention; push if a remote exists (never for
346
+ every point green or `fixme`, nothing red, and the last run before you claim anything is
347
+ a cold `--headed` run with the dev server stopped first. Headless wins timing races that
348
+ a demo-day browser loses, so a point that is green headless and red headed is red, and
349
+ headed is the mode the demo is actually shown in. Never put a command in the handoff
350
+ that you have not run yourself in exactly that form. Then: README with the start
351
+ command, the port and the manus command; commit `docs/MANUS.md` and
352
+ `docs/plans/round-1.md`. `haus doctor` shows `Profile: poc`; the CI-gate suggestion, the
353
+ `docs/SUMMARY.md` suggestion and the legacy `docs/adr/` migration finding are all gated
354
+ on the profile now, so a healthy PoC repo will not see any of the three, and seeing one
355
+ means the installed CLI predates this gate, not something to argue past. Whatever doctor
356
+ still flags here is real: read it and act on it, or, for a machine-level finding such as
357
+ a disabled global plugin, check it with the tool that owns it (`claude plugin list`)
358
+ before telling the user doctor is wrong. Then push if a remote exists (never for
296
359
  local-only). Then stop: do NOT follow `references/init.md`
297
360
  from step 3, because a PoC has no docs pass, no deep read and no recommend re-check. Hand off in
298
361
  the user's language, naming the demo date, the two re-entry phrases and the decision the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@haus-tech/haus-workflow",
3
- "version": "5.5.0",
3
+ "version": "5.6.1",
4
4
  "description": "Haus AI workflow CLI for Claude Code.",
5
5
  "type": "module",
6
6
  "bin": {