shapeup-sdlc 3.18.2 → 4.0.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/settings.local.example.json +2 -3
- package/.claude-plugin/plugin.json +1 -1
- package/AGENTS.md +115 -56
- package/README.md +101 -143
- package/SECURITY.md +16 -25
- package/agents/orient.md +14 -0
- package/agents/qa-edge-hunter.md +13 -0
- package/agents/spec-evaluator.md +14 -0
- package/agents/task-executor.md +14 -0
- package/bin/init.mjs +13 -16
- package/bin/lib/grant.mjs +8 -19
- package/commands/build.md +9 -14
- package/commands/eval.md +3 -3
- package/commands/hammer.md +9 -5
- package/commands/retro.md +12 -11
- package/commands/scopes.md +1 -12
- package/commands/ship.md +45 -88
- package/commands/wire.md +1 -8
- package/hooks/dispatch-receipt.mjs +94 -8
- package/hooks/hooks.json +0 -41
- package/hooks/lib/decision.mjs +6 -9
- package/hooks/lib/tier.mjs +50 -0
- package/hooks/sandbox-guard.mjs +140 -64
- package/kernel/gate.mjs +203 -22
- package/kernel/harness.mjs +20 -67
- package/kernel/init/run.mjs +175 -523
- package/kernel/lib/contract.mjs +9 -93
- package/kernel/lib/floor.mjs +64 -0
- package/kernel/lib/paths.mjs +39 -112
- package/kernel/order.mjs +157 -0
- package/kernel/probe/digest.mjs +2 -38
- package/kernel/schemas/domain.schema.json +146 -1125
- package/kernel/schemas/work-order.schema.json +8 -4
- package/kernel/schemas/work-result.schema.json +1 -1
- package/kernel/verify/envelope.mjs +2 -2
- package/kernel/verify/skills.mjs +27 -8
- package/kernel/verify/t0.mjs +2 -2
- package/package.json +4 -2
- package/skills/building/SKILL.md +144 -0
- package/skills/building/resources/gates.md +76 -0
- package/skills/building/resources/guidance.md +23 -0
- package/skills/{translator/references/preservation-rules.md → building/resources/intake-preservation.md} +1 -1
- package/skills/building/resources/intake.md +157 -0
- package/skills/building/resources/orders.md +69 -0
- package/skills/{ba-pitch-analyzer/references → building/resources/plan-references}/doc-schemas.md +1 -1
- package/skills/{ba-pitch-analyzer/references → building/resources/plan-references}/task-generation.md +13 -13
- package/skills/{ba-pitch-analyzer/references → building/resources/plan-references}/test-surface.md +6 -7
- package/skills/building/resources/plan-scopes.md +95 -0
- package/skills/building/resources/plan-spec.md +102 -0
- package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/_index.tmpl.md +1 -1
- package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/assess-report.tmpl.md +10 -11
- package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/run-state.tmpl.md +4 -4
- package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/scope-summary.tmpl.md +2 -2
- package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/synthesis.tmpl.md +4 -4
- package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/usecase.tmpl.md +2 -2
- package/skills/building/resources/plan-wire.md +82 -0
- package/skills/building/resources/plan.md +98 -0
- package/skills/building/resources/retro-research.md +60 -0
- package/skills/building/resources/retro-scan.md +49 -0
- package/skills/building/resources/retro.md +225 -0
- package/skills/building/resources/rounds.md +78 -0
- package/skills/building/resources/stop.md +127 -0
- package/skills/orient/SKILL.md +37 -37
- package/skills/qa-edge-hunter/SKILL.md +29 -14
- package/skills/shapeup/SKILL.md +5 -5
- package/skills/shapeup/resources/breadboarding.md +1 -1
- package/skills/shapeup/resources/context-compaction.md +5 -5
- package/skills/shapeup/resources/shaping.md +1 -1
- package/skills/spec-evaluator/SKILL.md +17 -21
- package/skills/spec-evaluator/references/dimension-contract.md +1 -1
- package/skills/spec-evaluator/references/dimensions/completeness.md +3 -3
- package/skills/spec-evaluator/references/dimensions/spec-conformance.md +1 -1
- package/skills/spec-evaluator/references/dimensions/test-surface-conformance.md +5 -5
- package/skills/task-executor/SKILL.md +11 -15
- package/commands/hill.md +0 -10
- package/hooks/gate-intake.mjs +0 -109
- package/hooks/gate-zerowork.mjs +0 -349
- package/hooks/tier-guard.mjs +0 -162
- package/kernel/compile.mjs +0 -1419
- package/kernel/init/fit.mjs +0 -199
- package/kernel/init/run-args.mjs +0 -206
- package/kernel/lib/breadboard.mjs +0 -165
- package/kernel/probe/attempts.mjs +0 -161
- package/kernel/probe/concurrency.mjs +0 -535
- package/kernel/probe/eval.mjs +0 -405
- package/kernel/probe/hunt.mjs +0 -104
- package/kernel/probe/leg.mjs +0 -229
- package/kernel/probe/owner.mjs +0 -142
- package/kernel/probe/requirements.mjs +0 -328
- package/kernel/probe/resume.mjs +0 -1110
- package/kernel/probe/rounds.mjs +0 -115
- package/kernel/probe/staged.mjs +0 -83
- package/kernel/probe/stats.mjs +0 -477
- package/kernel/probe/t0.mjs +0 -115
- package/kernel/reduce/board.mjs +0 -350
- package/kernel/reduce/graph.mjs +0 -482
- package/kernel/reduce/hill.mjs +0 -322
- package/kernel/reduce/ingest.mjs +0 -767
- package/kernel/reduce/leftovers.mjs +0 -159
- package/kernel/reduce/ship.mjs +0 -529
- package/kernel/reduce/snapshot.mjs +0 -291
- package/kernel/reduce/verdict.mjs +0 -172
- package/kernel/report/export.mjs +0 -384
- package/kernel/report/facts.mjs +0 -234
- package/kernel/verify/budget.mjs +0 -153
- package/kernel/verify/build.mjs +0 -428
- package/kernel/verify/dispatch.mjs +0 -114
- package/kernel/verify/spec.mjs +0 -975
- package/kernel/verify/trace.mjs +0 -604
- package/skills/ba-pitch-analyzer/SKILL.md +0 -204
- package/skills/coach/SKILL.md +0 -397
- package/skills/hill-chart/SKILL.md +0 -154
- package/skills/hill-chart/assets/dashboard.template.html +0 -1432
- package/skills/scope-architect/SKILL.md +0 -174
- package/skills/scope-hammer/SKILL.md +0 -231
- package/skills/solution-architect/SKILL.md +0 -158
- package/skills/tech-lead/SKILL.md +0 -151
- package/skills/tech-lead/references/gates.md +0 -605
- package/skills/tech-lead/references/protocol.md +0 -889
- package/skills/tech-lead/references/tiny-lane.md +0 -53
- package/skills/tech-lead/workflows/shapeup-run.js +0 -2187
- package/skills/translator/SKILL.md +0 -258
- /package/skills/{ba-pitch-analyzer/references → building/resources/plan-references}/contract-patterns.md +0 -0
- /package/skills/{ba-pitch-analyzer/references → building/resources/plan-references}/ddd-patterns.md +0 -0
- /package/skills/{ba-pitch-analyzer/references → building/resources/plan-references}/integration-analysis.md +0 -0
- /package/skills/{ba-pitch-analyzer/references → building/resources/plan-references}/ux-behavior-patterns.md +0 -0
- /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/api-feasibility.tmpl.md +0 -0
- /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/contracts/be-service.contract.tmpl.md +0 -0
- /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/contracts/offline-storage.contract.tmpl.md +0 -0
- /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/contracts/third-party-api.contract.tmpl.md +0 -0
- /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/cross-context/context-map.tmpl.md +0 -0
- /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/cross-context/event-choreography.tmpl.md +0 -0
- /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/cross-context/migration-plan.tmpl.md +0 -0
- /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/cross-context/team-handoff.tmpl.md +0 -0
- /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/domain-model.tmpl.md +0 -0
- /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/feedback.tmpl.md +0 -0
- /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/integration.tmpl.md +0 -0
- /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/task-board.tmpl.md +0 -0
- /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/task-spike.tmpl.md +0 -0
- /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/task.tmpl.md +0 -0
- /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/ux-behavior.tmpl.md +0 -0
package/kernel/probe/leg.mjs
DELETED
|
@@ -1,229 +0,0 @@
|
|
|
1
|
-
// probe leg — "did this scope's work actually reach the board?"
|
|
2
|
-
//
|
|
3
|
-
// CONTRACT. A bounded, read-only query over one scope's orders, results and leg-completion rows for
|
|
4
|
-
// one round. Prints `{closed, scope_id, round, orders: [...], unapplied: [...]}` on stdout; exits 0
|
|
5
|
-
// when every order that produced a result has been ingested, 1 when at least one has not, 2 on a bad
|
|
6
|
-
// argv. Writes nothing.
|
|
7
|
-
//
|
|
8
|
-
// WHY IT EXISTS. `probe t0` answers "is the T0 verdict green", and the BUILD round's confirm stage
|
|
9
|
-
// asked only that. A green T0 says the worker's fixtures ran and passed; it says nothing about
|
|
10
|
-
// whether the WorkResult was applied — and this is a state a real run reaches. A build leg can write
|
|
11
|
-
// its code, a green T0 verdict, a kept trial row and its WorkResult to disk, and never run step 3 of
|
|
12
|
-
// its own script, `reduce ingest`. It reports green; a confirm stage that re-verifies only the T0
|
|
13
|
-
// artifact agrees, and the round walks on. That scope's task is left `pending` with zero acceptance
|
|
14
|
-
// criteria ticked while its code sits finished on disk, beside a sibling scope whose task is `done`.
|
|
15
|
-
//
|
|
16
|
-
// Nothing has to be wrong with the order, the receipt or the result for this to happen — ingesting the
|
|
17
|
-
// same file afterwards succeeds and ticks the criteria. The single writer of shared state simply never
|
|
18
|
-
// ran, and the board GATE L2 reads as "100% ✅" silently disagrees with a scope that is genuinely
|
|
19
|
-
// finished. Under fan-out it gets worse rather than better: N legs can each drop the step
|
|
20
|
-
// independently, and the only symptom is a board that lags reality.
|
|
21
|
-
//
|
|
22
|
-
// WHY A LEG ROW IS THE RIGHT EVIDENCE. The row is appended BY `reduce ingest`. Its presence is
|
|
23
|
-
// therefore proof the writer ran, and it is not something the leg can assert about itself — the same
|
|
24
|
-
// reason the dispatch receipt is written by the hook layer rather than by the sub-agent making the
|
|
25
|
-
// call. "The worker says it ingested" is exactly the class of claim this pipeline does not accept.
|
|
26
|
-
|
|
27
|
-
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
28
|
-
import { join, resolve } from "node:path";
|
|
29
|
-
import { runArgs } from "../lib/argv.mjs";
|
|
30
|
-
import { ordersDir, resultsDir, legLedger, dispatchReceipts, readRunId } from "../lib/paths.mjs";
|
|
31
|
-
|
|
32
|
-
/** `<scope>-r<round>-a<attempt>.json` — the only address a build order is written under. */
|
|
33
|
-
const BUILD_ORDER = /^(.+)-r(\d+)-a(\d+)\.json$/;
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Every leg-completion row on disk, tolerant of a torn last line.
|
|
37
|
-
* @param {string} path - The leg ledger.
|
|
38
|
-
* @returns {object[]} Parsed rows; an unparsable line is skipped rather than fatal.
|
|
39
|
-
*/
|
|
40
|
-
export function readLegs(path) {
|
|
41
|
-
if (!existsSync(path)) return [];
|
|
42
|
-
return readFileSync(path, "utf8").split("\n").filter(Boolean)
|
|
43
|
-
.map((l) => { try { return JSON.parse(l); } catch { return null; } })
|
|
44
|
-
.filter(Boolean);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Which of one scope's orders in one round produced a result, and which of those were applied.
|
|
49
|
-
*
|
|
50
|
-
* An order with no result has not finished and is not a finding — the leg may still be running, or
|
|
51
|
-
* may have died, which the round already treats as a spent attempt. What this reports is the
|
|
52
|
-
* narrower and stranger case: a result on disk that the single writer never applied.
|
|
53
|
-
*
|
|
54
|
-
* @param {string} cwd - Project root.
|
|
55
|
-
* @param {string} slug - Feature slug.
|
|
56
|
-
* @param {string} scopeId - Scope contract id.
|
|
57
|
-
* @param {number} round - Build round.
|
|
58
|
-
* @returns {{closed: boolean, orders: object[], unapplied: object[]}} `closed` is false only when a
|
|
59
|
-
* result exists that was never ingested; a scope with no results at all reports `closed: false`
|
|
60
|
-
* with an empty `orders` list, so a caller can tell "nothing ran" from "nothing was applied".
|
|
61
|
-
*/
|
|
62
|
-
/**
|
|
63
|
-
* Every order the run compiled, with whether its result landed and whether the single writer
|
|
64
|
-
* applied it — planning orders (`analyze.json`) and build orders (`alpha-r1-a1.json`) alike.
|
|
65
|
-
*
|
|
66
|
-
* Why the generic form exists: the per-scope reading below was the only reader of the leg ledger
|
|
67
|
-
* in the run loop, and it was asked in one place, behind the checks that decide a scope is green.
|
|
68
|
-
* Measured on a live run: five dispatches, five results, three leg rows — and the planning phase
|
|
69
|
-
* whose result nobody applied walked on, because its post-condition checked the artifact the
|
|
70
|
-
* worker wrote directly and never asked whether the writer ran. This is the question, asked of any
|
|
71
|
-
* order by name.
|
|
72
|
-
*
|
|
73
|
-
* @param {string} cwd - Project root.
|
|
74
|
-
* @param {string} slug - Feature slug.
|
|
75
|
-
* @returns {{order:string, name:string, order_id:(string|null), has_result:boolean, applied:boolean}[]}
|
|
76
|
-
*/
|
|
77
|
-
export function legsOf(cwd, slug) {
|
|
78
|
-
const oDir = ordersDir(cwd, slug);
|
|
79
|
-
const rDir = resultsDir(cwd, slug);
|
|
80
|
-
const ingested = new Set(readLegs(legLedger(cwd, slug))
|
|
81
|
-
.filter((r) => r.ingested_at)
|
|
82
|
-
.map((r) => String(r.order_id)));
|
|
83
|
-
// Dispatch receipts are the hook layer's record that a Skill was invoked against an order —
|
|
84
|
-
// scoped to this run, since receipts over one slug accumulate across runs and order ids repeat.
|
|
85
|
-
const runId = readRunId(cwd, slug);
|
|
86
|
-
const receipted = new Set(readLegs(dispatchReceipts(cwd, slug))
|
|
87
|
-
.filter((r) => r?.order_id && (!runId || !r.run_id || r.run_id === runId))
|
|
88
|
-
.map((r) => String(r.order_id)));
|
|
89
|
-
const out = [];
|
|
90
|
-
for (const f of (existsSync(oDir) ? readdirSync(oDir) : []).filter((x) => x.endsWith(".json")).sort()) {
|
|
91
|
-
let orderId = null;
|
|
92
|
-
try { orderId = JSON.parse(readFileSync(join(oDir, f), "utf8")).order_id ?? null; } catch { /* unreadable — reported as unapplied */ }
|
|
93
|
-
out.push({
|
|
94
|
-
order: join(oDir, f),
|
|
95
|
-
name: f.replace(/\.json$/, ""),
|
|
96
|
-
order_id: orderId,
|
|
97
|
-
has_receipt: orderId !== null && receipted.has(orderId),
|
|
98
|
-
has_result: existsSync(join(rDir, f)),
|
|
99
|
-
applied: orderId !== null && ingested.has(orderId),
|
|
100
|
-
});
|
|
101
|
-
}
|
|
102
|
-
return out;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* One order by its file stem — `analyze`, `wire`, `alpha-r1-a1` — and whether its leg closed.
|
|
107
|
-
* @param {string} cwd - Project root.
|
|
108
|
-
* @param {string} slug - Feature slug.
|
|
109
|
-
* @param {string} name - The order file's stem.
|
|
110
|
-
* @returns {{closed:boolean, found:boolean, order:(string|null), order_id:(string|null), has_result:boolean, applied:boolean}}
|
|
111
|
-
*/
|
|
112
|
-
export function orderLegState(cwd, slug, name) {
|
|
113
|
-
const o = legsOf(cwd, slug).find((x) => x.name === name);
|
|
114
|
-
if (!o) return { closed: false, found: false, order: null, order_id: null, has_receipt: false, has_result: false, applied: false };
|
|
115
|
-
return { closed: o.applied, found: true, ...o };
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* Orders that were dispatched — a receipt says a Skill ran against them — and never answered:
|
|
120
|
-
* no WorkResult on disk. Measured on a live run: the first dispatch wrote its four artifacts and
|
|
121
|
-
* no envelope, every later phase read the artifacts, and the run closed `shipped` with the order
|
|
122
|
-
* still open by construction — the export's own dispatch row said `answered: false` and nothing
|
|
123
|
-
* upstream of it had noticed. Distinct from {@link openLegs}: that is work nobody applied; this is
|
|
124
|
-
* a leg that never came back.
|
|
125
|
-
* @param {string} cwd - Project root.
|
|
126
|
-
* @param {string} slug - Feature slug.
|
|
127
|
-
* @returns {{order:string, name:string, order_id:(string|null)}[]}
|
|
128
|
-
*/
|
|
129
|
-
export function unansweredOrders(cwd, slug) {
|
|
130
|
-
return legsOf(cwd, slug).filter((o) => o.has_receipt && !o.has_result);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
/**
|
|
134
|
-
* The results on disk that no leg row applied — finished work the board never saw.
|
|
135
|
-
* @param {string} cwd - Project root.
|
|
136
|
-
* @param {string} slug - Feature slug.
|
|
137
|
-
* @returns {{order:string, name:string, order_id:(string|null)}[]}
|
|
138
|
-
*/
|
|
139
|
-
export function openLegs(cwd, slug) {
|
|
140
|
-
return legsOf(cwd, slug).filter((o) => o.has_result && !o.applied);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
export function legState(cwd, slug, scopeId, round) {
|
|
144
|
-
const oDir = ordersDir(cwd, slug);
|
|
145
|
-
const rDir = resultsDir(cwd, slug);
|
|
146
|
-
const legs = readLegs(legLedger(cwd, slug));
|
|
147
|
-
const ingested = new Set(legs
|
|
148
|
-
.filter((r) => Number(r.round) === round && r.scope_id === scopeId && r.ingested_at)
|
|
149
|
-
.map((r) => String(r.order_id)));
|
|
150
|
-
|
|
151
|
-
const orders = [];
|
|
152
|
-
for (const f of (existsSync(oDir) ? readdirSync(oDir) : []).sort()) {
|
|
153
|
-
const m = f.match(BUILD_ORDER);
|
|
154
|
-
if (!m || m[1] !== scopeId || Number(m[2]) !== round) continue;
|
|
155
|
-
// The order id is read off the order rather than rebuilt from the filename: the filename is the
|
|
156
|
-
// path the round knows, and the id is the key every record joins on, and they are allowed to be
|
|
157
|
-
// written by different steps.
|
|
158
|
-
let orderId = null;
|
|
159
|
-
try { orderId = JSON.parse(readFileSync(join(oDir, f), "utf8")).order_id ?? null; } catch { /* unreadable — reported below */ }
|
|
160
|
-
orders.push({
|
|
161
|
-
order: join(oDir, f),
|
|
162
|
-
order_id: orderId,
|
|
163
|
-
attempt: Number(m[3]),
|
|
164
|
-
has_result: existsSync(join(rDir, f)),
|
|
165
|
-
applied: orderId !== null && ingested.has(orderId),
|
|
166
|
-
});
|
|
167
|
-
}
|
|
168
|
-
const unapplied = orders.filter((o) => o.has_result && !o.applied);
|
|
169
|
-
return { closed: orders.some((o) => o.applied) && unapplied.length === 0, orders, unapplied };
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
export const ARGV_SPEC = {
|
|
173
|
-
usage: "harness.mjs probe leg --slug <slug> (--scope <scope-id> --round N | --order <stem> | --open) [--cwd <dir>]",
|
|
174
|
-
_: { arity: 0, max: 0, name: "(no positional operands)" },
|
|
175
|
-
slug: { type: "str", required: true },
|
|
176
|
-
scope: { type: "str" },
|
|
177
|
-
round: { type: "int", min: 1 },
|
|
178
|
-
order: { type: "str" },
|
|
179
|
-
open: { type: "flag" },
|
|
180
|
-
cwd: { type: "path" },
|
|
181
|
-
};
|
|
182
|
-
|
|
183
|
-
/**
|
|
184
|
-
* Report whether one scope's results for one round reached the board.
|
|
185
|
-
*
|
|
186
|
-
* @param {string[]} rawArgv - The subcommand's own arguments (harness.mjs strips the verb words).
|
|
187
|
-
* @returns {void} Exits 0 when closed, 1 when a result was never applied — the shape a caller can
|
|
188
|
-
* branch on without parsing prose.
|
|
189
|
-
*/
|
|
190
|
-
export function cli(rawArgv) {
|
|
191
|
-
const args = runArgs(ARGV_SPEC, rawArgv);
|
|
192
|
-
const cwd = resolve(args.cwd || process.cwd());
|
|
193
|
-
// `--open`: every result nothing applied, across the whole run, any phase. Exit 0 when none.
|
|
194
|
-
if (args.open) {
|
|
195
|
-
const open = openLegs(cwd, args.slug);
|
|
196
|
-
const unanswered = unansweredOrders(cwd, args.slug);
|
|
197
|
-
console.log(JSON.stringify({
|
|
198
|
-
closed: open.length === 0 && unanswered.length === 0,
|
|
199
|
-
open_total: open.length, open: open.map((o) => o.order), open_ids: open.map((o) => o.order_id),
|
|
200
|
-
unanswered_total: unanswered.length, unanswered: unanswered.map((o) => o.order), unanswered_ids: unanswered.map((o) => o.order_id),
|
|
201
|
-
}));
|
|
202
|
-
process.exit(open.length === 0 && unanswered.length === 0 ? 0 : 1);
|
|
203
|
-
}
|
|
204
|
-
// `--order <stem>`: one order by file stem (`analyze`, `alpha-r1-a1`). Exit 0 when its leg closed.
|
|
205
|
-
if (args.order) {
|
|
206
|
-
const s = orderLegState(cwd, args.slug, args.order);
|
|
207
|
-
console.log(JSON.stringify(s));
|
|
208
|
-
process.exit(s.closed ? 0 : 1);
|
|
209
|
-
}
|
|
210
|
-
if (!args.scope || !args.round) {
|
|
211
|
-
console.error("probe leg: pass --scope <id> --round N, or --order <stem>, or --open");
|
|
212
|
-
process.exit(2);
|
|
213
|
-
}
|
|
214
|
-
const s = legState(cwd, args.slug, args.scope, args.round);
|
|
215
|
-
console.log(JSON.stringify({
|
|
216
|
-
closed: s.closed,
|
|
217
|
-
scope_id: args.scope,
|
|
218
|
-
round: args.round,
|
|
219
|
-
// The counts travel with the verdict: `closed: false` over zero orders means the leg has not run,
|
|
220
|
-
// and over three orders means its work is on disk and unapplied. Those need different answers
|
|
221
|
-
// from the caller, so they must not read the same.
|
|
222
|
-
orders_total: s.orders.length,
|
|
223
|
-
results_total: s.orders.filter((o) => o.has_result).length,
|
|
224
|
-
applied_total: s.orders.filter((o) => o.applied).length,
|
|
225
|
-
orders: s.orders,
|
|
226
|
-
unapplied: s.unapplied.map((o) => o.order),
|
|
227
|
-
}));
|
|
228
|
-
process.exit(s.closed ? 0 : 1);
|
|
229
|
-
}
|
package/kernel/probe/owner.mjs
DELETED
|
@@ -1,142 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// probe owner — which scope owns a path, derived from the contracts and nothing else.
|
|
3
|
-
//
|
|
4
|
-
// WHY THIS IS A QUERY AND NOT A SENTENCE. GATE H's census stated ownership facts without reading
|
|
5
|
-
// the contracts: a ship report said no scope owned the app's page files, while the composition
|
|
6
|
-
// root's committed substrate listed `pages/**` in plain sight. The real gap was elsewhere (a route
|
|
7
|
-
// table naming files nobody had written), and the report pointed the PO at a scope cut that was
|
|
8
|
-
// not the problem. A census that narrates ownership from memory is a census that can be wrong in
|
|
9
|
-
// exactly the way that looks most authoritative.
|
|
10
|
-
//
|
|
11
|
-
// Ownership already has ONE mechanical definition in this plugin — the substrate the sandbox hook
|
|
12
|
-
// enforces, and the election `harness compile` runs to address a cited bug to the scope that may
|
|
13
|
-
// write the file (`electOwner`). This probe exposes that same answer on the command line, so a
|
|
14
|
-
// worker that needs to say "no scope owns X" can cite the query instead of asserting it.
|
|
15
|
-
//
|
|
16
|
-
// With no `--path`, the input set is what a census would ask about anyway: every engine and
|
|
17
|
-
// entry-point call site the wiring map names, plus the profile's entry point. A row with no
|
|
18
|
-
// writers is an UNOWNED SEAM — the one kind of gap the disjointness lint cannot see, since DISJOINT
|
|
19
|
-
// fails two owners and nothing fails zero.
|
|
20
|
-
//
|
|
21
|
-
// Usage:
|
|
22
|
-
// node "${CLAUDE_PLUGIN_ROOT}/kernel/harness.mjs" probe owner --slug <slug> [--path <p>]... [--format json|table] [--cwd <dir>]
|
|
23
|
-
//
|
|
24
|
-
// Exit code: 0 = answered, 2 = bad argv or no readable scope contracts.
|
|
25
|
-
|
|
26
|
-
import { existsSync } from "node:fs";
|
|
27
|
-
import { resolve, join } from "node:path";
|
|
28
|
-
import { runArgs, isMain } from "../lib/argv.mjs";
|
|
29
|
-
import { wiringMap, projectProfile } from "../lib/paths.mjs";
|
|
30
|
-
import { readContract, WIRING_MAP, PROJECT_PROFILE } from "../lib/contract.mjs";
|
|
31
|
-
import { scopeSubstrates, electOwner, bugLocations } from "../compile.mjs";
|
|
32
|
-
import { matchesAny } from "../../hooks/sandbox-guard.mjs";
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Ownership of one repo-relative path under a set of scope substrates.
|
|
36
|
-
*
|
|
37
|
-
* @param {string} path - Repo-relative POSIX path.
|
|
38
|
-
* @param {Array<{scope_id:string, allowed:string[], shared:string[]}>} scopes - From
|
|
39
|
-
* {@link scopeSubstrates}.
|
|
40
|
-
* @param {string} [cwd] - Project root; when given, `exists` says whether the path is on disk.
|
|
41
|
-
* @returns {{path:string, owner:(string|null), writers:string[], shared_with:string[], exists:(boolean|null)}}
|
|
42
|
-
* `owner` is the elected fixer (exclusive first, lowest id among equals); `writers` every scope
|
|
43
|
-
* whose substrate admits the path; `shared_with` the writers that declare it shared. Empty
|
|
44
|
-
* `writers` means no scope may write the file — an unowned path. `exists` is the other half a
|
|
45
|
-
* census needs: a seam that is owned but not on disk is a route to a file nobody wrote.
|
|
46
|
-
*/
|
|
47
|
-
export function ownership(path, scopes, cwd = null) {
|
|
48
|
-
const rel = String(path).replace(/^\.\//, "");
|
|
49
|
-
const shared = (scopes || []).filter((s) => matchesAny(rel, s.shared || [])).map((s) => s.scope_id).sort();
|
|
50
|
-
// `writers` is admits-the-path, the same union the sandbox fence composes (allowed ++ shared) —
|
|
51
|
-
// a path declared only in a contract's `shared` list is still a scope this path may write, and
|
|
52
|
-
// must read as owned rather than UNOWNED. `shared_with` (below) stays the narrower subset.
|
|
53
|
-
const writers = (scopes || []).filter((s) => matchesAny(rel, s.allowed) || matchesAny(rel, s.shared || [])).map((s) => s.scope_id).sort();
|
|
54
|
-
return { path: rel, owner: electOwner(rel, scopes), writers, shared_with: shared, exists: cwd ? existsSync(join(cwd, rel)) : null };
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* The paths a census asks about when none are given: the wiring map's engines and entry call
|
|
59
|
-
* sites, and the profile's entry point. Only tokens that look like files are kept — an entry call
|
|
60
|
-
* site is prose around a path (`src/server.ts — POST /checkout route`), and the path is what the
|
|
61
|
-
* substrate can answer for.
|
|
62
|
-
*
|
|
63
|
-
* @param {string} cwd - Project root.
|
|
64
|
-
* @param {string} slug - Feature slug.
|
|
65
|
-
* @returns {Array<{path:string, cited_by:string}>} Distinct paths with where each was named.
|
|
66
|
-
*/
|
|
67
|
-
export function seamPaths(cwd, slug) {
|
|
68
|
-
const out = [];
|
|
69
|
-
const seen = new Set();
|
|
70
|
-
const add = (p, by) => { if (p && !seen.has(p)) { seen.add(p); out.push({ path: p, cited_by: by }); } };
|
|
71
|
-
const wm = wiringMap(cwd, slug);
|
|
72
|
-
if (existsSync(wm)) {
|
|
73
|
-
const entries = readContract(wm, WIRING_MAP)?.contract?.entries || [];
|
|
74
|
-
for (const e of entries) {
|
|
75
|
-
for (const p of bugLocations({ location: e.engine })) add(p, `${e.use_case || "?"} engine`);
|
|
76
|
-
for (const p of bugLocations({ location: e.entry_call_site })) add(p, `${e.use_case || "?"} entry_call_site`);
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
const pp = projectProfile(cwd, slug);
|
|
80
|
-
if (existsSync(pp)) {
|
|
81
|
-
const ep = readContract(pp, PROJECT_PROFILE)?.contract?.entry_point;
|
|
82
|
-
for (const p of bugLocations({ location: ep })) add(p, "project-profile entry_point");
|
|
83
|
-
}
|
|
84
|
-
return out;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Render the report as a fixed-width table for a human reading a gate block.
|
|
89
|
-
* @param {object} r - The report {@link cli} prints.
|
|
90
|
-
* @returns {string} One header line and one row per path.
|
|
91
|
-
*/
|
|
92
|
-
export function renderTable(r) {
|
|
93
|
-
const rows = r.paths.map((p) => [p.path, p.owner || "—", p.writers.join(",") || "UNOWNED", p.exists === null ? "?" : p.exists ? "yes" : "MISSING", p.cited_by || ""]);
|
|
94
|
-
const head = ["path", "owner", "writers", "on disk", "cited by"];
|
|
95
|
-
const w = head.map((h, i) => Math.max(h.length, ...rows.map((row) => String(row[i]).length)));
|
|
96
|
-
const line = (row) => row.map((c, i) => String(c).padEnd(w[i])).join(" ").trimEnd();
|
|
97
|
-
return [line(head), line(w.map((n) => "-".repeat(n))), ...rows.map(line),
|
|
98
|
-
"", `${r.scopes} scope(s) read · ${r.unowned.length} unowned path(s)${r.unowned.length ? `: ${r.unowned.join(", ")}` : ""}` +
|
|
99
|
-
` · ${r.missing.length} not on disk${r.missing.length ? `: ${r.missing.join(", ")}` : ""}`].join("\n");
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
export const ARGV_SPEC = {
|
|
103
|
-
usage: "harness.mjs probe owner --slug <slug> [--path <p>]... [--format json|table] [--cwd <dir>]",
|
|
104
|
-
_: { arity: 0, max: 0, name: "(no positional operands)" },
|
|
105
|
-
slug: { type: "str", required: true },
|
|
106
|
-
path: { type: "str", multiple: true },
|
|
107
|
-
format: { type: "enum", values: ["json", "table"], default: "json" },
|
|
108
|
-
cwd: { type: "path" },
|
|
109
|
-
};
|
|
110
|
-
|
|
111
|
-
/**
|
|
112
|
-
* Answer "which scope owns this path" from the committed contracts.
|
|
113
|
-
*
|
|
114
|
-
* @param {string[]} rawArgv - The subcommand's own arguments (harness.mjs strips the verb words).
|
|
115
|
-
* @returns {Promise<void>} Exits 0 with the report, 2 when no scope contract can be read.
|
|
116
|
-
*/
|
|
117
|
-
export async function cli(rawArgv) {
|
|
118
|
-
const args = runArgs(ARGV_SPEC, rawArgv);
|
|
119
|
-
const cwd = resolve(args.cwd || process.cwd());
|
|
120
|
-
const scopes = scopeSubstrates(cwd, args.slug);
|
|
121
|
-
if (!scopes.length) {
|
|
122
|
-
console.error(`probe owner: no readable scope contract for ${args.slug} — ownership is a property of the contracts, and there are none to read`);
|
|
123
|
-
process.exit(2);
|
|
124
|
-
}
|
|
125
|
-
const asked = (args.path || []).map((p) => ({ path: p, cited_by: "--path" }));
|
|
126
|
-
const inputs = asked.length ? asked : seamPaths(cwd, args.slug);
|
|
127
|
-
const paths = inputs.map((i) => ({ ...ownership(i.path, scopes, cwd), cited_by: i.cited_by }));
|
|
128
|
-
const report = {
|
|
129
|
-
slug: args.slug,
|
|
130
|
-
scopes: scopes.length,
|
|
131
|
-
source: asked.length ? "--path" : "wiring-map + project-profile",
|
|
132
|
-
paths,
|
|
133
|
-
unowned: paths.filter((p) => p.writers.length === 0).map((p) => p.path),
|
|
134
|
-
// Owned but absent: the wiring names a file nobody wrote — the gap a census reads as
|
|
135
|
-
// "unowned" when it does not check the disk.
|
|
136
|
-
missing: paths.filter((p) => p.exists === false).map((p) => p.path),
|
|
137
|
-
};
|
|
138
|
-
console.log(args.format === "table" ? renderTable(report) : JSON.stringify(report, null, 2));
|
|
139
|
-
process.exit(0);
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
if (isMain(import.meta.url)) cli(process.argv.slice(2));
|