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.
|
|
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
|
|
package/hooks/safety-spine.mjs
CHANGED
|
@@ -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, {
|
package/kernel/probe/digest.mjs
CHANGED
|
@@ -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
|
|
75
|
-
const
|
|
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
|
@@ -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` |
|