@opengsd/gsd-core 1.4.3 → 1.5.0-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +3 -3
- package/agents/gsd-executor.md +1 -1
- package/agents/gsd-plan-checker.md +11 -2
- package/agents/gsd-planner.md +11 -7
- package/agents/gsd-verifier.md +3 -3
- package/bin/install.js +825 -149
- package/commands/gsd/autonomous.md +5 -1
- package/commands/gsd/ns-manage.md +8 -1
- package/commands/gsd/ns-project.md +5 -0
- package/commands/gsd/ns-review.md +4 -1
- package/commands/gsd/ns-workflow.md +7 -1
- package/commands/gsd/plan-review-convergence.md +5 -4
- package/commands/gsd/surface.md +12 -5
- package/gemini-extension.json +1 -1
- package/gsd-core/bin/gsd-tools.cjs +198 -101
- package/gsd-core/bin/lib/audit-command-router.cjs +61 -0
- package/gsd-core/bin/lib/capability-registry.cjs +417 -0
- package/gsd-core/bin/lib/capability-state.cjs +336 -0
- package/gsd-core/bin/lib/cli-exit.cjs +22 -3
- package/gsd-core/bin/lib/config-loader.cjs +716 -0
- package/gsd-core/bin/lib/configuration.cjs +4 -34
- package/gsd-core/bin/lib/core-utils.cjs +198 -0
- package/gsd-core/bin/lib/core.cjs +57 -1817
- package/gsd-core/bin/lib/federated-config.cjs +182 -0
- package/gsd-core/bin/lib/graphify-command-router.cjs +74 -0
- package/gsd-core/bin/lib/init.cjs +58 -12
- package/gsd-core/bin/lib/install-profiles.cjs +157 -3
- package/gsd-core/bin/lib/intel-command-router.cjs +116 -0
- package/gsd-core/bin/lib/io.cjs +167 -0
- package/gsd-core/bin/lib/loop-host-contract.cjs +105 -0
- package/gsd-core/bin/lib/loop-resolver.cjs +460 -0
- package/gsd-core/bin/lib/model-resolver.cjs +426 -0
- package/gsd-core/bin/lib/phase-id.cjs +215 -0
- package/gsd-core/bin/lib/phase-locator.cjs +148 -0
- package/gsd-core/bin/lib/profile-pipeline.cjs +2 -2
- package/gsd-core/bin/lib/roadmap-parser.cjs +443 -0
- package/gsd-core/bin/lib/roadmap.cjs +4 -1
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +96 -9
- package/gsd-core/bin/lib/runtime-config-adapter-registry.cjs +2 -1
- package/gsd-core/bin/lib/runtime-homes.cjs +40 -0
- package/gsd-core/bin/lib/runtime-name-policy.cjs +1 -0
- package/gsd-core/bin/lib/shell-command-projection.cjs +13 -0
- package/gsd-core/bin/lib/state.cjs +114 -2
- package/gsd-core/bin/lib/surface.cjs +66 -14
- package/gsd-core/bin/lib/verify.cjs +1 -1
- package/gsd-core/bin/shared/model-catalog.json +5 -0
- package/gsd-core/bin/shared/runtime-aliases.manifest.json +3 -0
- package/gsd-core/references/planner-reviews.md +5 -2
- package/gsd-core/templates/phase-prompt.md +7 -7
- package/gsd-core/templates/project.md +19 -2
- package/gsd-core/templates/summary-complex.md +1 -0
- package/gsd-core/templates/summary-minimal.md +1 -0
- package/gsd-core/templates/summary-standard.md +1 -0
- package/gsd-core/templates/summary.md +1 -0
- package/gsd-core/workflows/_runtime-launcher.snippet.sh +1 -1
- package/gsd-core/workflows/add-backlog.md +1 -1
- package/gsd-core/workflows/add-phase.md +1 -1
- package/gsd-core/workflows/add-tests.md +1 -1
- package/gsd-core/workflows/add-todo.md +1 -1
- package/gsd-core/workflows/ai-integration-phase.md +1 -1
- package/gsd-core/workflows/audit-fix.md +1 -1
- package/gsd-core/workflows/audit-milestone.md +1 -1
- package/gsd-core/workflows/audit-uat.md +1 -1
- package/gsd-core/workflows/autonomous.md +86 -15
- package/gsd-core/workflows/check-todos.md +1 -1
- package/gsd-core/workflows/cleanup.md +1 -1
- package/gsd-core/workflows/code-review-fix.md +6 -4
- package/gsd-core/workflows/code-review.md +3 -2
- package/gsd-core/workflows/complete-milestone.md +11 -5
- package/gsd-core/workflows/debug.md +1 -1
- package/gsd-core/workflows/diagnose-issues.md +1 -1
- package/gsd-core/workflows/discuss-phase/modes/advisor.md +1 -1
- package/gsd-core/workflows/discuss-phase/modes/auto.md +1 -1
- package/gsd-core/workflows/discuss-phase/modes/chain.md +1 -1
- package/gsd-core/workflows/discuss-phase-assumptions.md +1 -1
- package/gsd-core/workflows/discuss-phase.md +8 -1
- package/gsd-core/workflows/do.md +1 -1
- package/gsd-core/workflows/docs-update.md +1 -1
- package/gsd-core/workflows/edit-phase.md +1 -1
- package/gsd-core/workflows/eval-review.md +4 -1
- package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +1 -1
- package/gsd-core/workflows/execute-phase.md +8 -1
- package/gsd-core/workflows/execute-plan.md +1 -1
- package/gsd-core/workflows/explore.md +1 -1
- package/gsd-core/workflows/extract-learnings.md +1 -1
- package/gsd-core/workflows/forensics.md +1 -1
- package/gsd-core/workflows/graduation.md +1 -1
- package/gsd-core/workflows/health.md +1 -1
- package/gsd-core/workflows/help/modes/full.md +1 -1
- package/gsd-core/workflows/import.md +1 -1
- package/gsd-core/workflows/ingest-docs.md +1 -1
- package/gsd-core/workflows/insert-phase.md +1 -1
- package/gsd-core/workflows/list-workspaces.md +1 -1
- package/gsd-core/workflows/manager.md +1 -1
- package/gsd-core/workflows/map-codebase.md +1 -1
- package/gsd-core/workflows/milestone-summary.md +1 -1
- package/gsd-core/workflows/mvp-phase.md +1 -1
- package/gsd-core/workflows/new-milestone.md +1 -1
- package/gsd-core/workflows/new-project.md +1 -1
- package/gsd-core/workflows/new-workspace.md +1 -1
- package/gsd-core/workflows/next.md +1 -1
- package/gsd-core/workflows/pause-work.md +1 -1
- package/gsd-core/workflows/plan-milestone-gaps.md +1 -1
- package/gsd-core/workflows/plan-phase.md +27 -2
- package/gsd-core/workflows/plan-review-convergence.md +60 -33
- package/gsd-core/workflows/plant-seed.md +1 -1
- package/gsd-core/workflows/profile-user.md +1 -1
- package/gsd-core/workflows/progress.md +1 -1
- package/gsd-core/workflows/quick.md +2 -2
- package/gsd-core/workflows/remove-phase.md +1 -1
- package/gsd-core/workflows/remove-workspace.md +1 -1
- package/gsd-core/workflows/resume-project.md +1 -1
- package/gsd-core/workflows/review.md +1 -1
- package/gsd-core/workflows/scan.md +1 -1
- package/gsd-core/workflows/secure-phase.md +1 -1
- package/gsd-core/workflows/settings-advanced.md +1 -1
- package/gsd-core/workflows/settings-integrations.md +1 -1
- package/gsd-core/workflows/settings.md +1 -1
- package/gsd-core/workflows/ship.md +8 -1
- package/gsd-core/workflows/sketch-wrap-up.md +1 -1
- package/gsd-core/workflows/sketch.md +1 -1
- package/gsd-core/workflows/spec-phase.md +1 -1
- package/gsd-core/workflows/spike-wrap-up.md +1 -1
- package/gsd-core/workflows/spike.md +1 -1
- package/gsd-core/workflows/stats.md +1 -1
- package/gsd-core/workflows/thread.md +1 -1
- package/gsd-core/workflows/transition.md +1 -1
- package/gsd-core/workflows/ui-phase.md +1 -1
- package/gsd-core/workflows/ui-review.md +1 -1
- package/gsd-core/workflows/ultraplan-phase.md +1 -1
- package/gsd-core/workflows/validate-phase.md +1 -1
- package/gsd-core/workflows/verify-phase.md +1 -1
- package/gsd-core/workflows/verify-work.md +8 -1
- package/package.json +7 -2
- package/scripts/base64-scan.sh +1 -1
- package/scripts/changeset/cli.cjs +8 -1
- package/scripts/changeset/lint.cjs +38 -2
- package/scripts/ci-test-scope.cjs +21 -10
- package/scripts/gen-capability-registry.cjs +1808 -0
- package/scripts/gen-loop-host-contract.cjs +471 -0
- package/scripts/lint-regression-test-names.allowlist.json +269 -0
- package/scripts/lint-regression-test-names.cjs +117 -0
- package/scripts/lint-test-file-count.allowlist.json +12 -4
- package/scripts/prompt-injection-scan.sh +4 -4
- package/scripts/run-tests.cjs +79 -16
- package/scripts/secret-scan.sh +3 -3
- package/scripts/run-cross-platform-tests.cjs +0 -67
|
@@ -0,0 +1,471 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* gen-loop-host-contract.cjs — generates gsd-core/bin/lib/loop-host-contract.cjs
|
|
6
|
+
* from the <!-- gsd:loop-host ... --> blocks in the five step workflows.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* node scripts/gen-loop-host-contract.cjs # print to stdout
|
|
10
|
+
* node scripts/gen-loop-host-contract.cjs --write # write loop-host-contract.cjs
|
|
11
|
+
* node scripts/gen-loop-host-contract.cjs --check # exit 1 if committed file is stale
|
|
12
|
+
*
|
|
13
|
+
* ADR-894 phase 3a-impl-2. Parses structured markers from workflow files,
|
|
14
|
+
* cross-checks declared agent-roles against actual agent references in each
|
|
15
|
+
* workflow, asserts that the union of all points equals the 12 canonical points,
|
|
16
|
+
* and emits a committed CommonJS module exporting the contract array.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const fs = require('node:fs');
|
|
20
|
+
const path = require('node:path');
|
|
21
|
+
|
|
22
|
+
const { ExitError, runMain } = require('./lib/cli-exit.cjs');
|
|
23
|
+
|
|
24
|
+
const ROOT = path.resolve(__dirname, '..');
|
|
25
|
+
const WORKFLOWS_DIR = path.join(ROOT, 'gsd-core', 'workflows');
|
|
26
|
+
const CONTRACT_PATH = path.join(ROOT, 'gsd-core', 'bin', 'lib', 'loop-host-contract.cjs');
|
|
27
|
+
|
|
28
|
+
// The five step workflows in pipeline order
|
|
29
|
+
const STEP_WORKFLOWS = [
|
|
30
|
+
{ file: 'discuss-phase.md', step: 'discuss' },
|
|
31
|
+
{ file: 'plan-phase.md', step: 'plan' },
|
|
32
|
+
{ file: 'execute-phase.md', step: 'execute' },
|
|
33
|
+
{ file: 'verify-work.md', step: 'verify' },
|
|
34
|
+
{ file: 'ship.md', step: 'ship' },
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
// Canonical 12 loop points in pipeline order
|
|
38
|
+
const CANONICAL_POINTS = [
|
|
39
|
+
'discuss:pre',
|
|
40
|
+
'discuss:post',
|
|
41
|
+
'plan:pre',
|
|
42
|
+
'plan:post',
|
|
43
|
+
'execute:pre',
|
|
44
|
+
'execute:wave:pre',
|
|
45
|
+
'execute:wave:post',
|
|
46
|
+
'execute:post',
|
|
47
|
+
'verify:pre',
|
|
48
|
+
'verify:post',
|
|
49
|
+
'ship:pre',
|
|
50
|
+
'ship:post',
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
// FIX 1: Per-step canonical point ownership. Each step must declare exactly these points.
|
|
54
|
+
const EXPECTED_POINTS_BY_STEP = {
|
|
55
|
+
discuss: ['discuss:pre', 'discuss:post'],
|
|
56
|
+
plan: ['plan:pre', 'plan:post'],
|
|
57
|
+
execute: ['execute:pre', 'execute:wave:pre', 'execute:wave:post', 'execute:post'],
|
|
58
|
+
verify: ['verify:pre', 'verify:post'],
|
|
59
|
+
ship: ['ship:pre', 'ship:post'],
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
// Role → agent-name mapping used for cross-check.
|
|
63
|
+
// Each non-orchestrator role must correspond to an actual agent reference in
|
|
64
|
+
// the workflow file (e.g. gsd-planner, gsd-executor, gsd-verifier, etc.).
|
|
65
|
+
const ROLE_TO_AGENT = {
|
|
66
|
+
researcher: 'gsd-phase-researcher',
|
|
67
|
+
planner: 'gsd-planner',
|
|
68
|
+
checker: 'gsd-plan-checker',
|
|
69
|
+
executor: 'gsd-executor',
|
|
70
|
+
verifier: 'gsd-verifier',
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
// ─── Parser ───────────────────────────────────────────────────────────────────
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Parse a single <!-- gsd:loop-host ... --> block from file content.
|
|
77
|
+
* Returns a plain object with keys: step, points[], agentRoles[], produces[], consumes[].
|
|
78
|
+
* Throws a descriptive error if the block is malformed or missing.
|
|
79
|
+
*
|
|
80
|
+
* Block format (one key: value per line, comma-separated list values):
|
|
81
|
+
* <!-- gsd:loop-host
|
|
82
|
+
* step: plan
|
|
83
|
+
* points: plan:pre, plan:post
|
|
84
|
+
* agent-roles: researcher, planner, checker
|
|
85
|
+
* produces: PLAN.md
|
|
86
|
+
* consumes: CONTEXT.md
|
|
87
|
+
* -->
|
|
88
|
+
*
|
|
89
|
+
* For empty list values (e.g. "consumes:") the field is an empty array.
|
|
90
|
+
*
|
|
91
|
+
* @param {string} content File content
|
|
92
|
+
* @param {string} fileName For error messages
|
|
93
|
+
* @returns {{ step: string, points: string[], agentRoles: string[], coreArtifacts: { produces: string[], consumes: string[] } }}
|
|
94
|
+
*/
|
|
95
|
+
function parseLoopHostBlock(content, fileName) {
|
|
96
|
+
// FIX 2: Detect ALL marker blocks — more than one is a hard error.
|
|
97
|
+
const blockRe = /<!--\s*gsd:loop-host\s*([\s\S]*?)-->/g;
|
|
98
|
+
const allMatches = Array.from(content.matchAll(blockRe));
|
|
99
|
+
if (allMatches.length === 0) {
|
|
100
|
+
throw new Error(fileName + ': missing <!-- gsd:loop-host ... --> block');
|
|
101
|
+
}
|
|
102
|
+
if (allMatches.length > 1) {
|
|
103
|
+
throw new Error(
|
|
104
|
+
fileName + ': expected exactly one gsd:loop-host marker block, found ' + allMatches.length,
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const blockBody = allMatches[0][1];
|
|
109
|
+
|
|
110
|
+
// FIX 2: Detect duplicate keys within the block.
|
|
111
|
+
const RECOGNIZED_KEYS = ['step', 'points', 'agent-roles', 'produces', 'consumes'];
|
|
112
|
+
const keyCounts = {};
|
|
113
|
+
for (const line of blockBody.split('\n')) {
|
|
114
|
+
const trimmed = line.trim();
|
|
115
|
+
for (const key of RECOGNIZED_KEYS) {
|
|
116
|
+
if (trimmed === key + ':' || trimmed.startsWith(key + ': ') || trimmed.startsWith(key + ':')) {
|
|
117
|
+
keyCounts[key] = (keyCounts[key] || 0) + 1;
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
for (const key of RECOGNIZED_KEYS) {
|
|
123
|
+
if (keyCounts[key] > 1) {
|
|
124
|
+
throw new Error(fileName + ': duplicate key \'' + key + '\' in gsd:loop-host marker');
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Parse a field line: "key: value1, value2" → [value1, value2] (trimmed, empty strings removed)
|
|
130
|
+
*/
|
|
131
|
+
function parseField(key) {
|
|
132
|
+
// Split on newlines and find the line starting with "key:"
|
|
133
|
+
const lines = blockBody.split('\n');
|
|
134
|
+
for (const line of lines) {
|
|
135
|
+
const trimmed = line.trim();
|
|
136
|
+
if (trimmed === key + ':' || trimmed.startsWith(key + ': ') || trimmed.startsWith(key + ':')) {
|
|
137
|
+
const colonIdx = trimmed.indexOf(':');
|
|
138
|
+
const raw = trimmed.slice(colonIdx + 1).trim();
|
|
139
|
+
if (raw === '') return [];
|
|
140
|
+
return raw.split(',').map((s) => s.trim()).filter((s) => s.length > 0);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
throw new Error(fileName + ': gsd:loop-host block missing required field "' + key + '"');
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function parseScalar(key) {
|
|
147
|
+
const lines = blockBody.split('\n');
|
|
148
|
+
for (const line of lines) {
|
|
149
|
+
const trimmed = line.trim();
|
|
150
|
+
if (trimmed === key + ':' || trimmed.startsWith(key + ': ') || trimmed.startsWith(key + ':')) {
|
|
151
|
+
const colonIdx = trimmed.indexOf(':');
|
|
152
|
+
const val = trimmed.slice(colonIdx + 1).trim();
|
|
153
|
+
if (val === '') {
|
|
154
|
+
throw new Error(fileName + ': gsd:loop-host block field "' + key + '" must be a non-empty string');
|
|
155
|
+
}
|
|
156
|
+
return val;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
throw new Error(fileName + ': gsd:loop-host block missing required field "' + key + '"');
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const step = parseScalar('step');
|
|
163
|
+
const points = parseField('points');
|
|
164
|
+
const agentRoles = parseField('agent-roles');
|
|
165
|
+
const produces = parseField('produces');
|
|
166
|
+
const consumes = parseField('consumes');
|
|
167
|
+
|
|
168
|
+
if (points.length === 0) {
|
|
169
|
+
throw new Error(fileName + ': gsd:loop-host block "points" must have at least one value');
|
|
170
|
+
}
|
|
171
|
+
if (agentRoles.length === 0) {
|
|
172
|
+
throw new Error(fileName + ': gsd:loop-host block "agent-roles" must have at least one value');
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return {
|
|
176
|
+
step,
|
|
177
|
+
points,
|
|
178
|
+
agentRoles,
|
|
179
|
+
coreArtifacts: { produces, consumes },
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// ─── Cross-check: declared roles vs. actual agent references ─────────────────
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* For each non-orchestrator role in agentRoles, verify the workflow content
|
|
187
|
+
* contains a reference to the corresponding agent name.
|
|
188
|
+
*
|
|
189
|
+
* @param {string} content Full workflow file content
|
|
190
|
+
* @param {string[]} agentRoles Roles declared in the block
|
|
191
|
+
* @param {string} fileName For error messages
|
|
192
|
+
* @returns {string[]} Array of error strings; empty = OK
|
|
193
|
+
*/
|
|
194
|
+
/**
|
|
195
|
+
* Escape a string for literal use in a RegExp.
|
|
196
|
+
*/
|
|
197
|
+
function escapeRegExp(s) {
|
|
198
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function crossCheckRoles(content, agentRoles, fileName) {
|
|
202
|
+
const errors = [];
|
|
203
|
+
for (const role of agentRoles) {
|
|
204
|
+
if (role === 'orchestrator') continue; // orchestrator = host itself; no agent file needed
|
|
205
|
+
const agentName = ROLE_TO_AGENT[role];
|
|
206
|
+
if (!agentName) {
|
|
207
|
+
errors.push(
|
|
208
|
+
fileName + ': declared agent-role "' + role + '" has no entry in ROLE_TO_AGENT mapping',
|
|
209
|
+
);
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
// FIX 3: Use word-boundary match so "gsd-plan-checker-v2" does NOT satisfy a required
|
|
213
|
+
// "gsd-plan-checker". Treat '-' as part of the token: boundary = start/end of string or
|
|
214
|
+
// a character that is neither \w nor '-'.
|
|
215
|
+
// Note: this is a presence check (any reference in the file), not a spawn-site check —
|
|
216
|
+
// a known limitation; spawn-site checks would require AST-level analysis.
|
|
217
|
+
const agentRe = new RegExp(
|
|
218
|
+
'(^|[^\\w-])' + escapeRegExp(agentName) + '($|[^\\w-])',
|
|
219
|
+
);
|
|
220
|
+
if (!agentRe.test(content)) {
|
|
221
|
+
errors.push(
|
|
222
|
+
fileName + ': declared agent-role "' + role + '" maps to agent "' + agentName +
|
|
223
|
+
'" but "' + agentName + '" is not referenced anywhere in the workflow file',
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return errors;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// ─── 12-points coverage assertion ────────────────────────────────────────────
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Assert that the union of all points across all contract entries equals
|
|
234
|
+
* exactly the 12 canonical points (no more, no fewer), AND that each step
|
|
235
|
+
* declares exactly its own canonical points (FIX 1: per-step ownership).
|
|
236
|
+
*
|
|
237
|
+
* @param {{ step: string, points: string[] }[]} entries
|
|
238
|
+
* @returns {string[]} Error strings; empty = OK
|
|
239
|
+
*/
|
|
240
|
+
function assertPointsCoverage(entries) {
|
|
241
|
+
const errors = [];
|
|
242
|
+
|
|
243
|
+
// FIX 1: Per-step ownership check — each step must declare exactly its own canonical points.
|
|
244
|
+
for (const entry of entries) {
|
|
245
|
+
const expected = EXPECTED_POINTS_BY_STEP[entry.step];
|
|
246
|
+
if (!expected) continue; // unknown step — caught elsewhere
|
|
247
|
+
const expectedSet = new Set(expected);
|
|
248
|
+
const actualSet = new Set(entry.points);
|
|
249
|
+
let mismatch = false;
|
|
250
|
+
for (const p of expectedSet) {
|
|
251
|
+
if (!actualSet.has(p)) mismatch = true;
|
|
252
|
+
}
|
|
253
|
+
for (const p of actualSet) {
|
|
254
|
+
if (!expectedSet.has(p)) mismatch = true;
|
|
255
|
+
}
|
|
256
|
+
if (mismatch) {
|
|
257
|
+
errors.push(
|
|
258
|
+
'step "' + entry.step + '" declares points [' + entry.points.join(', ') +
|
|
259
|
+
'] but expected [' + expected.join(', ') + ']',
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Global union + duplicate check (belt and suspenders alongside per-step check).
|
|
265
|
+
const allPoints = new Set();
|
|
266
|
+
for (const entry of entries) {
|
|
267
|
+
for (const p of entry.points) {
|
|
268
|
+
if (allPoints.has(p)) {
|
|
269
|
+
errors.push('point "' + p + '" declared more than once across all step workflows');
|
|
270
|
+
}
|
|
271
|
+
allPoints.add(p);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const canonical = new Set(CANONICAL_POINTS);
|
|
276
|
+
for (const p of allPoints) {
|
|
277
|
+
if (!canonical.has(p)) {
|
|
278
|
+
errors.push('declared point "' + p + '" is not in the canonical 12-point set');
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
for (const p of canonical) {
|
|
282
|
+
if (!allPoints.has(p)) {
|
|
283
|
+
errors.push('canonical point "' + p + '" is not declared in any step workflow');
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
return errors;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// ─── Contract builder ─────────────────────────────────────────────────────────
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Read and parse all five step workflows. Returns the contract array.
|
|
293
|
+
* Throws on any parse or cross-check error.
|
|
294
|
+
*
|
|
295
|
+
* @param {string} [workflowsDir] Override for testing
|
|
296
|
+
* @returns {{ step: string, points: string[], agentRoles: string[], coreArtifacts: { produces: string[], consumes: string[] } }[]}
|
|
297
|
+
*/
|
|
298
|
+
function buildContract(workflowsDir) {
|
|
299
|
+
const resolvedDir = workflowsDir !== undefined ? workflowsDir : WORKFLOWS_DIR;
|
|
300
|
+
const contract = [];
|
|
301
|
+
const allErrors = [];
|
|
302
|
+
|
|
303
|
+
for (const { file, step } of STEP_WORKFLOWS) {
|
|
304
|
+
const filePath = path.join(resolvedDir, file);
|
|
305
|
+
let content;
|
|
306
|
+
try {
|
|
307
|
+
content = fs.readFileSync(filePath, 'utf8');
|
|
308
|
+
} catch (err) {
|
|
309
|
+
allErrors.push('Could not read ' + file + ': ' + String(err.message));
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
let entry;
|
|
314
|
+
try {
|
|
315
|
+
entry = parseLoopHostBlock(content, file);
|
|
316
|
+
} catch (err) {
|
|
317
|
+
allErrors.push(String(err.message));
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Validate the declared step matches the expected step for this file
|
|
322
|
+
if (entry.step !== step) {
|
|
323
|
+
allErrors.push(
|
|
324
|
+
file + ': gsd:loop-host block declares step "' + entry.step +
|
|
325
|
+
'" but expected "' + step + '"',
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Cross-check roles
|
|
330
|
+
const roleErrors = crossCheckRoles(content, entry.agentRoles, file);
|
|
331
|
+
allErrors.push(...roleErrors);
|
|
332
|
+
|
|
333
|
+
contract.push(entry);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
if (allErrors.length > 0) {
|
|
337
|
+
throw new Error('Loop host contract generation failed:\n' + allErrors.map((e) => ' ' + e).join('\n'));
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Assert 12-points coverage
|
|
341
|
+
const pointErrors = assertPointsCoverage(contract);
|
|
342
|
+
if (pointErrors.length > 0) {
|
|
343
|
+
throw new Error('Loop host contract points coverage failed:\n' + pointErrors.map((e) => ' ' + e).join('\n'));
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
return contract;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// ─── Serialization ────────────────────────────────────────────────────────────
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Serialize the contract array to a CommonJS module string.
|
|
353
|
+
*
|
|
354
|
+
* @param {object[]} contract
|
|
355
|
+
* @returns {string}
|
|
356
|
+
*/
|
|
357
|
+
function serializeContract(contract) {
|
|
358
|
+
const lines = [];
|
|
359
|
+
|
|
360
|
+
lines.push("'use strict';");
|
|
361
|
+
lines.push('');
|
|
362
|
+
lines.push('/**');
|
|
363
|
+
lines.push(' * loop-host-contract.cjs — generated by scripts/gen-loop-host-contract.cjs');
|
|
364
|
+
lines.push(' * DO NOT EDIT BY HAND. Run: node scripts/gen-loop-host-contract.cjs --write');
|
|
365
|
+
lines.push(' * ADR-894 §3 — Loop Host Contract, generated from workflow markers.');
|
|
366
|
+
lines.push(' * 12 points: discuss:pre/post, plan:pre/post, execute:pre/wave:pre/wave:post/post,');
|
|
367
|
+
lines.push(' * verify:pre/post, ship:pre/post. Per-step agentRoles and coreArtifacts.');
|
|
368
|
+
lines.push(' */');
|
|
369
|
+
lines.push('');
|
|
370
|
+
lines.push('const LOOP_HOST_CONTRACT = ' + JSON.stringify(contract, null, 2) + ';');
|
|
371
|
+
lines.push('');
|
|
372
|
+
lines.push('module.exports = { LOOP_HOST_CONTRACT };');
|
|
373
|
+
lines.push('');
|
|
374
|
+
|
|
375
|
+
return lines.join('\n');
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// ─── --check diff helper ──────────────────────────────────────────────────────
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Normalize line endings to LF for CRLF-agnostic comparison.
|
|
382
|
+
* FIX 4: The serializer has no nondeterministic content (no timestamp), so
|
|
383
|
+
* the generated-by-line stripping that was here has been removed — full content
|
|
384
|
+
* comparison is now used so header drift is caught by --check.
|
|
385
|
+
*
|
|
386
|
+
* @param {string} content
|
|
387
|
+
* @returns {string}
|
|
388
|
+
*/
|
|
389
|
+
function normalizeLineEndings(content) {
|
|
390
|
+
return content.replace(/\r/g, '');
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// ─── Main ─────────────────────────────────────────────────────────────────────
|
|
394
|
+
|
|
395
|
+
function main() {
|
|
396
|
+
const flag = process.argv[2];
|
|
397
|
+
|
|
398
|
+
if (flag === '--check') {
|
|
399
|
+
let contract;
|
|
400
|
+
try {
|
|
401
|
+
contract = buildContract();
|
|
402
|
+
} catch (err) {
|
|
403
|
+
process.stderr.write(String(err.message) + '\n');
|
|
404
|
+
throw new ExitError(1, 'loop-host contract generation failed');
|
|
405
|
+
}
|
|
406
|
+
const live = serializeContract(contract);
|
|
407
|
+
|
|
408
|
+
if (!fs.existsSync(CONTRACT_PATH)) {
|
|
409
|
+
process.stderr.write(
|
|
410
|
+
'gsd-core/bin/lib/loop-host-contract.cjs does not exist. Run:\n' +
|
|
411
|
+
' node scripts/gen-loop-host-contract.cjs --write\n',
|
|
412
|
+
);
|
|
413
|
+
throw new ExitError(1);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const committed = fs.readFileSync(CONTRACT_PATH, 'utf8');
|
|
417
|
+
// FIX 4: Compare full content (no generated-by stripping) so header drift is caught.
|
|
418
|
+
if (normalizeLineEndings(committed) !== normalizeLineEndings(live)) {
|
|
419
|
+
process.stderr.write(
|
|
420
|
+
'gsd-core/bin/lib/loop-host-contract.cjs is stale. Run:\n' +
|
|
421
|
+
' node scripts/gen-loop-host-contract.cjs --write\n',
|
|
422
|
+
);
|
|
423
|
+
throw new ExitError(1);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
process.stdout.write('gsd-core/bin/lib/loop-host-contract.cjs is up to date.\n');
|
|
427
|
+
} else if (flag === '--write') {
|
|
428
|
+
let contract;
|
|
429
|
+
try {
|
|
430
|
+
contract = buildContract();
|
|
431
|
+
} catch (err) {
|
|
432
|
+
process.stderr.write(String(err.message) + '\n');
|
|
433
|
+
throw new ExitError(1, 'loop-host contract generation failed — file not written');
|
|
434
|
+
}
|
|
435
|
+
const content = serializeContract(contract);
|
|
436
|
+
fs.mkdirSync(path.dirname(CONTRACT_PATH), { recursive: true });
|
|
437
|
+
fs.writeFileSync(CONTRACT_PATH, content, 'utf8');
|
|
438
|
+
process.stdout.write('Wrote ' + CONTRACT_PATH + '\n');
|
|
439
|
+
} else {
|
|
440
|
+
// Default: print to stdout
|
|
441
|
+
let contract;
|
|
442
|
+
try {
|
|
443
|
+
contract = buildContract();
|
|
444
|
+
} catch (err) {
|
|
445
|
+
process.stderr.write(String(err.message) + '\n');
|
|
446
|
+
throw new ExitError(1, 'loop-host contract generation failed');
|
|
447
|
+
}
|
|
448
|
+
process.stdout.write(serializeContract(contract) + '\n');
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// ─── Exports (for tests) ─────────────────────────────────────────────────────
|
|
453
|
+
|
|
454
|
+
module.exports = {
|
|
455
|
+
parseLoopHostBlock,
|
|
456
|
+
crossCheckRoles,
|
|
457
|
+
assertPointsCoverage,
|
|
458
|
+
buildContract,
|
|
459
|
+
serializeContract,
|
|
460
|
+
normalizeLineEndings,
|
|
461
|
+
STEP_WORKFLOWS,
|
|
462
|
+
CANONICAL_POINTS,
|
|
463
|
+
EXPECTED_POINTS_BY_STEP,
|
|
464
|
+
ROLE_TO_AGENT,
|
|
465
|
+
};
|
|
466
|
+
|
|
467
|
+
// ─── CLI entry point ──────────────────────────────────────────────────────────
|
|
468
|
+
|
|
469
|
+
if (require.main === module) {
|
|
470
|
+
runMain(main);
|
|
471
|
+
}
|