shapeup-sdlc 3.11.0 → 3.12.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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "shapeup-sdlc-plugin",
3
3
  "displayName": "ShapeUp SDLC Plugin",
4
- "version": "3.11.0",
4
+ "version": "3.12.0",
5
5
  "description": "Shape Up SDLC harness for Claude Code: shaping, intake, orient, scope-mapping, building (T0-verified, sandboxed, scope-contracted), evaluation and QA skills orchestrated by a tech-lead.",
6
6
  "author": {
7
7
  "name": "Liberty Nguyen",
package/SECURITY.md CHANGED
@@ -58,7 +58,10 @@ test against machines you don't own.
58
58
  6. **Every hook decision is recorded.** `hooks/lib/decision.mjs` is the only exit path a hook
59
59
  has, so allow, deny, block and error each leave a row in `.shapeup/decisions.jsonl`. An
60
60
  inert hook and a permitting hook are therefore distinguishable — which matters, because
61
- "exit 0, no output" is what both used to look like.
61
+ "exit 0, no output" is what both used to look like. A permitted Bash call's row names the
62
+ programs the command runs, by basename (`hdc`, `curl | jq`) — never the arguments, which is
63
+ where a secret, a token or a private path would be. A denied call's row keeps the first 200
64
+ characters of the command, as it always has, because a denial must be reviewable.
62
65
 
63
66
  If you find any of these to be false, that is a vulnerability — report it as claim #ⁿ.
64
67
 
@@ -96,6 +96,32 @@ function tokens(segment) {
96
96
  .map((t) => t.replace(/^['"]|['"]$/g, ""));
97
97
  }
98
98
 
99
+ /**
100
+ * The programs a command runs — the executable of each `&&`/`;`/`|` segment, by basename, and
101
+ * nothing else.
102
+ *
103
+ * WHY AN ALLOW ROW NAMES THE PROGRAM. Every permitted Bash call used to be recorded with no subject,
104
+ * so "did this worker ever call the device tool, and was it refused?" had no answer in the ledger:
105
+ * a sub-agent that never tried and one whose call was stopped above this hook left the same rows.
106
+ * The program names answer it. Arguments are never recorded — they are where a secret, a token or a
107
+ * private path would be — and a basename says which tool ran without saying where it lives.
108
+ *
109
+ * @param {string} command - The raw Bash command.
110
+ * @returns {(string|null)} Program basenames joined by " | ", in order, duplicates kept; null when
111
+ * no segment yields one.
112
+ */
113
+ export function programsOf(command) {
114
+ if (!command || typeof command !== "string") return null;
115
+ const names = [];
116
+ for (const segment of command.split(/\s*(?:\|\||&&|;|\||\n)\s*/).filter(Boolean)) {
117
+ const first = commandTokens(segment)[0];
118
+ if (!first) continue;
119
+ const base = first.replace(/^["']|["']$/g, "").split("/").pop();
120
+ if (base && !/^[({]$/.test(base)) names.push(base.slice(0, 64));
121
+ }
122
+ return names.length ? names.join(" | ") : null;
123
+ }
124
+
99
125
  /** Strip leading env assignments and privilege/no-op wrappers to find the real command. */
100
126
  function commandTokens(segment) {
101
127
  const ts = tokens(segment);
@@ -217,8 +243,9 @@ async function main() {
217
243
  const raw = await readStdin();
218
244
  let p;
219
245
  /** Fail-open, with the reason on the record (hooks/lib/decision.mjs). */
220
- const defer = (reason, rule) => settle({
246
+ const defer = (reason, rule, subject = null) => settle({
221
247
  verdict: "allow", event: "PreToolUse", tool: p?.tool_name ?? null, cwd: p?.cwd, reason, rule,
248
+ ...(subject ? { subject } : {}),
222
249
  });
223
250
  try { p = JSON.parse(raw || "{}"); }
224
251
  catch (e) { settle({ verdict: "error", event: "PreToolUse", reason: `unparseable payload: ${e.message}` }); }
@@ -258,7 +285,7 @@ async function main() {
258
285
  if (p.tool_name === "Bash") {
259
286
  const command = p.tool_input?.command || "";
260
287
  const verdict = classifyCommand(command, overrides);
261
- if (!verdict.deny) defer("command matched no destructive rule — inspected and permitted", "bash-clean");
288
+ if (!verdict.deny) defer("command matched no destructive rule — inspected and permitted", "bash-clean", programsOf(command));
262
289
  if (commandOverridden(command, overrides)) {
263
290
  // Exercised override: allowed, but never invisible.
264
291
  logPathology(metricsPath, {
@@ -33,6 +33,12 @@ const PATTERNS = [
33
33
  // ("ERROR in the build pipeline", "ERROR in test suite failed to run") is left unmatched
34
34
  // instead of handing back a fabricated file.
35
35
  { re: /^(?:ERROR|WARNING)\s+in\s+(\.{1,2}\/[^\s:]*|[^\s:]+\.[A-Za-z0-9]{1,10})\b/i, kind: "compiler-diagnostic" },
36
+ // A test that FAILED BY NAME, with no file:line: "FAIL TS-05-05 step 4: no text 'Bread' on screen"
37
+ // or jest's "FAIL src/cart.test.js". Runners that drive an app from outside it (a device flow, an
38
+ // end-to-end script) report a case this way and nothing else, and the line is the whole signal —
39
+ // dropping it handed the next attempt an empty error list over a red fixture. The name is kept as
40
+ // the file only when it looks like a path; an id like TS-05-05 is not one.
41
+ { re: /^(?:FAIL|FAILED)\s+(\S+)(?:\s+.*)?$/, kind: "named-test-failure" },
36
42
  // Generic "Error: message" line followed later by a stack — capture the message alone.
37
43
  { re: /^\s*(?:Error|TypeError|ReferenceError|AssertionError)\s*:\s*(.+)$/, kind: "error-message" },
38
44
  ];
@@ -71,8 +77,9 @@ export function digest(rawText) {
71
77
  pendingMessage = coreMessage(m[1]);
72
78
  continue; // wait for the stack frame that follows to get a file:line
73
79
  }
74
- const file = m[1]?.trim();
75
- const lineNo = m[2] ? Number(m[2]) : null;
80
+ const named = kind === "named-test-failure";
81
+ const file = named ? (/[\\/]|\.[A-Za-z0-9]{1,10}$/.test(m[1]) ? m[1] : null) : m[1]?.trim();
82
+ const lineNo = !named && m[2] ? Number(m[2]) : null;
76
83
  triples.push({
77
84
  file: file || null,
78
85
  line: lineNo,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shapeup-sdlc",
3
- "version": "3.11.0",
3
+ "version": "3.12.0",
4
4
  "description": "Shape Up for coding agents \u2014 with gates the agent can't talk its way past. Harness for Claude Code.",
5
5
  "bin": {
6
6
  "shapeup-sdlc": "bin/init.mjs"
@@ -83,6 +83,11 @@ Done-when statements; `_index.md` Non-Go list. Which UCs are in scope comes from
83
83
  freeze through the judge). Ugly-but-correct PASSes; pretty-but-wrong-`data-state` FAILs.
84
84
  - `[data]`: query the DB/storage, capture actual state.
85
85
  - Contract work: send real requests, compare field-by-field.
86
+ - **A fixture that names a row is evidence for that row.** When a T0 artifact you cite, or the
87
+ build gate, carries output that names a Test Surface row by id — `PASS TS-05-05`, or
88
+ `FAIL TS-05-05 step 4: …` — grade that row on it: a named PASS confirms, a named FAIL is a
89
+ FAIL whose bug is that line. This is how a device row is evidenced when you cannot drive the
90
+ app yourself; it is never a reason to skip driving it when you can.
86
91
  - No evidence collected = recorded "NO EVIDENCE" → FAILs at verdict.
87
92
 
88
93
  **VERDICT.**
@@ -25,7 +25,7 @@ rely on (anything absent = **unknown**; never invent it):
25
25
  | `payload.scope_contract` | The active scope: `affordance_manifest`, `e2e_verification_fixtures`, topology |
26
26
  | `substrate.allowed` / `substrate.shared` | The ONLY globs you may write. A needed file outside them → ESCALATE, never a write (a sandbox hook blocks it anyway) |
27
27
  | `payload.decisions[]` | Adjudicated answers from prior escalations — binding precedent, apply them |
28
- | `payload.digested_errors[]` | `{file, line, core_message}` triples from the previous attempt's failed verification — your starting bug list |
28
+ | `payload.digested_errors[]` | `{file, line, core_message}` triples from the previous attempt's failed verification (a test that failed by name with no file:line — `FAIL TS-05-05 step 4: …` — arrives with `file: null` and the whole line as its message) — your starting bug list |
29
29
  | `payload.trial_history[]` | Up to 8 prior attempts on this scope, oldest first, CROSSING the round boundary: `{score, status, delta, digest}`. `status: "reverted"` is a change that was tried and made things WORSE — do not re-propose it. `status: "kept"` with a still-red score is the tree you are building ON, not a failure to undo. Absent on the first attempt |
30
30
  | `payload.verify.test_cmd` | The command that verifies your work. No test_cmd → command-verifiable ACs still need *some* observable check; say what you used |
31
31
  | `payload.kb_rules_path` | Team guidelines (read if the file exists) — steering, never spec; conflict → the AC wins, note it in `deviations` |