@mikenguyen69/harness 0.1.0-beta.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.
Files changed (127) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +43 -0
  3. package/dist/build-identity.json +1 -0
  4. package/dist/cancellation.js +138 -0
  5. package/dist/cli.js +47 -0
  6. package/dist/dispatch.js +70 -0
  7. package/dist/help.js +47 -0
  8. package/dist/internal/agent/adapter/conformance.js +53 -0
  9. package/dist/internal/agent/adapter/hook.js +113 -0
  10. package/dist/internal/agent/adapter/install.js +30 -0
  11. package/dist/internal/agent/adapter/scope.js +55 -0
  12. package/dist/internal/agent/adapter/session.js +63 -0
  13. package/dist/internal/agent/adapter/settings.js +51 -0
  14. package/dist/internal/agent/cli.js +79 -0
  15. package/dist/internal/agent/evals/index.js +111 -0
  16. package/dist/internal/agent/index.js +15 -0
  17. package/dist/internal/agent/ledger-read.js +27 -0
  18. package/dist/internal/agent/routing/cli.js +104 -0
  19. package/dist/internal/agent/routing/index.js +123 -0
  20. package/dist/internal/agent/telemetry/index.js +97 -0
  21. package/dist/internal/orchestration/activity/index.js +111 -0
  22. package/dist/internal/orchestration/analyze/index.js +237 -0
  23. package/dist/internal/orchestration/cli.js +475 -0
  24. package/dist/internal/orchestration/config/index.js +66 -0
  25. package/dist/internal/orchestration/doctor/index.js +202 -0
  26. package/dist/internal/orchestration/forge/index.js +121 -0
  27. package/dist/internal/orchestration/git/index.js +181 -0
  28. package/dist/internal/orchestration/harness/cli.js +378 -0
  29. package/dist/internal/orchestration/harness/index.js +186 -0
  30. package/dist/internal/orchestration/harness/shim.js +182 -0
  31. package/dist/internal/orchestration/index.js +29 -0
  32. package/dist/internal/orchestration/loop/index.js +1107 -0
  33. package/dist/internal/orchestration/prompts/index.js +199 -0
  34. package/dist/internal/orchestration/report/index.js +125 -0
  35. package/dist/internal/orchestration/routes/index.js +85 -0
  36. package/dist/internal/orchestration/run/index.js +221 -0
  37. package/dist/internal/orchestration/runner/index.js +90 -0
  38. package/dist/internal/orchestration/runners/claude/index.js +246 -0
  39. package/dist/internal/orchestration/runners/claude/sdk.js +198 -0
  40. package/dist/internal/orchestration/runners/claude/wiring.js +98 -0
  41. package/dist/internal/orchestration/runners/codex/exec.js +114 -0
  42. package/dist/internal/orchestration/runners/codex/index.js +72 -0
  43. package/dist/internal/orchestration/runners/conformance/index.js +169 -0
  44. package/dist/internal/orchestration/runners/cursor/exec.js +531 -0
  45. package/dist/internal/orchestration/runners/cursor/index.js +184 -0
  46. package/dist/internal/orchestration/runners/stub/index.js +168 -0
  47. package/dist/internal/orchestration/runners/telemetry.js +36 -0
  48. package/dist/internal/orchestration/runstate/index.js +213 -0
  49. package/dist/internal/orchestration/runstate/publish.js +115 -0
  50. package/dist/internal/orchestration/worktree/index.js +89 -0
  51. package/dist/internal/system/adapters/index.js +19 -0
  52. package/dist/internal/system/agentops/index.js +16 -0
  53. package/dist/internal/system/agentops/scope.js +67 -0
  54. package/dist/internal/system/assets.js +45 -0
  55. package/dist/internal/system/board/cli.js +56 -0
  56. package/dist/internal/system/board/index.js +276 -0
  57. package/dist/internal/system/board/live.js +277 -0
  58. package/dist/internal/system/calibration/cli.js +45 -0
  59. package/dist/internal/system/calibration/index.js +141 -0
  60. package/dist/internal/system/cli.js +174 -0
  61. package/dist/internal/system/corpus/replay.js +79 -0
  62. package/dist/internal/system/digest/cli.js +23 -0
  63. package/dist/internal/system/digest/index.js +145 -0
  64. package/dist/internal/system/doors/classify.js +94 -0
  65. package/dist/internal/system/doors/cli.js +184 -0
  66. package/dist/internal/system/doors/index.js +249 -0
  67. package/dist/internal/system/doors/metrics.js +47 -0
  68. package/dist/internal/system/escapes/cli.js +45 -0
  69. package/dist/internal/system/escapes/index.js +81 -0
  70. package/dist/internal/system/explore/cli.js +77 -0
  71. package/dist/internal/system/explore/index.js +91 -0
  72. package/dist/internal/system/gates/cli.js +43 -0
  73. package/dist/internal/system/gates/detectors.js +55 -0
  74. package/dist/internal/system/gates/pre.js +51 -0
  75. package/dist/internal/system/hub/cli.js +51 -0
  76. package/dist/internal/system/hub/index.js +146 -0
  77. package/dist/internal/system/init/index.js +83 -0
  78. package/dist/internal/system/ledger/cli.js +54 -0
  79. package/dist/internal/system/ledger/index.js +176 -0
  80. package/dist/internal/system/manifest/index.js +207 -0
  81. package/dist/internal/system/provenance/index.js +13 -0
  82. package/dist/internal/system/report/index.js +15 -0
  83. package/dist/internal/system/runner/index.js +74 -0
  84. package/dist/internal/system/runstatus/cli.js +86 -0
  85. package/dist/internal/system/runstatus/index.js +168 -0
  86. package/dist/internal/system/sequencer/briefing.js +75 -0
  87. package/dist/internal/system/sequencer/cli.js +170 -0
  88. package/dist/internal/system/sequencer/index.js +156 -0
  89. package/dist/internal/system/spec/cli.js +110 -0
  90. package/dist/internal/system/spec/index.js +212 -0
  91. package/dist/internal/system/spec/materialize.js +111 -0
  92. package/dist/internal/system/spec/openspec-parse.js +83 -0
  93. package/dist/internal/system/spec/project.js +208 -0
  94. package/dist/internal/system/substrate/index.js +14 -0
  95. package/dist/internal/system/system/index.js +159 -0
  96. package/dist/internal/system/units/index.js +87 -0
  97. package/dist/internal/system/verify/cli.js +103 -0
  98. package/dist/internal/system/verify/declined-reasons.js +61 -0
  99. package/dist/internal/system/verify/index.js +188 -0
  100. package/dist/internal/system/verify/signing.js +79 -0
  101. package/dist/paths.js +50 -0
  102. package/dist/routes.js +93 -0
  103. package/dist/version.js +30 -0
  104. package/dist/win32-process-group.js +360 -0
  105. package/package.json +41 -0
  106. package/profiles/python/bindings.toml +59 -0
  107. package/profiles/terraform/bindings.toml +66 -0
  108. package/profiles/typescript/bindings.toml +64 -0
  109. package/profiles/typescript/smoke/README.md +8 -0
  110. package/schema/baseline.schema.json +26 -0
  111. package/schema/decision-classes.yaml +45 -0
  112. package/schema/envelope.schema.json +186 -0
  113. package/schema/ledger-event.schema.json +235 -0
  114. package/schema/manifest.schema.json +119 -0
  115. package/schema/routes.schema.json +55 -0
  116. package/schema/run-status.schema.json +162 -0
  117. package/schema/session-event.schema.json +74 -0
  118. package/schema/spec.schema.json +114 -0
  119. package/schema/system.schema.json +142 -0
  120. package/templates/ci/verify.yml +23 -0
  121. package/templates/target-kit/README.md +78 -0
  122. package/templates/target-kit/component.toml +20 -0
  123. package/templates/target-kit/orchestration.json +9 -0
  124. package/templates/target-kit/profiles/target/bindings.toml +6 -0
  125. package/templates/target-kit/routes.toml +5 -0
  126. package/templates/target-kit/specs/first-unit.yaml +11 -0
  127. package/templates/target-kit/system.toml +7 -0
@@ -0,0 +1,212 @@
1
+ /**
2
+ * spec — the `spec` gate. Lints specs/*.yaml against schema/spec.schema.json
3
+ * (every acceptance criterion names a verifier or is explicitly
4
+ * manual-with-reason) and then cross-checks the set: unique ids, an acyclic
5
+ * dep DAG, and every `consumes` artifact produced by an upstream unit.
6
+ *
7
+ * This mechanizes the FORM of specs (principle 2, one layer up). Whether the
8
+ * criteria capture the right intent stays human judgment upstream; the
9
+ * defect→spec calibration loop is what ratchets that over time.
10
+ */
11
+ import { readdirSync, readFileSync } from "node:fs";
12
+ import { join } from "node:path";
13
+ import { Ajv2020 } from "ajv/dist/2020.js";
14
+ import addFormatsDefault from "ajv-formats";
15
+ import { parse as parseYaml } from "yaml";
16
+ import { packageSchemaDir } from "../assets.js";
17
+ const addFormats = addFormatsDefault;
18
+ function schemaPath() {
19
+ return join(packageSchemaDir(), "spec.schema.json");
20
+ }
21
+ let _validate;
22
+ function specValidator() {
23
+ if (!_validate) {
24
+ const ajv = new Ajv2020({ allErrors: true, strict: false });
25
+ addFormats(ajv);
26
+ _validate = ajv.compile(JSON.parse(readFileSync(schemaPath(), "utf8")));
27
+ }
28
+ return _validate;
29
+ }
30
+ export function loadSpecs(specsDir) {
31
+ const files = readdirSync(specsDir).filter((n) => n.endsWith(".yaml") || n.endsWith(".yml"));
32
+ return files.map((f) => ({
33
+ path: join(specsDir, f),
34
+ spec: parseYaml(readFileSync(join(specsDir, f), "utf8")),
35
+ }));
36
+ }
37
+ /**
38
+ * The canonical unit identifier is the globally unique bare spec id. Component
39
+ * identity is a separate field; a `component#id` dependency is accepted only
40
+ * as an explicit cross-component reference and normalizes to this value.
41
+ */
42
+ export function canonicalUnitId(ref) {
43
+ return ref.includes("#") ? ref.slice(ref.indexOf("#") + 1) : ref;
44
+ }
45
+ /** Full check: schema + uniqueness + acyclic DAG + consumes⊆upstream-produces + system.toml cross-refs. */
46
+ export function checkSpecs(specsDir, opts = {}) {
47
+ const findings = [];
48
+ let loaded;
49
+ try {
50
+ loaded = loadSpecs(specsDir);
51
+ }
52
+ catch (e) {
53
+ return [
54
+ {
55
+ specPath: specsDir,
56
+ level: "error",
57
+ message: `cannot read specs: ${e.message}`,
58
+ },
59
+ ];
60
+ }
61
+ const validate = specValidator();
62
+ for (const { path, spec } of loaded) {
63
+ if (!validate(spec)) {
64
+ for (const err of validate.errors ?? []) {
65
+ findings.push({
66
+ specPath: path,
67
+ level: "error",
68
+ message: `${err.instancePath || "/"} ${err.message ?? "invalid"}`,
69
+ });
70
+ }
71
+ }
72
+ if (typeof spec?.id === "string" && spec.id.includes("#")) {
73
+ findings.push({
74
+ specPath: path,
75
+ level: "error",
76
+ message: `spec id "${spec.id}" must be the bare canonical unit id; put component identity in "component"`,
77
+ });
78
+ }
79
+ if (opts.systemComponents &&
80
+ spec?.component &&
81
+ !opts.systemComponents.includes(spec.component)) {
82
+ findings.push({
83
+ specPath: path,
84
+ level: "error",
85
+ message: `component "${spec.component}" is not declared in system.toml`,
86
+ });
87
+ }
88
+ for (const out of spec?.outputs ?? []) {
89
+ const producer = opts.artifactProducers?.[out];
90
+ if (opts.artifactProducers && producer === undefined) {
91
+ findings.push({
92
+ specPath: path,
93
+ level: "error",
94
+ message: `output artifact "${out}" is not declared in system.toml`,
95
+ });
96
+ }
97
+ else if (producer !== undefined && producer !== spec.component) {
98
+ findings.push({
99
+ specPath: path,
100
+ level: "error",
101
+ message: `output "${out}" is produced by "${producer}" in system.toml, not "${spec.component}"`,
102
+ });
103
+ }
104
+ }
105
+ }
106
+ // uniqueness
107
+ const byId = new Map();
108
+ for (const ls of loaded) {
109
+ const id = canonicalUnitId(ls.spec?.id ?? "");
110
+ if (id === "")
111
+ continue;
112
+ if (byId.has(id)) {
113
+ findings.push({
114
+ specPath: ls.path,
115
+ level: "error",
116
+ message: `duplicate unit id "${id}" (also in ${byId.get(id).path})`,
117
+ });
118
+ }
119
+ else {
120
+ byId.set(id, ls);
121
+ }
122
+ }
123
+ // dep graph: unknown refs + cycles
124
+ const deps = new Map();
125
+ for (const ls of loaded) {
126
+ const id = canonicalUnitId(ls.spec?.id ?? "");
127
+ const ds = (ls.spec?.deps ?? []).map(canonicalUnitId);
128
+ deps.set(id, ds);
129
+ for (const [index, d] of ds.entries()) {
130
+ if (!byId.has(d)) {
131
+ findings.push({
132
+ specPath: ls.path,
133
+ level: "error",
134
+ message: `dep "${d}" refers to no known spec`,
135
+ });
136
+ }
137
+ else {
138
+ const raw = ls.spec.deps?.[index];
139
+ const separator = raw?.indexOf("#") ?? -1;
140
+ if (raw && separator >= 0) {
141
+ const declaredComponent = raw.slice(0, separator);
142
+ const actualComponent = byId.get(d)?.spec.component;
143
+ if (actualComponent && declaredComponent !== actualComponent) {
144
+ findings.push({
145
+ specPath: ls.path,
146
+ level: "error",
147
+ message: `dep "${raw}" names component "${declaredComponent}" but unit "${d}" belongs to "${actualComponent}"`,
148
+ });
149
+ }
150
+ }
151
+ }
152
+ }
153
+ }
154
+ const WHITE = 0, GREY = 1, BLACK = 2;
155
+ const color = new Map();
156
+ const cycleFrom = (n) => {
157
+ color.set(n, GREY);
158
+ for (const d of deps.get(n) ?? []) {
159
+ if (!byId.has(d))
160
+ continue;
161
+ const c = color.get(d) ?? WHITE;
162
+ if (c === GREY)
163
+ return true;
164
+ if (c === WHITE && cycleFrom(d))
165
+ return true;
166
+ }
167
+ color.set(n, BLACK);
168
+ return false;
169
+ };
170
+ for (const id of byId.keys()) {
171
+ if ((color.get(id) ?? WHITE) === WHITE && cycleFrom(id)) {
172
+ findings.push({
173
+ specPath: byId.get(id).path,
174
+ level: "error",
175
+ message: `dependency cycle through "${id}"`,
176
+ });
177
+ }
178
+ }
179
+ // consumes ⊆ artifacts produced by a transitive upstream unit
180
+ const upstream = (id, seen = new Set()) => {
181
+ for (const d of deps.get(id) ?? []) {
182
+ if (seen.has(d))
183
+ continue;
184
+ seen.add(d);
185
+ upstream(d, seen);
186
+ }
187
+ return seen;
188
+ };
189
+ for (const ls of loaded) {
190
+ const id = canonicalUnitId(ls.spec?.id ?? "");
191
+ const up = upstream(id);
192
+ const producedUpstream = new Set();
193
+ for (const u of up)
194
+ for (const o of byId.get(u)?.spec.outputs ?? [])
195
+ producedUpstream.add(o);
196
+ for (const c of ls.spec?.consumes ?? []) {
197
+ if (!producedUpstream.has(c)) {
198
+ findings.push({
199
+ specPath: ls.path,
200
+ level: "error",
201
+ message: `consumes "${c}" but no upstream unit in its dep chain produces it`,
202
+ });
203
+ }
204
+ }
205
+ }
206
+ return findings;
207
+ }
208
+ /** @deprecated criterion-level subset kept for the gate runner's original contract */
209
+ export function lintSpecs(specsDir) {
210
+ return checkSpecs(specsDir).filter((f) => f.specPath.endsWith(".yaml") || f.specPath.endsWith(".yml"));
211
+ }
212
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Accepted spec DAG -> durable `unit.planned` facts.
3
+ *
4
+ * Materialization is deliberately two phase: validate the whole input and
5
+ * detect every conflict before the first append. Re-running identical input is
6
+ * a no-op; changing an already-materialized unit fails closed.
7
+ */
8
+ import { relative } from "node:path";
9
+ import { append, canonicalJSON, read as ledgerRead, } from "../ledger/index.js";
10
+ import { loadSystem } from "../system/index.js";
11
+ import { canonicalUnitId, checkSpecs, loadSpecs, } from "./index.js";
12
+ function checkOptions(opts) {
13
+ const manifest = loadSystem(opts.systemToml);
14
+ return {
15
+ systemComponents: manifest.component.map((component) => component.id),
16
+ artifactProducers: Object.fromEntries(manifest.artifact.map((artifact) => [artifact.name, artifact.producer])),
17
+ };
18
+ }
19
+ /** Complete, deterministic ledger body for one accepted spec. */
20
+ export function plannedBody(spec, specPath, specsDir) {
21
+ return {
22
+ unit_id: canonicalUnitId(spec.id),
23
+ component: spec.component,
24
+ mode: spec.mode ?? "build",
25
+ deps: (spec.deps ?? []).map(canonicalUnitId),
26
+ intent: spec.intent,
27
+ acceptance_criteria: spec.acceptance_criteria,
28
+ scope: spec.scope,
29
+ ...(spec.size_budget ? { size_budget: spec.size_budget } : {}),
30
+ ...(spec.door ? { door: spec.door } : {}),
31
+ merge_facts: {
32
+ adds_dependency: spec.merge_facts?.adds_dependency ?? false,
33
+ schema_change: spec.merge_facts?.schema_change ?? false,
34
+ security: spec.merge_facts?.security ?? false,
35
+ gate_external: spec.merge_facts?.gate_external ?? false,
36
+ artifact_producer_change: spec.merge_facts?.artifact_producer_change ?? false,
37
+ },
38
+ outputs: spec.outputs ?? [],
39
+ consumes: spec.consumes ?? [],
40
+ spec_path: relative(specsDir, specPath).replaceAll("\\", "/"),
41
+ };
42
+ }
43
+ export function materializeSpecs(opts) {
44
+ let validationOptions;
45
+ try {
46
+ validationOptions = checkOptions(opts);
47
+ }
48
+ catch (error) {
49
+ return {
50
+ ok: false,
51
+ units: [],
52
+ findings: [{
53
+ specPath: opts.systemToml,
54
+ level: "error",
55
+ message: `cannot read system manifest: ${error.message}`,
56
+ }],
57
+ conflicts: [],
58
+ };
59
+ }
60
+ const findings = checkSpecs(opts.specsDir, validationOptions);
61
+ if (findings.some((finding) => finding.level === "error")) {
62
+ return { ok: false, units: [], findings, conflicts: [] };
63
+ }
64
+ const manifest = loadSystem(opts.systemToml);
65
+ const candidates = loadSpecs(opts.specsDir)
66
+ .map((loaded) => ({
67
+ id: canonicalUnitId(loaded.spec.id),
68
+ body: plannedBody(loaded.spec, loaded.path, opts.specsDir),
69
+ }))
70
+ .sort((a, b) => a.id.localeCompare(b.id));
71
+ const existing = new Map();
72
+ for (const event of ledgerRead(opts.ledgerDir)) {
73
+ if (event.type !== "unit.planned")
74
+ continue;
75
+ const id = event.body.unit_id;
76
+ if (typeof id === "string")
77
+ existing.set(canonicalUnitId(id), event.body);
78
+ }
79
+ const conflicts = candidates
80
+ .filter(({ id, body }) => {
81
+ const current = existing.get(id);
82
+ return current !== undefined && canonicalJSON(current) !== canonicalJSON(body);
83
+ })
84
+ .map(({ id }) => ({
85
+ id,
86
+ message: `unit "${id}" is already materialized with a different contract`,
87
+ }));
88
+ if (conflicts.length > 0)
89
+ return { ok: false, units: [], findings, conflicts };
90
+ const units = [];
91
+ for (const candidate of candidates) {
92
+ if (existing.has(candidate.id)) {
93
+ units.push({ id: candidate.id, status: "already_materialized" });
94
+ continue;
95
+ }
96
+ const event = append(opts.ledgerDir, {
97
+ system: manifest.system.id,
98
+ actor: { kind: "harness", id: "spec-materializer" },
99
+ type: "unit.planned",
100
+ body: candidate.body,
101
+ });
102
+ units.push({
103
+ id: candidate.id,
104
+ status: "materialized",
105
+ seq: event.seq,
106
+ hash: event.hash,
107
+ });
108
+ }
109
+ return { ok: true, units, findings, conflicts: [] };
110
+ }
111
+ //# sourceMappingURL=materialize.js.map
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Minimal OpenSpec capability-spec parser for harness spec projection.
3
+ *
4
+ * Parses `### Requirement:` blocks and optional `#### Scenario:` sub-blocks
5
+ * from markdown spec files under openspec/specs/ and change deltas.
6
+ */
7
+ import { readFileSync, existsSync } from "node:fs";
8
+ import { join } from "node:path";
9
+ const REQUIREMENT = /^### Requirement:\s*(.+)$/m;
10
+ const SCENARIO = /^#### Scenario:\s*(.+)$/m;
11
+ /** Split markdown into requirement blocks after the first `### Requirement:` header. */
12
+ export function parseCapabilityMarkdown(content) {
13
+ const start = content.search(/^### Requirement:/m);
14
+ if (start < 0)
15
+ return [];
16
+ const tail = content.slice(start);
17
+ const chunks = tail.split(/\n(?=### Requirement:)/);
18
+ const requirements = [];
19
+ for (const chunk of chunks) {
20
+ const header = chunk.match(REQUIREMENT);
21
+ if (!header)
22
+ continue;
23
+ const name = header[1].trim();
24
+ const afterHeader = chunk.slice(header.index + header[0].length).trim();
25
+ const scenarioParts = afterHeader.split(/\n(?=#### Scenario:)/);
26
+ const bodyPart = scenarioParts[0].trim();
27
+ const scenarios = [];
28
+ for (let i = 1; i < scenarioParts.length; i++) {
29
+ const part = scenarioParts[i];
30
+ const scenarioHeader = part.match(SCENARIO);
31
+ if (!scenarioHeader)
32
+ continue;
33
+ scenarios.push({
34
+ name: scenarioHeader[1].trim(),
35
+ body: part.slice(scenarioHeader.index + scenarioHeader[0].length).trim(),
36
+ });
37
+ }
38
+ requirements.push({ name, body: bodyPart, scenarios });
39
+ }
40
+ return requirements;
41
+ }
42
+ function normalizeName(name) {
43
+ return name.trim().toLowerCase();
44
+ }
45
+ /** Merge ADDED requirement blocks from a change delta into a requirement map. */
46
+ export function mergeAddedRequirements(base, delta) {
47
+ const byName = new Map(base.map((r) => [normalizeName(r.name), r]));
48
+ const addedSection = delta.match(/## ADDED Requirements\s*\n([\s\S]*?)(?:\n## (?:MODIFIED|REMOVED|RENAMED) Requirements|\s*$)/);
49
+ if (!addedSection)
50
+ return base;
51
+ for (const req of parseCapabilityMarkdown(addedSection[1])) {
52
+ byName.set(normalizeName(req.name), req);
53
+ }
54
+ return [...byName.values()];
55
+ }
56
+ export function loadCapabilityRequirements(openspecRoot, capability, changeDir) {
57
+ const segments = capability.split("/");
58
+ const mainPath = join(openspecRoot, "specs", ...segments, "spec.md");
59
+ let requirements = [];
60
+ if (existsSync(mainPath)) {
61
+ requirements = parseCapabilityMarkdown(readFileSync(mainPath, "utf8"));
62
+ }
63
+ if (changeDir) {
64
+ const deltaPath = join(changeDir, "specs", ...segments, "spec.md");
65
+ if (existsSync(deltaPath)) {
66
+ requirements = mergeAddedRequirements(requirements, readFileSync(deltaPath, "utf8"));
67
+ }
68
+ }
69
+ return new Map(requirements.map((r) => [normalizeName(r.name), r]));
70
+ }
71
+ export function resolveRequirementStatement(req, scenarioName) {
72
+ if (!scenarioName)
73
+ return req.body.trim();
74
+ const scenario = req.scenarios.find((s) => normalizeName(s.name) === normalizeName(scenarioName));
75
+ if (!scenario) {
76
+ throw new Error(`scenario "${scenarioName}" not found on requirement "${req.name}"`);
77
+ }
78
+ const scenarioText = scenario.body.trim();
79
+ if (!req.body.trim())
80
+ return scenarioText;
81
+ return `${req.body.trim()}\n${scenarioText}`;
82
+ }
83
+ //# sourceMappingURL=openspec-parse.js.map
@@ -0,0 +1,208 @@
1
+ /**
2
+ * OpenSpec → harness unit projector.
3
+ *
4
+ * Reads a change's harness.projection.yaml sidecar, resolves acceptance
5
+ * criteria from OpenSpec capability specs, and emits harness unit YAML files.
6
+ */
7
+ import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
8
+ import { join } from "node:path";
9
+ import { parse as parseYaml } from "yaml";
10
+ import { checkSpecs, } from "./index.js";
11
+ import { loadCapabilityRequirements, resolveRequirementStatement, } from "./openspec-parse.js";
12
+ function loadProjection(changeDir) {
13
+ const path = join(changeDir, "harness.projection.yaml");
14
+ const raw = parseYaml(readFileSync(path, "utf8"));
15
+ if (raw.schema !== "harness.projection/v1") {
16
+ throw new Error(`unsupported projection schema "${raw.schema}" (expected harness.projection/v1)`);
17
+ }
18
+ if (!raw.change || !raw.component || !Array.isArray(raw.units)) {
19
+ throw new Error("harness.projection.yaml missing change, component, or units");
20
+ }
21
+ return raw;
22
+ }
23
+ function capabilityCache() {
24
+ return new Map();
25
+ }
26
+ function requirementsFor(cache, openspecRoot, changeDir, capability) {
27
+ let loaded = cache.get(capability);
28
+ if (!loaded) {
29
+ loaded = loadCapabilityRequirements(openspecRoot, capability, changeDir);
30
+ cache.set(capability, loaded);
31
+ }
32
+ return loaded;
33
+ }
34
+ function resolveAcceptance(entry, cache, openspecRoot, changeDir, unitId, index) {
35
+ if (entry.statement) {
36
+ const criterion = { statement: entry.statement.trim() };
37
+ if (entry.verifier)
38
+ criterion.verifier = entry.verifier;
39
+ if (entry.manual)
40
+ criterion.manual = entry.manual.trim();
41
+ return { criterion };
42
+ }
43
+ const reqs = requirementsFor(cache, openspecRoot, changeDir, entry.source.capability);
44
+ const key = entry.source.requirement.trim().toLowerCase();
45
+ const req = reqs.get(key);
46
+ if (!req) {
47
+ return {
48
+ criterion: { statement: "UNRESOLVED" },
49
+ finding: {
50
+ specPath: join(changeDir, "harness.projection.yaml"),
51
+ level: "error",
52
+ message: `unit "${unitId}" acceptance[${index}]: requirement "${entry.source.requirement}" not found in capability "${entry.source.capability}"`,
53
+ criterionIndex: index,
54
+ },
55
+ };
56
+ }
57
+ try {
58
+ const statement = resolveRequirementStatement(req, entry.source.scenario);
59
+ const criterion = { statement };
60
+ if (entry.verifier)
61
+ criterion.verifier = entry.verifier;
62
+ if (entry.manual)
63
+ criterion.manual = entry.manual.trim();
64
+ if (!criterion.verifier && !criterion.manual) {
65
+ return {
66
+ criterion,
67
+ finding: {
68
+ specPath: join(changeDir, "harness.projection.yaml"),
69
+ level: "error",
70
+ message: `unit "${unitId}" acceptance[${index}]: must name verifier or manual`,
71
+ criterionIndex: index,
72
+ },
73
+ };
74
+ }
75
+ return { criterion };
76
+ }
77
+ catch (error) {
78
+ return {
79
+ criterion: { statement: "UNRESOLVED" },
80
+ finding: {
81
+ specPath: join(changeDir, "harness.projection.yaml"),
82
+ level: "error",
83
+ message: `unit "${unitId}" acceptance[${index}]: ${error.message}`,
84
+ criterionIndex: index,
85
+ },
86
+ };
87
+ }
88
+ }
89
+ function toHarnessSpec(projection, unit, cache, openspecRoot, changeDir) {
90
+ const findings = [];
91
+ const acceptance_criteria = [];
92
+ for (const [index, entry] of unit.acceptance.entries()) {
93
+ const { criterion, finding } = resolveAcceptance(entry, cache, openspecRoot, changeDir, unit.id, index);
94
+ acceptance_criteria.push(criterion);
95
+ if (finding)
96
+ findings.push(finding);
97
+ }
98
+ const spec = {
99
+ id: unit.id,
100
+ component: projection.component,
101
+ intent: unit.intent.trim(),
102
+ acceptance_criteria,
103
+ scope: unit.scope,
104
+ ...(unit.mode ? { mode: unit.mode } : {}),
105
+ ...(unit.size_budget ? { size_budget: unit.size_budget } : {}),
106
+ ...(unit.door ? { door: unit.door } : {}),
107
+ merge_facts: {
108
+ adds_dependency: unit.merge_facts?.adds_dependency ?? false,
109
+ ...(unit.merge_facts?.schema_change ? { schema_change: true } : {}),
110
+ ...(unit.merge_facts?.security ? { security: true } : {}),
111
+ ...(unit.merge_facts?.gate_external ? { gate_external: true } : {}),
112
+ ...(unit.merge_facts?.artifact_producer_change
113
+ ? { artifact_producer_change: true }
114
+ : {}),
115
+ },
116
+ ...(unit.deps?.length ? { deps: unit.deps } : {}),
117
+ ...(unit.outputs?.length ? { outputs: unit.outputs } : {}),
118
+ ...(unit.consumes?.length ? { consumes: unit.consumes } : {}),
119
+ };
120
+ return { spec, findings };
121
+ }
122
+ export function emitUnitYamlPretty(spec) {
123
+ const acLines = spec.acceptance_criteria
124
+ .map((ac) => {
125
+ const parts = [` - statement: >`, ...indentBlock(ac.statement, 6)];
126
+ if (ac.verifier)
127
+ parts.push(` verifier: ${ac.verifier}`);
128
+ if (ac.manual) {
129
+ parts.push(` manual: >`, ...indentBlock(ac.manual, 6));
130
+ }
131
+ return parts.join("\n");
132
+ })
133
+ .join("\n");
134
+ const lines = [
135
+ `id: ${spec.id}`,
136
+ `component: ${spec.component}`,
137
+ `intent: >`,
138
+ ...indentBlock(spec.intent.trim(), 2),
139
+ `acceptance_criteria:`,
140
+ acLines,
141
+ `scope:`,
142
+ ...spec.scope.map((s) => ` - ${s}`),
143
+ ];
144
+ if (spec.size_budget) {
145
+ lines.push(`size_budget:`);
146
+ if (spec.size_budget.max_changed_lines !== undefined) {
147
+ lines.push(` max_changed_lines: ${spec.size_budget.max_changed_lines}`);
148
+ }
149
+ if (spec.size_budget.max_changed_files !== undefined) {
150
+ lines.push(` max_changed_files: ${spec.size_budget.max_changed_files}`);
151
+ }
152
+ }
153
+ if (spec.deps?.length) {
154
+ lines.push(`deps:`);
155
+ for (const dep of spec.deps)
156
+ lines.push(` - ${dep}`);
157
+ }
158
+ if (spec.door)
159
+ lines.push(`door: ${spec.door}`);
160
+ const mf = spec.merge_facts;
161
+ if (mf) {
162
+ lines.push(`merge_facts:`);
163
+ lines.push(` adds_dependency: ${mf.adds_dependency ?? false}`);
164
+ if (mf.schema_change)
165
+ lines.push(` schema_change: true`);
166
+ if (mf.security)
167
+ lines.push(` security: true`);
168
+ if (mf.gate_external)
169
+ lines.push(` gate_external: true`);
170
+ if (mf.artifact_producer_change) {
171
+ lines.push(` artifact_producer_change: true`);
172
+ }
173
+ }
174
+ if (spec.outputs?.length) {
175
+ lines.push(`outputs:`);
176
+ for (const o of spec.outputs)
177
+ lines.push(` - ${o}`);
178
+ }
179
+ if (spec.consumes?.length) {
180
+ lines.push(`consumes:`);
181
+ for (const c of spec.consumes)
182
+ lines.push(` - ${c}`);
183
+ }
184
+ return lines.join("\n") + "\n";
185
+ }
186
+ function indentBlock(text, spaces) {
187
+ const pad = " ".repeat(spaces);
188
+ return text.trim().split("\n").map((line) => `${pad}${line}`);
189
+ }
190
+ export function projectSpecs(opts) {
191
+ const projection = loadProjection(opts.changeDir);
192
+ const cache = capabilityCache();
193
+ mkdirSync(opts.outDir, { recursive: true });
194
+ const findings = [];
195
+ const units = [];
196
+ for (const unit of projection.units) {
197
+ const { spec, findings: unitFindings } = toHarnessSpec(projection, unit, cache, opts.openspecRoot, opts.changeDir);
198
+ findings.push(...unitFindings);
199
+ const outPath = join(opts.outDir, `${unit.id}.yaml`);
200
+ writeFileSync(outPath, emitUnitYamlPretty(spec), "utf8");
201
+ units.push({ id: unit.id, path: outPath });
202
+ }
203
+ const checkFindings = checkSpecs(opts.outDir);
204
+ findings.push(...checkFindings);
205
+ const errors = findings.filter((f) => f.level === "error");
206
+ return { ok: errors.length === 0, units, findings };
207
+ }
208
+ //# sourceMappingURL=project.js.map
@@ -0,0 +1,14 @@
1
+ /**
2
+ * substrate — applies and audits the agent-blind guarantee layer: branch
3
+ * protection (nothing lands without a green envelope; no client-side
4
+ * --no-verify bypass because enforcement isn't client-side), required
5
+ * workflows, merge-queue settings.
6
+ *
7
+ * `harness init` applies it; `harness verify` includes an audit that the
8
+ * substrate is still configured — a guardrail is only as strong as the
9
+ * lowest layer that enforces it, and this IS the lowest layer.
10
+ */
11
+ export function audit(_projectRoot) {
12
+ throw new Error("not implemented (skeleton)");
13
+ }
14
+ //# sourceMappingURL=index.js.map