@haus-tech/haus-workflow 3.1.2 → 4.0.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 +14 -0
- package/dist/cli.js +147 -49
- package/library/global/skills/haus-workflow/references/init.md +5 -5
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [4.0.0](https://github.com/WeAreHausTech/haus-workflow/compare/v3.1.2...v4.0.0) (2026-08-20)
|
|
4
|
+
|
|
5
|
+
### ⚠ BREAKING CHANGES
|
|
6
|
+
|
|
7
|
+
- **init:** stdout is `{ scan, recommendation, hooks,
|
|
8
|
+
repoName, warnings }`, not scan-then-recommend documents plus
|
|
9
|
+
human doctor lines. Callers that parsed the first document as
|
|
10
|
+
the scan must read `envelope.scan`.
|
|
11
|
+
|
|
12
|
+
### Bug Fixes
|
|
13
|
+
|
|
14
|
+
- **init:** emit one JSON envelope on --json ([#318](https://github.com/WeAreHausTech/haus-workflow/issues/318)) ([2657428](https://github.com/WeAreHausTech/haus-workflow/commit/2657428a47a2459d3a1ccdc3a9137f9553ccb3d7)), closes [#306](https://github.com/WeAreHausTech/haus-workflow/issues/306)
|
|
15
|
+
- surface worktree hydrate install failure diagnostics ([#319](https://github.com/WeAreHausTech/haus-workflow/issues/319)) ([3fb6a53](https://github.com/WeAreHausTech/haus-workflow/commit/3fb6a53c00471e5b4c9aa0658cd19806785db717))
|
|
16
|
+
|
|
3
17
|
## [3.1.2](https://github.com/WeAreHausTech/haus-workflow/compare/v3.1.1...v3.1.2) (2026-08-20)
|
|
4
18
|
|
|
5
19
|
### Bug Fixes
|
package/dist/cli.js
CHANGED
|
@@ -10014,6 +10014,9 @@ async function hasMultipleSiblingRepos(root) {
|
|
|
10014
10014
|
init_fs();
|
|
10015
10015
|
init_logger();
|
|
10016
10016
|
init_paths();
|
|
10017
|
+
function emitJsonEnvelope(json, quiet, envelope) {
|
|
10018
|
+
if (json && !quiet) log(JSON.stringify(envelope, null, 2));
|
|
10019
|
+
}
|
|
10017
10020
|
async function readPriorManualIncludes(root) {
|
|
10018
10021
|
const prior = await readJson(hausPath(root, "recommendation.json"));
|
|
10019
10022
|
if (!prior?.recommended) return [];
|
|
@@ -10021,16 +10024,12 @@ async function readPriorManualIncludes(root) {
|
|
|
10021
10024
|
}
|
|
10022
10025
|
async function runSetupCore(root, opts) {
|
|
10023
10026
|
const { json, apply, dryRun, quiet, confirm: confirm2, force } = opts;
|
|
10024
|
-
const say = quiet ? () => {
|
|
10027
|
+
const say = quiet || json ? () => {
|
|
10025
10028
|
} : log;
|
|
10026
10029
|
const scanResult = await scanProject(root);
|
|
10027
|
-
|
|
10028
|
-
|
|
10029
|
-
|
|
10030
|
-
say("Haus scan complete");
|
|
10031
|
-
say(`Roles: ${scanResult.repoRoles.join(", ") || "unknown"}`);
|
|
10032
|
-
say(`Package manager: ${scanResult.packageManager}`);
|
|
10033
|
-
}
|
|
10030
|
+
say("Haus scan complete");
|
|
10031
|
+
say(`Roles: ${scanResult.repoRoles.join(", ") || "unknown"}`);
|
|
10032
|
+
say(`Package manager: ${scanResult.packageManager}`);
|
|
10034
10033
|
const siblingWarning = await hasMultipleSiblingRepos(root) ? [SIBLING_REPO_WARNING] : [];
|
|
10035
10034
|
const context = await readContextOrScan(root);
|
|
10036
10035
|
const priorManualIncludes = await readPriorManualIncludes(root);
|
|
@@ -10043,39 +10042,39 @@ async function runSetupCore(root, opts) {
|
|
|
10043
10042
|
const warningLines2 = [
|
|
10044
10043
|
.../* @__PURE__ */ new Set([...context.warnings, ...recommendation.warnings ?? [], ...siblingWarning])
|
|
10045
10044
|
];
|
|
10046
|
-
|
|
10047
|
-
|
|
10048
|
-
|
|
10049
|
-
|
|
10050
|
-
|
|
10051
|
-
say(`Skipped: ${recommendation.skipped.length}`);
|
|
10052
|
-
say(`Repo: ${context.repoName}`);
|
|
10053
|
-
for (const warning of warningLines2) say(`- WARN: ${warning}`);
|
|
10054
|
-
}
|
|
10045
|
+
say("Haus recommendation ready");
|
|
10046
|
+
say("Recommended: 0");
|
|
10047
|
+
say(`Skipped: ${recommendation.skipped.length}`);
|
|
10048
|
+
say(`Repo: ${context.repoName}`);
|
|
10049
|
+
for (const warning of warningLines2) say(`- WARN: ${warning}`);
|
|
10055
10050
|
const supersededCount = recommendation.skipped.filter(
|
|
10056
10051
|
(s) => s.skipReasons?.some((r) => r.code === "superseded-by-global-plugin")
|
|
10057
10052
|
).length;
|
|
10058
10053
|
const message2 = supersededCount > 0 ? `No catalog items matched this project after ${supersededCount} item(s) were gated as already provided by an installed global plugin \u2014 refusing to write recommendation.json/haus.lock.json. Those skills are available globally; re-run with \`haus init --force\` to write anyway.` : "No catalog items matched this project at all (zero signal) \u2014 refusing to write recommendation.json/haus.lock.json. Re-run with `haus init --force` to write anyway.";
|
|
10059
10054
|
warn(message2);
|
|
10060
10055
|
process.exitCode = 1;
|
|
10056
|
+
const warnings = [...warningLines2, message2];
|
|
10057
|
+
emitJsonEnvelope(json, quiet, {
|
|
10058
|
+
scan: scanResult,
|
|
10059
|
+
recommendation,
|
|
10060
|
+
hooks: null,
|
|
10061
|
+
repoName: context.repoName,
|
|
10062
|
+
warnings
|
|
10063
|
+
});
|
|
10061
10064
|
return {
|
|
10062
10065
|
root,
|
|
10063
10066
|
repoName: context.repoName,
|
|
10064
10067
|
roles: scanResult.repoRoles,
|
|
10065
10068
|
recommendedCount: 0,
|
|
10066
|
-
warnings
|
|
10069
|
+
warnings,
|
|
10067
10070
|
hooksOk: false,
|
|
10068
10071
|
written: []
|
|
10069
10072
|
};
|
|
10070
10073
|
}
|
|
10071
10074
|
await writeJson(hausPath(root, "recommendation.json"), recommendation);
|
|
10072
|
-
|
|
10073
|
-
|
|
10074
|
-
|
|
10075
|
-
say("Haus recommendation ready");
|
|
10076
|
-
say(`Recommended: ${recommendation.recommended.length}`);
|
|
10077
|
-
say(`Skipped: ${recommendation.skipped.length}`);
|
|
10078
|
-
}
|
|
10075
|
+
say("Haus recommendation ready");
|
|
10076
|
+
say(`Recommended: ${recommendation.recommended.length}`);
|
|
10077
|
+
say(`Skipped: ${recommendation.skipped.length}`);
|
|
10079
10078
|
const hooks = await verifyProjectSettingsHooksContract(root);
|
|
10080
10079
|
const warningLines = [
|
|
10081
10080
|
.../* @__PURE__ */ new Set([...context.warnings, ...recommendation.warnings ?? [], ...siblingWarning])
|
|
@@ -10100,12 +10099,23 @@ async function runSetupCore(root, opts) {
|
|
|
10100
10099
|
hooksOk,
|
|
10101
10100
|
written: []
|
|
10102
10101
|
};
|
|
10103
|
-
|
|
10102
|
+
const envelope = (hooksResult) => ({
|
|
10103
|
+
scan: scanResult,
|
|
10104
|
+
recommendation,
|
|
10105
|
+
hooks: hooksResult,
|
|
10106
|
+
repoName: context.repoName,
|
|
10107
|
+
warnings: warningLines
|
|
10108
|
+
});
|
|
10109
|
+
if (!apply) {
|
|
10110
|
+
emitJsonEnvelope(json, quiet, envelope(hooks));
|
|
10111
|
+
return baseResult;
|
|
10112
|
+
}
|
|
10104
10113
|
if (confirm2) {
|
|
10105
10114
|
const approved = await confirm2();
|
|
10106
10115
|
if (!approved) {
|
|
10107
10116
|
say("Setup reviewed. No files written.");
|
|
10108
10117
|
say("Next step: run `haus apply --write` when ready.");
|
|
10118
|
+
emitJsonEnvelope(json, quiet, envelope(hooks));
|
|
10109
10119
|
return baseResult;
|
|
10110
10120
|
}
|
|
10111
10121
|
}
|
|
@@ -10115,6 +10125,7 @@ async function runSetupCore(root, opts) {
|
|
|
10115
10125
|
if (sync.formatRefusal) {
|
|
10116
10126
|
error(sync.formatRefusal);
|
|
10117
10127
|
process.exitCode = 1;
|
|
10128
|
+
emitJsonEnvelope(json, quiet, envelope(hooks));
|
|
10118
10129
|
return baseResult;
|
|
10119
10130
|
}
|
|
10120
10131
|
if (sync.newItems.length > 0) {
|
|
@@ -10142,6 +10153,7 @@ async function runSetupCore(root, opts) {
|
|
|
10142
10153
|
} else {
|
|
10143
10154
|
say(`- HOOKS OK: ${hooksAfter.message}`);
|
|
10144
10155
|
}
|
|
10156
|
+
emitJsonEnvelope(json, quiet, envelope(hooksAfter));
|
|
10145
10157
|
return { ...baseResult, hooksOk: hooksOkAfter, written: files };
|
|
10146
10158
|
}
|
|
10147
10159
|
|
|
@@ -11926,9 +11938,72 @@ async function computeLockfileSignals(dir) {
|
|
|
11926
11938
|
hasDotnetProject
|
|
11927
11939
|
};
|
|
11928
11940
|
}
|
|
11941
|
+
var INSTALL_OUTPUT_CAP = 8192;
|
|
11942
|
+
var INSTALL_TAIL_LINES = 8;
|
|
11943
|
+
function capTail(text, max = INSTALL_OUTPUT_CAP) {
|
|
11944
|
+
return text.length <= max ? text : text.slice(-max);
|
|
11945
|
+
}
|
|
11946
|
+
function nonemptyLines(text) {
|
|
11947
|
+
return text.split(/\r?\n/).filter((line2) => line2.trim().length > 0);
|
|
11948
|
+
}
|
|
11949
|
+
function findRequiredYarnrcEnvVars(content2) {
|
|
11950
|
+
const required = /\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
|
|
11951
|
+
const names = /* @__PURE__ */ new Set();
|
|
11952
|
+
for (const line2 of content2.split(/\r?\n/)) {
|
|
11953
|
+
if (line2.trimStart().startsWith("#")) continue;
|
|
11954
|
+
required.lastIndex = 0;
|
|
11955
|
+
let match;
|
|
11956
|
+
while ((match = required.exec(line2)) !== null) {
|
|
11957
|
+
const name = match[1];
|
|
11958
|
+
if (name !== void 0) names.add(name);
|
|
11959
|
+
}
|
|
11960
|
+
}
|
|
11961
|
+
return [...names];
|
|
11962
|
+
}
|
|
11963
|
+
function missingYarnrcEnvVars(content2, env) {
|
|
11964
|
+
return findRequiredYarnrcEnvVars(content2).filter((name) => env[name] === void 0);
|
|
11965
|
+
}
|
|
11966
|
+
function missingYarnrcEnvVarsInDir(dir, env) {
|
|
11967
|
+
const yarnrcPath = path59.join(dir, ".yarnrc.yml");
|
|
11968
|
+
if (!existsSync3(yarnrcPath)) return [];
|
|
11969
|
+
try {
|
|
11970
|
+
return missingYarnrcEnvVars(readFileSync3(yarnrcPath, "utf8"), env);
|
|
11971
|
+
} catch {
|
|
11972
|
+
return [];
|
|
11973
|
+
}
|
|
11974
|
+
}
|
|
11975
|
+
function tailInstallOutput(stdout2, stderr) {
|
|
11976
|
+
const errLines = nonemptyLines(stderr).slice(-INSTALL_TAIL_LINES);
|
|
11977
|
+
const remaining = INSTALL_TAIL_LINES - errLines.length;
|
|
11978
|
+
const outLines = remaining > 0 ? nonemptyLines(stdout2).slice(-remaining) : [];
|
|
11979
|
+
return [...errLines, ...outLines].join("\n");
|
|
11980
|
+
}
|
|
11981
|
+
function summarizeInstall(install) {
|
|
11982
|
+
if (install.plan.manager === "none") return "no install step (no recognized lockfile)";
|
|
11983
|
+
const envNotes = (install.missingEnvVars ?? []).map(
|
|
11984
|
+
(name) => `${name} not set in environment \u2014 yarn install will fail`
|
|
11985
|
+
);
|
|
11986
|
+
if (!install.ran) {
|
|
11987
|
+
const skip = `install skipped (${install.skippedReason ?? "unavailable"})`;
|
|
11988
|
+
return envNotes.length > 0 ? [skip, ...envNotes].join("\n ") : skip;
|
|
11989
|
+
}
|
|
11990
|
+
if (install.ok) return `hydrated via ${install.plan.manager}`;
|
|
11991
|
+
const head = `install FAILED via ${install.plan.manager}`;
|
|
11992
|
+
const tail = tailInstallOutput(install.stdout ?? "", install.stderr ?? "");
|
|
11993
|
+
const details = [...envNotes, ...tail.length > 0 ? tail.split("\n") : []];
|
|
11994
|
+
return details.length > 0 ? [head, ...details].join("\n ") : head;
|
|
11995
|
+
}
|
|
11996
|
+
function withMissingEnv(result, missingEnvVars) {
|
|
11997
|
+
return missingEnvVars.length > 0 ? { ...result, missingEnvVars } : result;
|
|
11998
|
+
}
|
|
11929
11999
|
async function runInstallPlan(dir, plan, deps = {}) {
|
|
12000
|
+
const env = deps.env ?? process.env;
|
|
12001
|
+
const missingEnvVars = plan.manager === "yarn" ? missingYarnrcEnvVarsInDir(dir, env) : [];
|
|
11930
12002
|
if (plan.manager === "none") {
|
|
11931
|
-
return
|
|
12003
|
+
return withMissingEnv(
|
|
12004
|
+
{ plan, ran: false, ok: true, skippedReason: "no recognized lockfile" },
|
|
12005
|
+
missingEnvVars
|
|
12006
|
+
);
|
|
11932
12007
|
}
|
|
11933
12008
|
const probe = deps.probe ?? probeCommand;
|
|
11934
12009
|
const probed = await probe(plan.command);
|
|
@@ -11936,25 +12011,41 @@ async function runInstallPlan(dir, plan, deps = {}) {
|
|
|
11936
12011
|
case "available":
|
|
11937
12012
|
break;
|
|
11938
12013
|
case "absent":
|
|
11939
|
-
return
|
|
12014
|
+
return withMissingEnv(
|
|
12015
|
+
{ plan, ran: false, ok: true, skippedReason: `${plan.command} not found on PATH` },
|
|
12016
|
+
missingEnvVars
|
|
12017
|
+
);
|
|
11940
12018
|
case "inconclusive":
|
|
11941
|
-
return
|
|
11942
|
-
|
|
11943
|
-
|
|
11944
|
-
|
|
11945
|
-
|
|
11946
|
-
|
|
12019
|
+
return withMissingEnv(
|
|
12020
|
+
{
|
|
12021
|
+
plan,
|
|
12022
|
+
ran: false,
|
|
12023
|
+
ok: true,
|
|
12024
|
+
skippedReason: `could not determine whether ${plan.command} is on PATH (${probed.reason})`
|
|
12025
|
+
},
|
|
12026
|
+
missingEnvVars
|
|
12027
|
+
);
|
|
11947
12028
|
default:
|
|
11948
12029
|
return assertNeverCommandProbe(probed);
|
|
11949
12030
|
}
|
|
11950
|
-
const
|
|
11951
|
-
|
|
11952
|
-
|
|
11953
|
-
ran: true,
|
|
11954
|
-
|
|
11955
|
-
|
|
11956
|
-
|
|
11957
|
-
|
|
12031
|
+
const run = deps.run ?? runCommand;
|
|
12032
|
+
const result = await run(plan.command, plan.args, { cwd: dir });
|
|
12033
|
+
if (result.exitCode === 0) {
|
|
12034
|
+
return withMissingEnv({ plan, ran: true, ok: true, exitCode: 0 }, missingEnvVars);
|
|
12035
|
+
}
|
|
12036
|
+
const stdout2 = capTail(result.stdout);
|
|
12037
|
+
const stderr = capTail(result.stderr);
|
|
12038
|
+
return withMissingEnv(
|
|
12039
|
+
{
|
|
12040
|
+
plan,
|
|
12041
|
+
ran: true,
|
|
12042
|
+
ok: false,
|
|
12043
|
+
exitCode: result.exitCode,
|
|
12044
|
+
stdout: stdout2.length > 0 ? stdout2 : void 0,
|
|
12045
|
+
stderr: stderr.length > 0 ? stderr : void 0
|
|
12046
|
+
},
|
|
12047
|
+
missingEnvVars
|
|
12048
|
+
);
|
|
11958
12049
|
}
|
|
11959
12050
|
|
|
11960
12051
|
// src/workspace/worktree/hydrate.ts
|
|
@@ -12629,10 +12720,11 @@ function emitJson(result) {
|
|
|
12629
12720
|
log(JSON.stringify(result, null, 2));
|
|
12630
12721
|
}
|
|
12631
12722
|
function describeInstall(hydration) {
|
|
12632
|
-
|
|
12633
|
-
|
|
12634
|
-
|
|
12635
|
-
|
|
12723
|
+
return summarizeInstall(hydration.install);
|
|
12724
|
+
}
|
|
12725
|
+
function emitMemberLine(hydration, line2) {
|
|
12726
|
+
if (hydration?.install.ran && !hydration.install.ok) warn(line2);
|
|
12727
|
+
else log(line2);
|
|
12636
12728
|
}
|
|
12637
12729
|
async function handleAdd(opts) {
|
|
12638
12730
|
if (!opts.slug) {
|
|
@@ -12677,7 +12769,10 @@ async function handleAdd(opts) {
|
|
|
12677
12769
|
for (const m of result.members) {
|
|
12678
12770
|
if (m.status === "ok") {
|
|
12679
12771
|
const hydrationNote = m.hydration ? describeInstall(m.hydration) : "not hydrated (--no-hydrate)";
|
|
12680
|
-
|
|
12772
|
+
emitMemberLine(
|
|
12773
|
+
m.hydration,
|
|
12774
|
+
` ${m.member}: ${m.branchAction} "${m.branch}" \u2014 ${hydrationNote} (${m.durationMs}ms)`
|
|
12775
|
+
);
|
|
12681
12776
|
} else {
|
|
12682
12777
|
warn(` ${m.member}: FAILED \u2014 ${m.error}`);
|
|
12683
12778
|
}
|
|
@@ -12721,7 +12816,10 @@ async function handleHydrate(opts) {
|
|
|
12721
12816
|
for (const r of result.results) {
|
|
12722
12817
|
if (r.status === "ok" && r.hydration) {
|
|
12723
12818
|
const materializedNote = r.branchAction ? `materialized (${r.branchAction}), ` : "";
|
|
12724
|
-
|
|
12819
|
+
emitMemberLine(
|
|
12820
|
+
r.hydration,
|
|
12821
|
+
` ${r.member}: ${materializedNote}${describeInstall(r.hydration)}`
|
|
12822
|
+
);
|
|
12725
12823
|
} else {
|
|
12726
12824
|
warn(` ${r.member}: ${r.status}${r.error ? ` \u2014 ${r.error}` : ""}`);
|
|
12727
12825
|
}
|
|
@@ -14058,7 +14156,7 @@ backups.command("prune").description("Delete old backups by age and/or count (re
|
|
|
14058
14156
|
})
|
|
14059
14157
|
);
|
|
14060
14158
|
program.command("explain-recommendation").description("Print a per-item breakdown of the current recommendation, with skip reasons").option("--json").action(runExplainRecommendation);
|
|
14061
|
-
program.command("init").description("Full setup: scan, recommend, and optionally apply Claude files (safe to re-run)").option("--dry-run", "Preview the `.claude/` write; scan artifacts are still written").option("--json", "
|
|
14159
|
+
program.command("init").description("Full setup: scan, recommend, and optionally apply Claude files (safe to re-run)").option("--dry-run", "Preview the `.claude/` write; scan artifacts are still written").option("--json", "Emit one JSON envelope on stdout (requires --dry-run)").option(
|
|
14062
14160
|
"--force",
|
|
14063
14161
|
"Write recommendation.json/haus.lock.json even when zero catalog items matched at all"
|
|
14064
14162
|
).action(runInit);
|
|
@@ -7,11 +7,11 @@ commands; they read plain language and approve.
|
|
|
7
7
|
Do this in order:
|
|
8
8
|
|
|
9
9
|
1. **Detect.** Run `haus init --json --dry-run`. Read the JSON yourself —
|
|
10
|
-
do not show it.
|
|
11
|
-
e.g. "This looks like a Next.js
|
|
12
|
-
end-to-end tests." If
|
|
13
|
-
honestly ("I couldn't fully
|
|
14
|
-
workflow and security guidance").
|
|
10
|
+
do not show it. Stdout is one envelope: `{ scan, recommendation, hooks, repoName, warnings }`.
|
|
11
|
+
Translate `scan` into one or two plain sentences, e.g. "This looks like a Next.js
|
|
12
|
+
website using Yarn. I found unit tests but no end-to-end tests." If
|
|
13
|
+
`scan.detectionStatus` is `unknown` or `partial`, say so honestly ("I couldn't fully
|
|
14
|
+
recognise this stack, so I'll apply the general workflow and security guidance").
|
|
15
15
|
|
|
16
16
|
2. **Apply the basics.** Run `haus apply --write`. Read the result. This installs
|
|
17
17
|
the core guardrails and helpers — including the documentation skill haus uses
|