werkmeister 0.17.1 → 0.19.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/dist/lib/gate.js CHANGED
@@ -1,278 +1,5 @@
1
1
  import { createRequire as __sandboxCreateRequire } from "node:module";
2
2
  const require = __sandboxCreateRequire(import.meta.url);
3
-
4
- // ../../packages/sandbox-core/dist/gate.js
5
- import * as fs2 from "node:fs";
6
- import * as path2 from "node:path";
7
-
8
- // ../../packages/sandbox-core/dist/dot_dir.js
9
- import * as fs from "node:fs";
10
- import * as path from "node:path";
11
- var DOT_DIRS = [".werkmeister", ".sandbox"];
12
- function dotDir(parent) {
13
- const found = DOT_DIRS.find((d) => fs.existsSync(path.join(parent, d)));
14
- return path.join(parent, found ?? DOT_DIRS[0]);
15
- }
16
-
17
- // ../../packages/sandbox-core/dist/gate.js
18
- var _PRECEDENCE_TEXT = "Precedence: these standards' rules win over conflicting task framing \u2014 if a rule and the task clash, follow the rule and say so (invariant: apply it and write a `decision` note; rule: surface the conflict in one line, then apply). This gate fires once; your retry will proceed.";
19
- var _PHASE_ENTRY_TEXT = "Standards gate: invoke `sb-implement` before coding \u2014 it loads the universal base (ponytail + comments + engineering) and the workspace notes. Then retry this edit.";
20
- function _translate(pat) {
21
- let res = "";
22
- let i = 0;
23
- const n = pat.length;
24
- while (i < n) {
25
- const c = pat[i];
26
- i += 1;
27
- if (c === "*") {
28
- res += ".*";
29
- } else if (c === "?") {
30
- res += ".";
31
- } else if (c === "[") {
32
- let j = i;
33
- if (j < n && pat[j] === "!")
34
- j += 1;
35
- if (j < n && pat[j] === "]")
36
- j += 1;
37
- while (j < n && pat[j] !== "]")
38
- j += 1;
39
- if (j >= n) {
40
- res += "\\[";
41
- } else {
42
- let stuff = pat.slice(i, j).replace(/\\/g, "\\\\");
43
- i = j + 1;
44
- if (stuff.startsWith("!"))
45
- stuff = "^" + stuff.slice(1);
46
- else if (stuff.startsWith("^"))
47
- stuff = "\\" + stuff;
48
- res += `[${stuff}]`;
49
- }
50
- } else {
51
- res += c.replace(/[.+^${}()|[\]\\]/g, "\\$&");
52
- }
53
- }
54
- return `^(?:${res})$`;
55
- }
56
- function fnmatch(name, pat) {
57
- return new RegExp(_translate(pat), "s").test(name);
58
- }
59
- function findWorkspaceRoot(start) {
60
- let directory = path2.resolve(start);
61
- while (true) {
62
- if (_isDir(dotDir(directory))) {
63
- return directory;
64
- }
65
- const parent = path2.dirname(directory);
66
- if (parent === directory) {
67
- return null;
68
- }
69
- directory = parent;
70
- }
71
- }
72
- function _isDir(p) {
73
- try {
74
- return fs2.statSync(p).isDirectory();
75
- } catch {
76
- return false;
77
- }
78
- }
79
- function _isFile(p) {
80
- try {
81
- return fs2.statSync(p).isFile();
82
- } catch {
83
- return false;
84
- }
85
- }
86
- function _isDict(value) {
87
- return typeof value === "object" && value !== null && !Array.isArray(value);
88
- }
89
- function loadManifest(workspace) {
90
- const manifestPath = path2.join(dotDir(workspace), "standards.json");
91
- if (!_isFile(manifestPath)) {
92
- return null;
93
- }
94
- let data;
95
- try {
96
- data = JSON.parse(fs2.readFileSync(manifestPath, "utf8"));
97
- } catch {
98
- return null;
99
- }
100
- return _isDict(data) ? data : null;
101
- }
102
- function _statePath(workspace, sessionId) {
103
- return path2.join(dotDir(workspace), ".gate", `${sessionId}.json`);
104
- }
105
- function _loadState(workspace, sessionId) {
106
- const statePath = _statePath(workspace, sessionId);
107
- if (!_isFile(statePath)) {
108
- return {};
109
- }
110
- let data;
111
- try {
112
- data = JSON.parse(fs2.readFileSync(statePath, "utf8"));
113
- } catch {
114
- return null;
115
- }
116
- return _isDict(data) ? data : null;
117
- }
118
- function _saveState(workspace, sessionId, state) {
119
- try {
120
- const statePath = _statePath(workspace, sessionId);
121
- fs2.mkdirSync(path2.dirname(statePath), { recursive: true });
122
- fs2.writeFileSync(statePath, JSON.stringify(state));
123
- return true;
124
- } catch {
125
- return false;
126
- }
127
- }
128
- function _sorted(values) {
129
- return [...values].sort();
130
- }
131
- function _noteStatus(text) {
132
- const block = /^---\r?\n([\s\S]*?)\r?\n---/.exec(text);
133
- if (!block) {
134
- return void 0;
135
- }
136
- const status = /^status:\s*["']?([^\s"'#]+)/m.exec(block[1]);
137
- return status ? status[1].toLowerCase() : null;
138
- }
139
- function _phaseGateSatisfied(workspace, gate) {
140
- const note = String(gate["note"] ?? "");
141
- if (!/^[a-z]+$/.test(note)) {
142
- return true;
143
- }
144
- const want = gate["status"] ? String(gate["status"]).toLowerCase() : null;
145
- const dir = path2.join(dotDir(workspace), "notes");
146
- let names;
147
- try {
148
- names = fs2.readdirSync(dir);
149
- } catch (exc) {
150
- return exc.code !== "ENOENT";
151
- }
152
- const pattern = new RegExp(`^\\d{3}-${note}-.+\\.md$`);
153
- for (const name of names) {
154
- if (!pattern.test(name)) {
155
- continue;
156
- }
157
- let text;
158
- try {
159
- text = fs2.readFileSync(path2.join(dir, name), "utf8");
160
- } catch {
161
- return true;
162
- }
163
- const status = _noteStatus(text);
164
- if (status === void 0) {
165
- return true;
166
- }
167
- if (status === "superseded" || status === "graduated") {
168
- continue;
169
- }
170
- if (want === null || status === want) {
171
- return true;
172
- }
173
- }
174
- return false;
175
- }
176
- function _phaseGateText(gate) {
177
- const note = String(gate["note"]);
178
- if (gate["status"]) {
179
- const status = String(gate["status"]);
180
- return `Phase gate: no \`${note}\` note in \`.sandbox/notes/\` has \`status: ${status}\`, so source edits wait for an approved design. Invoke \`sb-research\`, present the design to the user, and when they approve write \`NNN-${note}-<slug>.md\` with \`status: ${status}\` recording what was approved. Then retry this edit. Edits under \`.sandbox/\` are not gated.`;
181
- }
182
- return `Phase gate: no \`${note}\` note in \`.sandbox/notes/\` names the root cause yet. Invoke \`sb-research\`, reproduce the problem, and write \`NNN-${note}-<slug>.md\` stating the root cause. Then retry this edit. Edits under \`.sandbox/\` are not gated.`;
183
- }
184
- function decide(workspaceStart, sessionId, filePath) {
185
- if (!sessionId || sessionId.includes("/") || sessionId.includes("..")) {
186
- return null;
187
- }
188
- const workspace = findWorkspaceRoot(workspaceStart);
189
- if (workspace === null) {
190
- return null;
191
- }
192
- const manifest = loadManifest(workspace);
193
- if (manifest === null) {
194
- return null;
195
- }
196
- const state = _loadState(workspace, sessionId);
197
- if (state === null) {
198
- return null;
199
- }
200
- const relPath = filePath ? path2.relative(workspace, filePath) : "";
201
- const phaseGate = manifest["phase_gate"];
202
- if (_isDict(phaseGate) && relPath && relPath !== ".." && !relPath.startsWith(".." + path2.sep) && !path2.isAbsolute(relPath) && !DOT_DIRS.some((d) => relPath.startsWith(d + path2.sep)) && !_phaseGateSatisfied(workspace, phaseGate)) {
203
- return _phaseGateText(phaseGate);
204
- }
205
- const delivered = new Set(Array.isArray(state["delivered"]) ? state["delivered"] : []);
206
- const needsPhaseEntry = !state["phase_entry_delivered"];
207
- let domain = null;
208
- const domains = _isDict(manifest["domains"]) ? manifest["domains"] : {};
209
- for (const name of _sorted(Object.keys(domains))) {
210
- const meta = domains[name];
211
- if (delivered.has(name) || !_isDict(meta)) {
212
- continue;
213
- }
214
- const globs = Array.isArray(meta["applies_to"]) ? meta["applies_to"] : [];
215
- if (globs.some((glob) => fnmatch(relPath, String(glob)))) {
216
- domain = name;
217
- break;
218
- }
219
- }
220
- if (!needsPhaseEntry && domain === null) {
221
- return null;
222
- }
223
- const parts = [];
224
- if (needsPhaseEntry) {
225
- parts.push(_PHASE_ENTRY_TEXT);
226
- }
227
- if (domain !== null) {
228
- const meta = domains[domain];
229
- const summary = String(_isDict(meta) && meta["summary"] || "").trim();
230
- let domainText = `Standards gate: you are editing a path governed by the \`${domain}\` standard, which has not been loaded this session. Invoke the \`sb-${domain}-standard\` skill, then retry this edit.`;
231
- if (summary) {
232
- domainText += ` Summary: ${summary}.`;
233
- }
234
- parts.push(domainText);
235
- }
236
- parts.push(_PRECEDENCE_TEXT);
237
- const newState = {
238
- phase_entry_delivered: true,
239
- delivered: _sorted(domain ? /* @__PURE__ */ new Set([...delivered, domain]) : delivered)
240
- };
241
- if (!_saveState(workspace, sessionId, newState)) {
242
- return null;
243
- }
244
- return parts.join("\n");
245
- }
246
- function main() {
247
- let data;
248
- try {
249
- const raw = process.stdin.isTTY ? "" : fs2.readFileSync(0, "utf8");
250
- const parsed = raw.trim() ? JSON.parse(raw) : {};
251
- data = _isDict(parsed) ? parsed : {};
252
- } catch {
253
- data = {};
254
- }
255
- const sessionId = String(data["session_id"] || "");
256
- const toolInput = data["tool_input"];
257
- const filePath = String(_isDict(toolInput) && toolInput["file_path"] || "");
258
- const start = String(data["cwd"] || process.cwd());
259
- let reason;
260
- try {
261
- reason = decide(start, sessionId, filePath);
262
- } catch {
263
- reason = null;
264
- }
265
- if (reason) {
266
- console.log(JSON.stringify({
267
- hookSpecificOutput: {
268
- hookEventName: "PreToolUse",
269
- permissionDecision: "deny",
270
- permissionDecisionReason: reason
271
- }
272
- }));
273
- }
274
- }
275
-
276
- // src/lib/gate.ts
277
- main();
3
+ var P=Object.defineProperty;var o=(t,e)=>P(t,"name",{value:e,configurable:!0});import*as c from"node:fs";import*as a from"node:path";import*as $ from"node:fs";import*as w from"node:path";var _=[".werkmeister",".sandbox"];function S(t){let e=_.find(n=>$.existsSync(w.join(t,n)));return w.join(t,e??_[0])}o(S,"dotDir");var O="Precedence: these standards' rules win over conflicting task framing \u2014 if a rule and the task clash, follow the rule and say so (invariant: apply it and write a `decision` note; rule: surface the conflict in one line, then apply). This gate fires once; your retry will proceed.",A="Standards gate: invoke `sb-implement` before coding \u2014 it loads the universal base (ponytail + comments + engineering) and the workspace notes. Then retry this edit.";function R(t){let e="",n=0,s=t.length;for(;n<s;){let u=t[n];if(n+=1,u==="*")e+=".*";else if(u==="?")e+=".";else if(u==="["){let r=n;for(r<s&&t[r]==="!"&&(r+=1),r<s&&t[r]==="]"&&(r+=1);r<s&&t[r]!=="]";)r+=1;if(r>=s)e+="\\[";else{let i=t.slice(n,r).replace(/\\/g,"\\\\");n=r+1,i.startsWith("!")?i="^"+i.slice(1):i.startsWith("^")&&(i="\\"+i),e+=`[${i}]`}}else e+=u.replace(/[.+^${}()|[\]\\]/g,"\\$&")}return`^(?:${e})$`}o(R,"_translate");function F(t,e){return new RegExp(R(e),"s").test(t)}o(F,"fnmatch");function J(t){let e=a.resolve(t);for(;;){if(W(S(e)))return e;let n=a.dirname(e);if(n===e)return null;e=n}}o(J,"findWorkspaceRoot");function W(t){try{return c.statSync(t).isDirectory()}catch{return!1}}o(W,"_isDir");function T(t){try{return c.statSync(t).isFile()}catch{return!1}}o(T,"_isFile");function h(t){return typeof t=="object"&&t!==null&&!Array.isArray(t)}o(h,"_isDict");function C(t){let e=a.join(S(t),"standards.json");if(!T(e))return null;let n;try{n=JSON.parse(c.readFileSync(e,"utf8"))}catch{return null}return h(n)?n:null}o(C,"loadManifest");function E(t,e){return a.join(S(t),".gate",`${e}.json`)}o(E,"_statePath");function G(t,e){let n=E(t,e);if(!T(n))return{};let s;try{s=JSON.parse(c.readFileSync(n,"utf8"))}catch{return null}return h(s)?s:null}o(G,"_loadState");function L(t,e,n){try{let s=E(t,e);return c.mkdirSync(a.dirname(s),{recursive:!0}),c.writeFileSync(s,JSON.stringify(n)),!0}catch{return!1}}o(L,"_saveState");function k(t){return[...t].sort()}o(k,"_sorted");function X(t){let e=/^---\r?\n([\s\S]*?)\r?\n---/.exec(t);if(!e)return;let n=/^status:\s*["']?([^\s"'#]+)/m.exec(e[1]);return n?n[1].toLowerCase():null}o(X,"_noteStatus");function Y(t,e){let n=String(e.note??"");if(!/^[a-z]+$/.test(n))return!0;let s=e.status?String(e.status).toLowerCase():null,u=a.join(S(t),"notes"),r;try{r=c.readdirSync(u)}catch(l){return l.code!=="ENOENT"}let i=new RegExp(`^\\d{3}-${n}-.+\\.md$`);for(let l of r){if(!i.test(l))continue;let m;try{m=c.readFileSync(a.join(u,l),"utf8")}catch{return!0}let p=X(m);if(p===void 0)return!0;if(!(p==="superseded"||p==="graduated")&&(s===null||p===s))return!0}return!1}o(Y,"_phaseGateSatisfied");function z(t){let e=String(t.note);if(t.status){let n=String(t.status);return`Phase gate: no \`${e}\` note in \`.sandbox/notes/\` has \`status: ${n}\`, so source edits wait for an approved design. Invoke \`sb-research\`, present the design to the user, and when they approve write \`NNN-${e}-<slug>.md\` with \`status: ${n}\` recording what was approved. Then retry this edit. Edits under \`.sandbox/\` are not gated.`}return`Phase gate: no \`${e}\` note in \`.sandbox/notes/\` names the root cause yet. Invoke \`sb-research\`, reproduce the problem, and write \`NNN-${e}-<slug>.md\` stating the root cause. Then retry this edit. Edits under \`.sandbox/\` are not gated.`}o(z,"_phaseGateText");function H(t,e,n){if(!e||e.includes("/")||e.includes(".."))return null;let s=J(t);if(s===null)return null;let u=C(s);if(u===null)return null;let r=G(s,e);if(r===null)return null;let i=n?a.relative(s,n):"",l=u.phase_gate;if(h(l)&&i&&i!==".."&&!i.startsWith(".."+a.sep)&&!a.isAbsolute(i)&&!_.some(f=>i.startsWith(f+a.sep))&&!Y(s,l))return z(l);let m=new Set(Array.isArray(r.delivered)?r.delivered:[]),p=!r.phase_entry_delivered,d=null,b=h(u.domains)?u.domains:{};for(let f of k(Object.keys(b))){let y=b[f];if(m.has(f)||!h(y))continue;if((Array.isArray(y.applies_to)?y.applies_to:[]).some(j=>F(i,String(j)))){d=f;break}}if(!p&&d===null)return null;let g=[];if(p&&g.push(A),d!==null){let f=b[d],y=String(h(f)&&f.summary||"").trim(),x=`Standards gate: you are editing a path governed by the \`${d}\` standard, which has not been loaded this session. Invoke the \`sb-${d}-standard\` skill, then retry this edit.`;y&&(x+=` Summary: ${y}.`),g.push(x)}g.push(O);let D={phase_entry_delivered:!0,delivered:k(d?new Set([...m,d]):m)};return L(s,e,D)?g.join(`
4
+ `):null}o(H,"decide");function N(){let t;try{let i=process.stdin.isTTY?"":c.readFileSync(0,"utf8"),l=i.trim()?JSON.parse(i):{};t=h(l)?l:{}}catch{t={}}let e=String(t.session_id||""),n=t.tool_input,s=String(h(n)&&n.file_path||""),u=String(t.cwd||process.cwd()),r;try{r=H(u,e,s)}catch{r=null}r&&console.log(JSON.stringify({hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:r}}))}o(N,"main");N();
278
5
  //# sourceMappingURL=gate.js.map
@@ -1,75 +1,4 @@
1
1
  import { createRequire as __sandboxCreateRequire } from "node:module";
2
2
  const require = __sandboxCreateRequire(import.meta.url);
3
-
4
- // ../../packages/sandbox-core/dist/pr_guard.js
5
- import * as fs2 from "node:fs";
6
- import * as path2 from "node:path";
7
-
8
- // ../../packages/sandbox-core/dist/dot_dir.js
9
- import * as fs from "node:fs";
10
- import * as path from "node:path";
11
- var DOT_DIRS = [".werkmeister", ".sandbox"];
12
- function dotDir(parent) {
13
- const found = DOT_DIRS.find((d) => fs.existsSync(path.join(parent, d)));
14
- return path.join(parent, found ?? DOT_DIRS[0]);
15
- }
16
-
17
- // ../../packages/sandbox-core/dist/pr_guard.js
18
- function findRecordRoot(start) {
19
- let dir = path2.resolve(start);
20
- for (; ; ) {
21
- if (fs2.existsSync(path2.join(dotDir(dir), "workspace.yml")))
22
- return dir;
23
- const parent = path2.dirname(dir);
24
- if (parent === dir)
25
- return null;
26
- dir = parent;
27
- }
28
- }
29
- function prFileDirName(root) {
30
- try {
31
- const names = fs2.readdirSync(path2.join(root, "stages")).sort();
32
- const dir = names.find((n) => /^\d+_propose$/.test(n)) ?? names.find((n) => /^\d+_ship$/.test(n));
33
- return dir ? `stages/${dir}/output/` : "the propose stage's output/";
34
- } catch {
35
- return `${path2.basename(dotDir(root))}/notes/`;
36
- }
37
- }
38
- var GH_PR_CREATE = /\bgh\s+pr\s+create\b/;
39
- var MCP_CREATE_PR = /create_pull_request$/;
40
- function decide(input) {
41
- const root = findRecordRoot(String(input["cwd"] || process.cwd()));
42
- if (!root)
43
- return null;
44
- const tool = String(input["tool_name"] || "");
45
- const command = String(input["tool_input"]?.["command"] || "");
46
- const opensPr = tool === "Bash" && GH_PR_CREATE.test(command) || MCP_CREATE_PR.test(tool);
47
- if (!opensPr)
48
- return null;
49
- const rules = ["_shared/pr-rules.md", ...DOT_DIRS.map((d) => `${d}/pr-rules.md`)].find((p) => fs2.existsSync(path2.join(root, p))) ?? "docs/workspace-lifecycle.md in the sandbox repo";
50
- return `Inside sandbox workspace \`${path2.basename(root)}\`, PRs open only through \`sandbox pr open\`: it runs the pre-PR gate and records the URL. Write the PR file in \`${prFileDirName(root)}\`, have the person set \`status: approved\`, then run \`sandbox pr open\`. Rules: \`${rules}\`.`;
51
- }
52
- function main() {
53
- let reason = null;
54
- try {
55
- const raw = process.stdin.isTTY ? "" : fs2.readFileSync(0, "utf8");
56
- const parsed = raw.trim() ? JSON.parse(raw) : {};
57
- if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed))
58
- reason = decide(parsed);
59
- } catch {
60
- reason = null;
61
- }
62
- if (reason) {
63
- console.log(JSON.stringify({
64
- hookSpecificOutput: {
65
- hookEventName: "PreToolUse",
66
- permissionDecision: "deny",
67
- permissionDecisionReason: reason
68
- }
69
- }));
70
- }
71
- }
72
-
73
- // src/lib/pr_guard.ts
74
- main();
3
+ var h=Object.defineProperty;var n=(e,t)=>h(e,"name",{value:t,configurable:!0});import*as s from"node:fs";import*as r from"node:path";import*as u from"node:fs";import*as a from"node:path";var p=[".werkmeister",".sandbox"];function d(e){let t=p.find(o=>u.existsSync(a.join(e,o)));return a.join(e,t??p[0])}n(d,"dotDir");function x(e){let t=r.resolve(e);for(;;){if(s.existsSync(r.join(d(t),"workspace.yml")))return t;let o=r.dirname(t);if(o===t)return null;t=o}}n(x,"findRecordRoot");function y(e){try{let t=s.readdirSync(r.join(e,"stages")).sort(),o=t.find(i=>/^\d+_propose$/.test(i))??t.find(i=>/^\d+_ship$/.test(i));return o?`stages/${o}/output/`:"the propose stage's output/"}catch{return`${r.basename(d(e))}/notes/`}}n(y,"prFileDirName");var R=/\bgh\s+pr\s+create\b/,b=/create_pull_request$/;function S(e){let t=x(String(e.cwd||process.cwd()));if(!t)return null;let o=String(e.tool_name||""),i=String(e.tool_input?.command||"");if(!(o==="Bash"&&R.test(i)||b.test(o)))return null;let l=["_shared/pr-rules.md",...p.map(c=>`${c}/pr-rules.md`)].find(c=>s.existsSync(r.join(t,c)))??"docs/workspace-lifecycle.md in the sandbox repo";return`Inside sandbox workspace \`${r.basename(t)}\`, PRs open only through \`sandbox pr open\`: it runs the pre-PR gate and records the URL. Write the PR file in \`${y(t)}\`, have the person set \`status: approved\`, then run \`sandbox pr open\`. Rules: \`${l}\`.`}n(S,"decide");function m(){let e=null;try{let t=process.stdin.isTTY?"":s.readFileSync(0,"utf8"),o=t.trim()?JSON.parse(t):{};typeof o=="object"&&o!==null&&!Array.isArray(o)&&(e=S(o))}catch{e=null}e&&console.log(JSON.stringify({hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:e}}))}n(m,"main");m();
75
4
  //# sourceMappingURL=pr_guard.js.map