shapeup-sdlc 3.16.0 → 3.16.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/.claude-plugin/plugin.json +1 -1
- package/commands/ship.md +4 -2
- package/kernel/gate.mjs +33 -1
- package/kernel/reduce/ship.mjs +18 -7
- package/package.json +1 -1
- package/skills/tech-lead/SKILL.md +4 -4
- package/skills/tech-lead/references/gates.md +3 -1
- package/skills/tech-lead/workflows/shapeup-run.js +6 -2
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "shapeup-sdlc-plugin",
|
|
3
3
|
"displayName": "ShapeUp SDLC Plugin",
|
|
4
|
-
"version": "3.16.
|
|
4
|
+
"version": "3.16.1",
|
|
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/commands/ship.md
CHANGED
|
@@ -99,6 +99,8 @@ Additional flags, pass through to `tech-lead` only when the user names them:
|
|
|
99
99
|
- `--rounds N` → override the outer circuit breaker (build+eval cycles, default 3).
|
|
100
100
|
- `--attempts N` → override the inner circuit breaker (per-scope T0 attempts, default 5;
|
|
101
101
|
no-op on specs without scope contracts).
|
|
102
|
-
- `--
|
|
102
|
+
- `--exec-model / --eval-model / --qa-model <name>` → override GATE L0.8's
|
|
103
103
|
resolved model matrix for this run only (highest precedence over `.claude/settings.local.json`
|
|
104
|
-
/ `.claude/settings.json` / skill defaults).
|
|
104
|
+
/ `.claude/settings.json` / skill defaults). `--orch-model` is shown in the L0 block and changes
|
|
105
|
+
nothing: the orchestrator is the session already running this command, so its model is the one
|
|
106
|
+
that session was started with.
|
package/kernel/gate.mjs
CHANGED
|
@@ -54,7 +54,7 @@ import { readFileSync, writeFileSync, appendFileSync, existsSync, mkdirSync } fr
|
|
|
54
54
|
import { parseBoard } from "./reduce/board.mjs";
|
|
55
55
|
import { join, dirname } from "node:path";
|
|
56
56
|
import { runArgs } from "./lib/argv.mjs";
|
|
57
|
-
import { gateAnswerCandidates, gates as gatesPath, LOCAL, resultsDir, tasksDir, hammerCensus, readRunId } from "./lib/paths.mjs";
|
|
57
|
+
import { gateAnswerCandidates, gates as gatesPath, LOCAL, resultsDir, tasksDir, hammerCensus, readRunId, harnessRun } from "./lib/paths.mjs";
|
|
58
58
|
|
|
59
59
|
export const GATE_IDS = ["L0", "L1a", "L1a.5", "L1b", "L2", "L3", "QA", "H", "L4", "COACH-1"];
|
|
60
60
|
|
|
@@ -327,6 +327,34 @@ export function appendGateLedger(cwd, slug, row) {
|
|
|
327
327
|
} catch { return false; }
|
|
328
328
|
}
|
|
329
329
|
|
|
330
|
+
/**
|
|
331
|
+
* This run's earlier ship sign-off, if it already has one.
|
|
332
|
+
*
|
|
333
|
+
* A launched run crosses L4 itself and then closes, and the orchestrator's own closing step resolved
|
|
334
|
+
* it again afterwards — a second `ship` row, after the close, for a decision already taken. Once the
|
|
335
|
+
* run is closed, its decided L4 row is returned instead of resolving again. An open run still
|
|
336
|
+
* re-resolves (the census it reads may have changed), a reopened run has no close, and a paused or
|
|
337
|
+
* aborted row is not a sign-off.
|
|
338
|
+
*
|
|
339
|
+
* @param {string} cwd - Project root.
|
|
340
|
+
* @param {string} slug - Feature slug.
|
|
341
|
+
* @param {string|null} runId - The run the resolve belongs to.
|
|
342
|
+
* @returns {(object|null)} The earlier L4 row, or null.
|
|
343
|
+
*/
|
|
344
|
+
export function priorSignOff(cwd, slug, runId) {
|
|
345
|
+
if (!runId) return null;
|
|
346
|
+
let ledger = "";
|
|
347
|
+
try { ledger = readFileSync(harnessRun(cwd, slug), "utf8"); } catch { return null; }
|
|
348
|
+
if (!/^closed_status:[ \t]*[^\s~]/m.test(ledger)) return null;
|
|
349
|
+
let text = "";
|
|
350
|
+
try { text = readFileSync(gatesPath(cwd, slug), "utf8"); } catch { return null; }
|
|
351
|
+
for (const line of text.split("\n")) {
|
|
352
|
+
let row; try { row = JSON.parse(line); } catch { continue; }
|
|
353
|
+
if (row?.gate === "L4" && row.run_id === runId && row.status === "ok") return row;
|
|
354
|
+
}
|
|
355
|
+
return null;
|
|
356
|
+
}
|
|
357
|
+
|
|
330
358
|
/** Gates this lane will actually hit — used by --verify to catch a set that stalls halfway. */
|
|
331
359
|
export function requiredGates({ autoLevel = "unattended", tiny = false, qa = true } = {}) {
|
|
332
360
|
if (tiny) return ["L0", "L4"];
|
|
@@ -483,6 +511,10 @@ export function cli(rawArgv) {
|
|
|
483
511
|
const gate = args.resolve ?? null;
|
|
484
512
|
if (!gate) die("nothing to do — pass --init, --list, --verify, or --resolve <gate-id>");
|
|
485
513
|
|
|
514
|
+
if (gate === "L4" && args.slug) {
|
|
515
|
+
const prior = priorSignOff(cwd, args.slug, readRunId(cwd, args.slug));
|
|
516
|
+
if (prior) out({ ok: true, gate: "L4", status: prior.status, decision: prior.decision, source: prior.source, already_resolved_at: prior.at }, 0);
|
|
517
|
+
}
|
|
486
518
|
const r = narrowToEvidence(resolve(found.set, gate, found.source), cwd, args.slug ?? null);
|
|
487
519
|
if (r.status === "error") die(r.reason);
|
|
488
520
|
// A gate with no `--slug` (e.g. `--file` used ad hoc, outside any run) has nowhere to file a
|
package/kernel/reduce/ship.mjs
CHANGED
|
@@ -32,7 +32,7 @@ import { runArgs } from "../lib/argv.mjs";
|
|
|
32
32
|
import {
|
|
33
33
|
report as reportPath, tasksDir, verdictsDir, trials, evaluationDir, qaDir,
|
|
34
34
|
roundLedger, discoveryLedger, receipt as receiptPath, harnessRun, relShared,
|
|
35
|
-
activeOrder, runArgsPath, readReceipt, runIdFromReceipt, hammerCensus,
|
|
35
|
+
activeOrder, runArgsPath, readReceipt, runIdFromReceipt, hammerCensus, LOCAL,
|
|
36
36
|
} from "../lib/paths.mjs";
|
|
37
37
|
import { readTrials } from "../verify/t0.mjs";
|
|
38
38
|
import { ratchetReport } from "../probe/stats.mjs";
|
|
@@ -112,16 +112,27 @@ export function boardCensus(cwd, slug) {
|
|
|
112
112
|
* An id with no resolvable use case becomes a neutral phrase rather than the id: the report loses a
|
|
113
113
|
* pointer that never resolved off this machine anyway, and keeps the sentence around it.
|
|
114
114
|
*
|
|
115
|
+
* A path into the run trace is the same leak by another spelling: the QA findings section quotes
|
|
116
|
+
* the hunt report, which points at the discovery ledger by its local path, and the next run's
|
|
117
|
+
* spec-lint refused the report for it. The path becomes "the run trace", and the sentence stays.
|
|
118
|
+
*
|
|
115
119
|
* @param {*} text - Any value destined for the committed report.
|
|
116
120
|
* @param {Record<string, string[]>} anchors - Board id → its `use_case_refs`.
|
|
117
|
-
* @returns {string} The text with every `TASK-…` replaced by a stable anchor
|
|
121
|
+
* @returns {string} The text with every `TASK-…` replaced by a stable anchor and every run-trace
|
|
122
|
+
* path by a phrase.
|
|
118
123
|
*/
|
|
119
124
|
export function deboard(text, anchors = {}) {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
+
const esc = LOCAL.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
126
|
+
return String(text ?? "")
|
|
127
|
+
.replace(/\bTASK-[A-Za-z0-9][\w.-]*/g, (id) => {
|
|
128
|
+
const ucs = anchors[id];
|
|
129
|
+
if (ucs && ucs.length) return ucs.join("/");
|
|
130
|
+
return "a board task";
|
|
131
|
+
})
|
|
132
|
+
.replace(new RegExp(`\`?${esc}/[^\\s\`]+\`?`, "g"), (m) => {
|
|
133
|
+
const tail = (m.replace(/`/g, "").match(/[)\]},.;:'"]+$/) || [""])[0];
|
|
134
|
+
return `the run trace${tail}`;
|
|
135
|
+
});
|
|
125
136
|
}
|
|
126
137
|
|
|
127
138
|
/**
|
package/package.json
CHANGED
|
@@ -110,11 +110,11 @@ failed, launch from the install path and have the operator `/add-dir` the plugin
|
|
|
110
110
|
did not actually receive from the PO — an unattended lane with no answer for a gate is meant to
|
|
111
111
|
`abort` (see `harness gate`'s `on_missing`), not silently proceed.
|
|
112
112
|
|
|
113
|
-
## Step 4 — GATE L4 — Ship Sign-Off
|
|
113
|
+
## Step 4 — GATE L4 — Ship Sign-Off
|
|
114
114
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
115
|
+
A `shipped`/`escalated` return already crossed L4 in the run and froze `REPORT.md`: never resolve it
|
|
116
|
+
again (a row after the close is a second sign-off). A return that did not reach L4 freezes the report
|
|
117
|
+
now and RESOLVES the gate (`references/gates.md` GATE L4). Either way, emit:
|
|
118
118
|
|
|
119
119
|
```
|
|
120
120
|
⏸ GATE L4 — Ship Sign-Off
|
|
@@ -571,7 +571,9 @@ On confirm:
|
|
|
571
571
|
- If the PO provides substantive feedback (not just 'y' or empty) → automatically delegate via Agent (model: exec — see references/protocol.md "Invocation mechanism"): Skill(shapeup-sdlc-plugin:coach) with the provided feedback for RLHF. The coach runs its own GATE COACH-1 to have the PO categorize each rule, then files it under the responsible skill in `shapeup/knowledge-base/<skill>.md` (committed → team-shared). Coachable: `task-executor`, `ba-pitch-analyzer`, `qa-edge-hunter`, `orient`, `scope-architect`, `solution-architect` (each reads its own file at the top of its next run) and `tech-lead` (workflow guidance, read at the next GATE L0). Guidance never decides a gate: a filed rule may add a question or a check to a gate block, never an answer. The tech lead does not categorize the feedback itself — that is the coach's gate, by design (no assumptions).
|
|
572
572
|
- Then output → `✅ [slug] [shipped & deployed | built & verified, deploy pending] — [r] rounds, verdict PASS.`
|
|
573
573
|
|
|
574
|
-
**
|
|
574
|
+
**A launched run already crossed L4 — never resolve it again after a `shipped` or `escalated`
|
|
575
|
+
return; a second row after the close is a second sign-off.** In the prose lane, **resolve the gate
|
|
576
|
+
itself before any of the above** — this is the decision that shipped the run,
|
|
575
577
|
and without it the trace holds no record of that decision at all: `node
|
|
576
578
|
"${CLAUDE_PLUGIN_ROOT}/kernel/harness.mjs" gate --resolve L4 --slug <slug>
|
|
577
579
|
[--file <path>|--preset <name>]`. Exit 0 (`decision=ship|hold`) — render the block above and close
|
|
@@ -112,6 +112,10 @@ const argProblems = validateArgs(args);
|
|
|
112
112
|
if (argProblems.length) return { status: "aborted", aborted_at: "args", reason: argProblems.join("; ") };
|
|
113
113
|
|
|
114
114
|
const slug = args.slug;
|
|
115
|
+
// The report's path, never the ship command's one-line `detail`: that is a sub-agent's sentence, and
|
|
116
|
+
// it reached the RunReturn's `report` field in place of the path the orchestrator opens. The
|
|
117
|
+
// committed root is a fixed name, so the path is known without reading anything.
|
|
118
|
+
const REPORT_PATH = `shapeup/${slug}/REPORT.md`;
|
|
115
119
|
const KERNEL = `${args.pluginRoot}/kernel/harness.mjs`;
|
|
116
120
|
const execModel = args.models.exec;
|
|
117
121
|
const evalModel = args.models.eval;
|
|
@@ -697,7 +701,7 @@ async function settleAtGateH(ret) {
|
|
|
697
701
|
status: "shipped", verdict, rounds_used: round, after: "gate_h", breaker: ret.breaker ?? null,
|
|
698
702
|
census: h.verdict, cut_list: h.cut_list, green_scopes: ret.green_scopes,
|
|
699
703
|
unapplied_results: ret.unapplied_results || [], qa_findings: 0,
|
|
700
|
-
report:
|
|
704
|
+
report: REPORT_PATH,
|
|
701
705
|
});
|
|
702
706
|
}
|
|
703
707
|
|
|
@@ -2012,7 +2016,7 @@ return await withWarnings({
|
|
|
2012
2016
|
rounds_used: round,
|
|
2013
2017
|
dims_not_evaluated: ALL_DIMS.filter((d) => !evalDims.includes(d)),
|
|
2014
2018
|
qa_findings: qaFindings,
|
|
2015
|
-
report:
|
|
2019
|
+
report: REPORT_PATH,
|
|
2016
2020
|
});
|
|
2017
2021
|
|
|
2018
2022
|
// =============================================================================================
|