shapeup-sdlc 1.6.2 → 1.7.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/.claude-plugin/plugin.json +1 -1
- package/AGENTS.md +36 -103
- package/README.md +60 -67
- package/SECURITY.md +31 -16
- package/bin/init.mjs +24 -69
- package/commands/build.md +19 -0
- package/commands/eval.md +5 -4
- package/commands/scopes.md +5 -4
- package/commands/shape.md +1 -1
- package/commands/ship.md +37 -4
- package/hooks/anti-rationalization.mjs +7 -13
- package/hooks/compact-snapshot.mjs +5 -5
- package/hooks/gate-deadline.mjs +12 -16
- package/hooks/gate-intake.mjs +13 -14
- package/hooks/gate-l2.mjs +3 -3
- package/hooks/gate-zerowork.mjs +97 -16
- package/hooks/lib/decision.mjs +21 -13
- package/hooks/safety-spine.mjs +1 -1
- package/hooks/sandbox-guard.mjs +83 -47
- package/hooks/session-rehydrate.mjs +8 -9
- package/hooks/slop-cleaner.mjs +1 -1
- package/oracles/_shared.mjs +1 -1
- package/oracles/http-oracle.mjs +1 -1
- package/oracles/index.mjs +1 -1
- package/oracles/process-oracle.mjs +1 -1
- package/oracles/snapshot-oracle.mjs +1 -1
- package/oracles/test-oracle.mjs +1 -1
- package/package.json +3 -7
- package/skills/ba-pitch-analyzer/SKILL.md +20 -15
- package/skills/ba-pitch-analyzer/references/doc-schemas.md +1 -1
- package/skills/ba-pitch-analyzer/references/task-generation.md +6 -6
- package/skills/ba-pitch-analyzer/references/test-surface.md +1 -1
- package/skills/ba-pitch-analyzer/scripts/board-derive.mjs +5 -5
- package/skills/ba-pitch-analyzer/scripts/spec-lint.mjs +2 -2
- package/skills/coach/SKILL.md +3 -3
- package/skills/orient/SKILL.md +2 -1
- package/skills/qa-edge-hunter/SKILL.md +9 -39
- package/skills/scope-architect/SKILL.md +17 -12
- package/skills/scope-hammer/SKILL.md +5 -5
- package/skills/shapeup/SKILL.md +3 -3
- package/skills/shapeup/resources/context-compaction.md +4 -3
- package/skills/solution-architect/SKILL.md +18 -11
- package/skills/spec-evaluator/SKILL.md +12 -5
- package/skills/spec-evaluator/references/dimensions/_registry.md +2 -2
- package/skills/spec-evaluator/references/dimensions/completeness.md +1 -1
- package/skills/spec-evaluator/references/dimensions/integration.md +98 -67
- package/skills/spec-evaluator/references/dimensions/spec-conformance.md +4 -4
- package/skills/spec-evaluator/references/dimensions/test-surface-conformance.md +1 -1
- package/skills/spec-evaluator/references/probing.md +42 -23
- package/skills/spec-evaluator/references/report-schema.md +2 -2
- package/skills/spec-evaluator/scripts/verdict-ledger.mjs +1 -1
- package/skills/task-executor/SKILL.md +25 -12
- package/skills/tech-lead/SKILL.md +115 -438
- package/skills/tech-lead/references/delegation.md +38 -31
- package/skills/tech-lead/references/gates.md +75 -33
- package/skills/tech-lead/references/hard-rules.md +34 -0
- package/skills/tech-lead/references/invocation.md +2 -2
- package/skills/tech-lead/references/ledger-schema.md +17 -18
- package/skills/tech-lead/references/round-protocol.md +77 -27
- package/skills/tech-lead/references/state-model.md +1 -1
- package/skills/tech-lead/schemas/domain.schema.json +320 -148
- package/skills/tech-lead/schemas/gate-answers.schema.json +2 -2
- package/skills/tech-lead/schemas/work-result.schema.json +55 -17
- package/skills/tech-lead/scripts/aegis-digest.mjs +1 -1
- package/skills/tech-lead/scripts/budget-check.mjs +7 -13
- package/skills/tech-lead/scripts/compile-order.mjs +69 -22
- package/skills/tech-lead/scripts/fit-check.mjs +10 -10
- package/skills/tech-lead/scripts/gate-answers.mjs +11 -12
- package/skills/tech-lead/scripts/hill-derive.mjs +133 -0
- package/skills/tech-lead/scripts/ingest-result.mjs +7 -19
- package/skills/tech-lead/scripts/init-run.mjs +60 -18
- package/skills/tech-lead/scripts/lib/argv.mjs +1 -1
- package/skills/tech-lead/scripts/lib/contract-md.mjs +10 -10
- package/skills/tech-lead/scripts/lib/is-main.mjs +11 -12
- package/skills/tech-lead/scripts/lib/paths.mjs +15 -13
- package/skills/tech-lead/scripts/resume-state.mjs +387 -0
- package/skills/tech-lead/scripts/run-snapshot.mjs +1 -6
- package/skills/tech-lead/scripts/run-workflow.mjs +381 -0
- package/skills/tech-lead/scripts/ship-report.mjs +33 -2
- package/skills/tech-lead/scripts/stats.mjs +9 -10
- package/skills/tech-lead/scripts/t0-verify.mjs +4 -4
- package/skills/tech-lead/scripts/trace-lint.mjs +2 -2
- package/skills/tech-lead/workflows/shapeup-run.js +911 -0
- package/skills/translator/SKILL.md +2 -2
- package/skills/advisor-protocol/SKILL.md +0 -171
|
@@ -1,18 +1,17 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// GATE L0.1 — START THE RUN. The orchestrator's first tool call, before any prose.
|
|
3
3
|
//
|
|
4
|
-
// WHY THIS EXISTS (
|
|
4
|
+
// WHY THIS EXISTS (reproduced, not theorized).
|
|
5
5
|
//
|
|
6
|
-
//
|
|
7
|
-
// orchestrator was dispatched with a valid spec and did this:
|
|
6
|
+
// Dispatched with a valid spec, the orchestrator did this:
|
|
8
7
|
//
|
|
9
|
-
// TOOL Skill(tech-lead, "--unattended --rounds 3\n\n#
|
|
8
|
+
// TOOL Skill(tech-lead, "--unattended --rounds 3\n\n# the requirement…")
|
|
10
9
|
// TEXT "The tech-lead skill is orchestrating the full Shape Up harness. It will: 1. …"
|
|
11
10
|
// FINAL (same text — session ends)
|
|
12
11
|
//
|
|
13
|
-
// It loaded
|
|
14
|
-
//
|
|
15
|
-
//
|
|
12
|
+
// It loaded an instruction file describing eleven gates and returned a description of eleven
|
|
13
|
+
// gates. No code, no board, no gate artifacts — and prose that reads exactly like a successful
|
|
14
|
+
// run, with every defect still in the deliverable.
|
|
16
15
|
//
|
|
17
16
|
// Two guards existed and neither could see it:
|
|
18
17
|
// • `gate-intake.mjs` (L0.0) fires on an EMPTY intake. Intake was valid here. Correct no-op.
|
|
@@ -34,10 +33,10 @@
|
|
|
34
33
|
// assumed someone had established. Previously that someone was the model, deciding to.
|
|
35
34
|
// An invariant that depends on the model choosing to establish it is a prompt, not a gate.
|
|
36
35
|
//
|
|
37
|
-
// It also HASHES the intake into the receipt. The
|
|
38
|
-
// requirement text
|
|
39
|
-
//
|
|
40
|
-
//
|
|
36
|
+
// It also HASHES the intake into the receipt. The first (wrong) diagnosis of the failure above was
|
|
37
|
+
// that requirement text had been dropped on the hand-off. It had not — but nothing on disk could
|
|
38
|
+
// settle that either way. Now it can: the intake that reached the orchestrator is recorded
|
|
39
|
+
// verbatim next to its digest, so "the spec was dropped" is checkable, not arguable.
|
|
41
40
|
//
|
|
42
41
|
// USAGE
|
|
43
42
|
// node init-run.mjs --slug <slug> --intake-file <path> [options] <- prefer this
|
|
@@ -46,13 +45,14 @@
|
|
|
46
45
|
//
|
|
47
46
|
// PREFER --intake-file. A multi-line requirement inlined into a shell argument is where this step
|
|
48
47
|
// goes wrong: quoting breaks, a `#` after a newline trips path validation, and the run spends six
|
|
49
|
-
// turns fighting its own command line instead of starting.
|
|
48
|
+
// turns fighting its own command line instead of starting. Observed, repeatedly.
|
|
50
49
|
//
|
|
51
50
|
// --auto-level interactive | auto | unattended (default: interactive)
|
|
52
51
|
// --lens lite | standard | cross-context (default: standard)
|
|
53
52
|
// --max-rounds N outer circuit breaker (default: 3)
|
|
54
53
|
// --attempts N inner per-scope T0 budget (default: 5)
|
|
55
54
|
// --spec-folder SHARED spec deliverable path (default: shapeup/<slug>/spec/)
|
|
55
|
+
// --dimensions comma-separated eval dimensions (default: spec-conformance)
|
|
56
56
|
// --gate-answers path | preset name (see gate-answers.mjs; recorded, not read)
|
|
57
57
|
// --wall-clock-budget N deadline breaker, seconds (off by default; see budget-check.mjs)
|
|
58
58
|
// --cwd project root (default: process.cwd())
|
|
@@ -74,6 +74,7 @@ import { createHash } from "node:crypto";
|
|
|
74
74
|
import { decideLane, treeSize } from "./fit-check.mjs";
|
|
75
75
|
import { isMain } from "./lib/is-main.mjs";
|
|
76
76
|
import { runArgs } from "./lib/argv.mjs";
|
|
77
|
+
import { uncoerce } from "./lib/contract-md.mjs";
|
|
77
78
|
import { deriveSnapshot } from "./run-snapshot.mjs";
|
|
78
79
|
import { localRoot, activeScope, globLocal, globShared } from "./lib/paths.mjs";
|
|
79
80
|
|
|
@@ -82,6 +83,35 @@ export const RECEIPT_VERSION = 1;
|
|
|
82
83
|
const AUTO_LEVELS = new Set(["interactive", "auto", "unattended"]);
|
|
83
84
|
const LENSES = new Set(["lite", "standard", "cross-context"]);
|
|
84
85
|
|
|
86
|
+
/**
|
|
87
|
+
* The eval dimension set when the caller names none. Kept to the base correctness dimension so an
|
|
88
|
+
* unconfigured run behaves exactly as it did before this flag existed.
|
|
89
|
+
*/
|
|
90
|
+
export const DEFAULT_DIMENSIONS = ["spec-conformance"];
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Parse `--dimensions` into the set written to the ledger. Shape-validated only, NOT checked against
|
|
94
|
+
* the dimensions that ship: adding `references/dimensions/<id>.md` and naming it here is the
|
|
95
|
+
* documented injection path, so a closed list here would make the evaluator's own extension point
|
|
96
|
+
* unreachable. An id with no file behind it is skipped-with-a-warning at dimension resolution, which
|
|
97
|
+
* is where that check belongs and where it can actually see the files.
|
|
98
|
+
*
|
|
99
|
+
* @param {(string|null|undefined)} raw - The comma-separated flag value; absent → the default set.
|
|
100
|
+
* @returns {string[]} Trimmed, de-duplicated ids in the caller's order.
|
|
101
|
+
* @throws {Error} If the list is empty or an entry is not a kebab-case id.
|
|
102
|
+
*/
|
|
103
|
+
export function parseDimensions(raw) {
|
|
104
|
+
if (raw === null || raw === undefined) return [...DEFAULT_DIMENSIONS];
|
|
105
|
+
const ids = String(raw).split(",").map((s) => s.trim()).filter(Boolean);
|
|
106
|
+
if (!ids.length) throw new Error("--dimensions: empty list — omit the flag to use the default [spec-conformance]");
|
|
107
|
+
for (const id of ids) {
|
|
108
|
+
if (!/^[a-z][a-z0-9]*(-[a-z0-9]+)*$/.test(id)) {
|
|
109
|
+
throw new Error(`--dimensions: "${id}" is not a dimension id (kebab-case, e.g. spec-conformance, tdd-surface, integration)`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return [...new Set(ids)];
|
|
113
|
+
}
|
|
114
|
+
|
|
85
115
|
/** Slugify a free-text feature name into a filesystem-safe run id. */
|
|
86
116
|
export function slugify(text) {
|
|
87
117
|
return String(text || "")
|
|
@@ -124,7 +154,10 @@ export function runFrontmatter({ slug, config, startedAt }) {
|
|
|
124
154
|
`feature: ${slug}`,
|
|
125
155
|
`spec_folder: ${config.spec_folder}`,
|
|
126
156
|
`lens: ${config.lens}`,
|
|
127
|
-
|
|
157
|
+
// The ledger is the ONE place the dimension set lives: resume-state reads it back off this line
|
|
158
|
+
// and the workflow hands it to every evaluate order. Written through the same list dialect the
|
|
159
|
+
// parser reads (contract-md), never hand-joined.
|
|
160
|
+
`eval_dimensions: ${uncoerce(config.eval_dimensions ?? DEFAULT_DIMENSIONS)}`,
|
|
128
161
|
`max_rounds: ${config.max_rounds}`,
|
|
129
162
|
`attempt_budget: ${config.attempt_budget}`,
|
|
130
163
|
`wall_clock_budget_s: ${config.wall_clock_budget_s ?? "~"}`,
|
|
@@ -164,7 +197,8 @@ export function runFrontmatter({ slug, config, startedAt }) {
|
|
|
164
197
|
export const ARGV_SPEC = {
|
|
165
198
|
usage: 'init-run.mjs (--intake-file <path> | --intake-text "<req>" | --intake-stdin) ' +
|
|
166
199
|
"[--slug <slug>] [--auto-level interactive|auto|unattended] [--lens <lens>] " +
|
|
167
|
-
"[--max-rounds N] [--attempts N] [--spec-folder <dir>] [--
|
|
200
|
+
"[--max-rounds N] [--attempts N] [--spec-folder <dir>] [--dimensions <a,b>] " +
|
|
201
|
+
"[--gate-answers <preset|path>] " +
|
|
168
202
|
"[--lane full|tiny] [--tiny] [--wall-clock-budget <seconds>] [--cwd <dir>] [--force]",
|
|
169
203
|
_: { arity: 0, max: 0, name: "(no positional operands)" },
|
|
170
204
|
cwd: { type: "path" },
|
|
@@ -177,6 +211,7 @@ export const ARGV_SPEC = {
|
|
|
177
211
|
"max-rounds": { type: "int", min: 1 },
|
|
178
212
|
attempts: { type: "int", min: 1 },
|
|
179
213
|
"spec-folder": { type: "path" },
|
|
214
|
+
dimensions: { type: "str" },
|
|
180
215
|
"gate-answers": { type: "str" },
|
|
181
216
|
lane: { type: "str" },
|
|
182
217
|
tiny: { type: "flag" },
|
|
@@ -220,10 +255,17 @@ export function main() {
|
|
|
220
255
|
if (!AUTO_LEVELS.has(auto_level)) fail(2, `--auto-level must be one of: ${[...AUTO_LEVELS].join(", ")}`);
|
|
221
256
|
const lens = args.lens ?? "standard";
|
|
222
257
|
if (!LENSES.has(lens)) fail(2, `--lens must be one of: ${[...LENSES].join(", ")}`);
|
|
258
|
+
// GATE L0.5 — the eval dimension set, resolved ONCE here and carried by the ledger. Before this
|
|
259
|
+
// flag the line was a constant, so a dimension the PO asked for at L0.5 had nowhere to be
|
|
260
|
+
// recorded and the run graded spec-conformance whatever the answer had been.
|
|
261
|
+
let eval_dimensions;
|
|
262
|
+
try { eval_dimensions = parseDimensions(args.dimensions ?? null); }
|
|
263
|
+
catch (e) { fail(2, e.message); }
|
|
223
264
|
|
|
224
265
|
const config = {
|
|
225
266
|
auto_level,
|
|
226
267
|
lens,
|
|
268
|
+
eval_dimensions,
|
|
227
269
|
max_rounds: args.maxRounds ?? 3,
|
|
228
270
|
attempt_budget: args.attempts ?? 5,
|
|
229
271
|
spec_folder: args.specFolder ?? `${globShared(slug, "spec")}/`,
|
|
@@ -242,7 +284,7 @@ export function main() {
|
|
|
242
284
|
})(),
|
|
243
285
|
// The third breaker (see scripts/budget-check.mjs). Null = off, which is the default and
|
|
244
286
|
// keeps every existing run behaving exactly as before. Set it in any lane with a hard clock
|
|
245
|
-
// — CI,
|
|
287
|
+
// — CI, an overnight run — so the harness trips its own breaker and ships what
|
|
246
288
|
// is green, instead of being killed from outside and shipping nothing.
|
|
247
289
|
wall_clock_budget_s: args.wallClockBudget ?? null,
|
|
248
290
|
};
|
|
@@ -258,9 +300,9 @@ export function main() {
|
|
|
258
300
|
// step, the runtime named a mechanism that does not exist, on a script whose failure mode was
|
|
259
301
|
// already invisible (see lib/is-main.mjs — under a symlinked install this whole body did not run).
|
|
260
302
|
//
|
|
261
|
-
//
|
|
262
|
-
//
|
|
263
|
-
//
|
|
303
|
+
// Observed consequence on a handoff: a fresh session in a workspace with an open run burns most
|
|
304
|
+
// of its budget before its first write, largely on forensics against this step, and closes none
|
|
305
|
+
// of the gap.
|
|
264
306
|
//
|
|
265
307
|
// So the refusal now DOES the resume work instead of describing it. It emits the derived snapshot
|
|
266
308
|
// — the same file-only derivation `hooks/session-rehydrate.mjs` injects — so the orchestrator gets
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
// rewrite call sites. Unknown flags are rejected rather than silently swallowed as positionals:
|
|
38
38
|
// a typo'd `--rounds 2` that lands in `_` is the same defect wearing a different hat.
|
|
39
39
|
//
|
|
40
|
-
//
|
|
40
|
+
// The structural suite executes every entry point with each declared int flag
|
|
41
41
|
// both empty and non-numeric, and asserts exit 2 with a parseable reason and no artifact on disk.
|
|
42
42
|
|
|
43
43
|
/** Thrown by {@link parseArgs} when argv does not satisfy the spec. `detail` is the wire shape. */
|
|
@@ -38,7 +38,7 @@ import { join } from "node:path";
|
|
|
38
38
|
// Per-type layout: which array-of-objects field lives under which heading.
|
|
39
39
|
//
|
|
40
40
|
// `signatures` names the columns that identify a table as THAT field's, and it exists because of
|
|
41
|
-
//
|
|
41
|
+
// The heading match is exact, so a table written under `# Wiring map — <slug>` instead of
|
|
42
42
|
// `## Wiring` parsed as ABSENT, and every reader downstream treats absent as "none declared". The
|
|
43
43
|
// observed consequence was `trace-lint` reporting `🟢 green · 0/0 engines reach src/cli/main.js`
|
|
44
44
|
// for a committed map holding six correct rows — the gate whose whole purpose is that no engine
|
|
@@ -91,7 +91,7 @@ export const UNREADABLE = "$unreadable_tables";
|
|
|
91
91
|
*/
|
|
92
92
|
export function coerce(raw) {
|
|
93
93
|
let trimmed = String(raw ?? "").trim();
|
|
94
|
-
//
|
|
94
|
+
// A MARKDOWN CODE SPAN IS FORMATTING, NOT PART OF THE VALUE.
|
|
95
95
|
//
|
|
96
96
|
// These contracts are MARKDOWN on disk, and a code span is the idiomatic way to write a path or
|
|
97
97
|
// an identifier in one; this repo's own prose backticks every path it names. Read literally,
|
|
@@ -99,7 +99,7 @@ export function coerce(raw) {
|
|
|
99
99
|
// anywhere — so `trace-lint` reported "engine file not on disk" and then "reachability is not
|
|
100
100
|
// demonstrated" for a wiring map whose engines all resolve AND all reach the entry point. The
|
|
101
101
|
// gate that exists so no engine ships orphaned failing CLOSED, on a correct map, is the same
|
|
102
|
-
// silent-format family as
|
|
102
|
+
// silent-format family as the parser defects above, arriving from the other direction.
|
|
103
103
|
//
|
|
104
104
|
// Stripped ONLY when the whole value is a single span: a cell like
|
|
105
105
|
// "Registered as the `add` entry in `TABLE`" is prose that happens to contain spans, and its
|
|
@@ -125,7 +125,7 @@ export function coerce(raw) {
|
|
|
125
125
|
*/
|
|
126
126
|
export function uncoerce(v) {
|
|
127
127
|
if (v === null || v === undefined) return "~";
|
|
128
|
-
//
|
|
128
|
+
// The other half of the comma defect. A member containing the delimiter must go back out QUOTED, or the round
|
|
129
129
|
// trip that wrote it re-reads as several members — the same shredding, arriving from the writer's
|
|
130
130
|
// side instead of the reader's.
|
|
131
131
|
if (Array.isArray(v)) return `[${v.map((x) => (/[,"]/.test(String(x)) ? JSON.stringify(String(x)) : String(x))).join(", ")}]`;
|
|
@@ -135,7 +135,7 @@ export function uncoerce(v) {
|
|
|
135
135
|
/**
|
|
136
136
|
* Split a `[a, b]` list body on commas that are NOT inside quotes.
|
|
137
137
|
*
|
|
138
|
-
*
|
|
138
|
+
* The old implementation was `body.split(",")`, and it shredded any member carrying a
|
|
139
139
|
* comma — even a correctly quoted one. Measured: a `scope-architect` run probed the running CLI,
|
|
140
140
|
* confirmed `tag` was unimplemented, and wrote the honest entry its own SKILL.md asks for —
|
|
141
141
|
* ["TBD — `tag` is not in dispatch.js's TABLE (exits 1, confirmed against the running CLI). A
|
|
@@ -191,12 +191,12 @@ export function splitFrontmatter(md) {
|
|
|
191
191
|
const key = line.slice(0, c).trim();
|
|
192
192
|
const inline = line.slice(c + 1).trim();
|
|
193
193
|
|
|
194
|
-
//
|
|
194
|
+
// An indented run beneath a key is a YAML BLOCK SEQUENCE, and it used to be skipped
|
|
195
195
|
// entirely — so `e2e_verification_fixtures:` followed by two `- "node …"` lines parsed to
|
|
196
196
|
// null, the members vanished, and no reader could tell "declared nothing" from "declared
|
|
197
197
|
// something I discarded". Measured: a scope-architect run wrote three scopes of researched
|
|
198
198
|
// fixtures in this form and every one evaporated. It is ACCEPTED now, because it is the form a
|
|
199
|
-
// model reaches for by default and the one that carries prose members without
|
|
199
|
+
// model reaches for by default and the one that carries prose members without the quoting
|
|
200
200
|
// problem — and anything indented that is NOT a block sequence is reported rather than dropped.
|
|
201
201
|
const block = [];
|
|
202
202
|
let stray = 0;
|
|
@@ -322,13 +322,13 @@ export function parseContract(md, spec = SCOPE_CONTRACT) {
|
|
|
322
322
|
const { meta, body } = splitFrontmatter(md);
|
|
323
323
|
const tables = parseTables(body);
|
|
324
324
|
const out = { ...meta };
|
|
325
|
-
// Frontmatter-level diagnostics
|
|
325
|
+
// Frontmatter-level diagnostics and table-level ones share one channel, so a
|
|
326
326
|
// reader asks `unreadableReason()` once and cannot check for one while missing the other.
|
|
327
327
|
const unreadable = [...(meta[UNREADABLE] || [])];
|
|
328
328
|
delete out[UNREADABLE];
|
|
329
329
|
for (const [field, heading] of Object.entries(spec.tables || {})) {
|
|
330
330
|
if (tables[heading]) { out[field] = tables[heading]; continue; }
|
|
331
|
-
//
|
|
331
|
+
// The field is absent — but is it absent because nobody declared it, or because the
|
|
332
332
|
// author declared it somewhere this parser does not look? Those are opposite facts and the
|
|
333
333
|
// old code returned the same thing for both. A table carrying this field's signature columns,
|
|
334
334
|
// under a heading the spec does not claim, is the second case.
|
|
@@ -351,7 +351,7 @@ export function parseContract(md, spec = SCOPE_CONTRACT) {
|
|
|
351
351
|
* The one-line reason a contract could not be read, or null when it read cleanly.
|
|
352
352
|
*
|
|
353
353
|
* Exported so every consumer asks the same question the same way. A reader that skips this is
|
|
354
|
-
* back to treating "I could not see your table" as "you declared no table"
|
|
354
|
+
* back to treating "I could not see your table" as "you declared no table" — the whole defect class.
|
|
355
355
|
* @param {Object|null} contract - A parsed contract (or a `readContract()` result's `.contract`).
|
|
356
356
|
* @returns {string|null} A human-readable failure, or null if the contract parsed cleanly.
|
|
357
357
|
*/
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// isMain — "was this module executed directly, or imported?"
|
|
2
2
|
//
|
|
3
|
-
// WHY THIS FILE EXISTS (
|
|
3
|
+
// WHY THIS FILE EXISTS (observed, not theorized).
|
|
4
4
|
//
|
|
5
5
|
// Eighteen of this plugin's scripts and hooks decided whether to do anything at all with:
|
|
6
6
|
//
|
|
@@ -12,22 +12,21 @@
|
|
|
12
12
|
//
|
|
13
13
|
// 1. A SYMLINKED DIRECTORY ANYWHERE IN THE PATH. Node resolves `import.meta.url` through
|
|
14
14
|
// symlinks; `process.argv[1]` is the string as typed. On macOS `/var` is a symlink to
|
|
15
|
-
// `/private/var`, so EVERY path under the system temp directory mismatches — which is
|
|
16
|
-
//
|
|
15
|
+
// `/private/var`, so EVERY path under the system temp directory mismatches — which is where
|
|
16
|
+
// an npm-packed install lands (`/var/folders/…/package`). nvm, pnpm's content store,
|
|
17
17
|
// Homebrew and any symlinked checkout do the same thing on every platform.
|
|
18
18
|
//
|
|
19
19
|
// 2. A SPACE OR OTHER URL-RESERVED CHARACTER IN THE PATH. `import.meta.url` is percent-encoded
|
|
20
20
|
// (`My%20Plugins`); the template literal is not (`My Plugins`). So a plugin installed under
|
|
21
21
|
// `~/Library/Application Support/…` or any directory with a space in its name mismatches too.
|
|
22
22
|
//
|
|
23
|
-
// WHAT THAT
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
//
|
|
30
|
-
// the gap while the artifact it needed sat on disk the entire time.
|
|
23
|
+
// WHAT THAT COSTS. `init-run.mjs` is GATE L0.1 — the orchestrator's mandatory first tool call, the
|
|
24
|
+
// script that writes the run receipt everything else is derived from. Under a `/var` path it
|
|
25
|
+
// exited 0 with empty stdout and wrote no receipt. The orchestrator then cannot distinguish "the
|
|
26
|
+
// run opened" from "nothing happened", and burns its budget on forensics against its own
|
|
27
|
+
// bootstrap — retrying the script several ways, hitting permission refusals trying to capture an
|
|
28
|
+
// exit code, and finally searching the filesystem for its own skill — while the artifact it needs
|
|
29
|
+
// sits on disk the entire time.
|
|
31
30
|
//
|
|
32
31
|
// The same guard sits in seven hooks, including `gate-zerowork`, `safety-spine` and
|
|
33
32
|
// `sandbox-guard`. This project's stated organising idea is that "every invariant that matters
|
|
@@ -36,7 +35,7 @@
|
|
|
36
35
|
// an enforcement layer can have, because it is indistinguishable from working.
|
|
37
36
|
//
|
|
38
37
|
// THE FIX. Compare resolved URL to resolved URL, using `pathToFileURL` so encoding matches and
|
|
39
|
-
// `realpathSync` so symlinks match.
|
|
38
|
+
// `realpathSync` so symlinks match. The structural suite asserts that no file
|
|
40
39
|
// reintroduces the fragile form, and actually executes each entry point through a symlinked
|
|
41
40
|
// directory AND through a directory with a space in its name to prove the guard holds — because
|
|
42
41
|
// this defect was invisible to every test that invoked scripts by their real path.
|
|
@@ -2,17 +2,18 @@
|
|
|
2
2
|
//
|
|
3
3
|
// WHY THIS FILE EXISTS (measured by grepping the shipped tree, not theorized).
|
|
4
4
|
//
|
|
5
|
-
// The two storage roots were hard-coded
|
|
5
|
+
// The two storage roots were hard-coded across the tree in TWO syntaxes that no single search
|
|
6
|
+
// finds:
|
|
6
7
|
//
|
|
7
|
-
// "docs/shapeup-sdlc/<slug>/scopes/..."
|
|
8
|
-
// join(cwd, "docs", "shapeup-sdlc", slug, ...)
|
|
8
|
+
// "docs/shapeup-sdlc/<slug>/scopes/..." spelled string literals
|
|
9
|
+
// join(cwd, "docs", "shapeup-sdlc", slug, ...) segment-built sites
|
|
9
10
|
//
|
|
10
11
|
// A find/replace over the first set leaves the second silently pointing at the old root. That is
|
|
11
12
|
// the failure mode this project keeps rediscovering: a change that appears complete, produces no
|
|
12
13
|
// error, and is wrong — `lib/is-main.mjs` (a guard duplicated 18 times, inert under a symlink) and
|
|
13
14
|
// `lib/argv.mjs` (`rNaN-a1.json` written with exit 0) are the same defect at different layers. The
|
|
14
15
|
// remedy each time is to give the duplicated thing one home and add a test that no one may bypass
|
|
15
|
-
// it
|
|
16
|
+
// it — asserted by the structural suite.
|
|
16
17
|
//
|
|
17
18
|
// It also removes a live ambiguity. `gate-answers.mjs` resolved three candidate paths and
|
|
18
19
|
// `sandbox-guard.mjs` built a fourth independently; the same filename meant "my personal lane" at
|
|
@@ -21,7 +22,7 @@
|
|
|
21
22
|
// TIER DISCIPLINE, restated here because this is where it becomes mechanical:
|
|
22
23
|
// SHARED (committed) — prose a teammate reads: shaping, spec, contracts, requirements, report.
|
|
23
24
|
// LOCAL (gitignored) — run state, envelopes, verification artifacts, machine policy.
|
|
24
|
-
// See
|
|
25
|
+
// See ADR-0001 (consumer file organization).
|
|
25
26
|
//
|
|
26
27
|
// Zero dependencies. Pure — every function takes `cwd` and returns a path; nothing here touches
|
|
27
28
|
// the filesystem, so importing this module can never have a side effect.
|
|
@@ -122,10 +123,11 @@ export const report = (cwd, slug) => join(sharedRoot(cwd, slug), "REPORT.md");
|
|
|
122
123
|
export const knowledgeBaseDir = (cwd) => join(sharedDir(cwd), "knowledge-base");
|
|
123
124
|
/** One worker's coaching file. */
|
|
124
125
|
export const knowledgeBase = (cwd, skill) => join(knowledgeBaseDir(cwd), `${skill}.md`);
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
126
|
+
// `.harness-version` and `.harness-migrations` were resolved here, committed by necessity as
|
|
127
|
+
// ADR-0001's third exception. Their sole writer was the upgrade path's data-migration runner, which
|
|
128
|
+
// has been removed; no code in the plugin has written either file since. The helpers went with
|
|
129
|
+
// them rather than remaining as resolvable paths to files nothing produces — a path helper is a
|
|
130
|
+
// claim that the file is part of the layout, and this one would have been false.
|
|
129
131
|
|
|
130
132
|
// ---------------------------------------------------------------------------
|
|
131
133
|
// LOCAL — run state
|
|
@@ -167,8 +169,6 @@ export const spikesDir = (cwd, slug) => join(localRoot(cwd, slug), "spikes");
|
|
|
167
169
|
export const traceDir = (cwd, slug) => join(localRoot(cwd, slug), "trace");
|
|
168
170
|
/** The discovered-task ledger every discovery flow appends to. */
|
|
169
171
|
export const discoveryLedger = (cwd, slug) => join(localRoot(cwd, slug), "discovery", "ledger.md");
|
|
170
|
-
/** Queued worker escalations awaiting adjudication. */
|
|
171
|
-
export const escalatesDir = (cwd, slug) => join(localRoot(cwd, slug), "escalates");
|
|
172
172
|
/**
|
|
173
173
|
* Adjudicated decisions, read back by `compile-order` as binding precedent.
|
|
174
174
|
*
|
|
@@ -191,6 +191,8 @@ export const workingDir = (cwd, slug) => join(localRoot(cwd, slug), "working");
|
|
|
191
191
|
|
|
192
192
|
/** The pointer the sandbox guard reads to answer "which scope is checked out?". */
|
|
193
193
|
export const activeScope = (cwd) => join(localDir(cwd), "active-scope");
|
|
194
|
+
/** The pointer the sandbox guard reads to answer "which order is executing?". */
|
|
195
|
+
export const activeOrder = (cwd) => join(localDir(cwd), "active-order");
|
|
194
196
|
/** Hook receipts — one row per evaluation, so `allow` carries evidence. */
|
|
195
197
|
export const decisions = (cwd) => join(localDir(cwd), "decisions.jsonl");
|
|
196
198
|
/** Human-authored safety escape hatch. LOCAL so no PR can widen another machine's envelope. */
|
|
@@ -266,13 +268,13 @@ export const globLocal = (slug, ...parts) => [LOCAL, slug, ...parts].join("/");
|
|
|
266
268
|
* A deliverable glob, POSIX-separated for the same reason as {@link globLocal}.
|
|
267
269
|
* @param {string} slug - Feature slug.
|
|
268
270
|
* @param {...string} parts - Glob segments under the feature's shared root.
|
|
269
|
-
* @returns {string} e.g. `
|
|
271
|
+
* @returns {string} e.g. `shapeup/checkout/scopes/*.md`.
|
|
270
272
|
*/
|
|
271
273
|
export const globShared = (slug, ...parts) => [SHARED, slug, ...parts].join("/");
|
|
272
274
|
|
|
273
275
|
/**
|
|
274
276
|
* The coaching file a coachable worker reads, as a repo-relative path for the WorkOrder payload.
|
|
275
277
|
* @param {string} skill - Worker name (task-executor | ba-pitch-analyzer | qa-edge-hunter).
|
|
276
|
-
* @returns {string} e.g. `
|
|
278
|
+
* @returns {string} e.g. `shapeup/knowledge-base/task-executor.md`.
|
|
277
279
|
*/
|
|
278
280
|
export const relKnowledgeBase = (skill) => [SHARED, "knowledge-base", `${skill}.md`].join("/");
|