forgedock 1.1.7 → 1.1.9
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/README.md +10 -33
- package/bin/cinema.mjs +23 -5
- package/bin/engine/invariants.mjs +361 -0
- package/bin/engine-cli.mjs +137 -11
- package/bin/env-detect.mjs +249 -0
- package/bin/forge-utils.mjs +364 -4
- package/bin/forgedock.mjs +1064 -135
- package/bin/hooks/pre-tool-use.mjs +182 -0
- package/bin/hooks/session-start.mjs +68 -3
- package/bin/init-detect.mjs +101 -12
- package/bin/journey.mjs +917 -16
- package/bin/labels.json +10 -0
- package/bin/recall.mjs +557 -0
- package/bin/registry.mjs +91 -3
- package/bin/runner.mjs +38 -2
- package/bin/tui.mjs +49 -3
- package/commands/adopt.md +1 -1
- package/commands/analytics.md +8 -8
- package/commands/audit.md +1 -1
- package/commands/autopilot.md +958 -77
- package/commands/ci-audit.md +1 -1
- package/commands/cleanup.md +139 -55
- package/commands/compat-audit.md +1 -1
- package/commands/deploy-info.md +1 -1
- package/commands/deploy-pr.md +1 -1
- package/commands/diagnose.md +1 -1
- package/commands/explain.md +1 -1
- package/commands/fix-ci.md +1 -1
- package/commands/forgedock-init.md +1 -1
- package/commands/incident-response.md +3 -3
- package/commands/issue.md +1 -1
- package/commands/optimize.md +586 -1
- package/commands/orchestrate/config.md +13 -3
- package/commands/orchestrate/phase-1-resolve.md +36 -26
- package/commands/orchestrate/phase-2-triage.md +1 -1
- package/commands/orchestrate/phase-2.5-synthesis.md +1 -1
- package/commands/orchestrate/phase-3-dependency.md +587 -48
- package/commands/orchestrate/phase-4-execution.md +1031 -57
- package/commands/orchestrate/phase-5-cleanup.md +1 -1
- package/commands/orchestrate/phase-6-report.md +248 -11
- package/commands/orchestrate/safety.md +2 -2
- package/commands/pipeline-health/phase-2-metrics.md +305 -1
- package/commands/pipeline-health/phase-4-proposals.md +127 -0
- package/commands/qa-sweep.md +1 -1
- package/commands/quality-gate.md +418 -1
- package/commands/recover-orphans.md +5 -5
- package/commands/replay.md +1 -1
- package/commands/review-pr-agents/api.md +2 -2
- package/commands/review-pr-agents/auth.md +2 -2
- package/commands/review-pr-agents/billing.md +2 -2
- package/commands/review-pr-agents/concurrency.md +2 -2
- package/commands/review-pr-agents/database.md +2 -2
- package/commands/review-pr-agents/frontend.md +2 -2
- package/commands/review-pr-agents/infra.md +2 -2
- package/commands/review-pr-agents/protocols.md +1 -1
- package/commands/review-pr-agents/scraper.md +2 -2
- package/commands/review-pr-agents/security.md +2 -2
- package/commands/review-pr-staging.md +41 -3
- package/commands/review-pr.md +462 -19
- package/commands/rollback.md +1 -1
- package/commands/scope.md +1 -1
- package/commands/security-audit.md +1 -1
- package/commands/spec-doctor.md +429 -0
- package/commands/test-gate.md +1 -1
- package/commands/validate.md +53 -1
- package/commands/work-on/build/architect.md +78 -1
- package/commands/work-on/build/context.md +360 -22
- package/commands/work-on/build/implement.md +3 -1
- package/commands/work-on/build/validate.md +29 -1
- package/commands/work-on/build.md +75 -6
- package/commands/work-on/close.md +624 -10
- package/commands/work-on/decompose.md +1 -1
- package/commands/work-on/investigate.md +235 -5
- package/commands/work-on/remediate.md +331 -0
- package/commands/work-on/review.md +1 -1
- package/commands/work-on-monolithic.md +1 -1
- package/commands/work-on.md +205 -66
- package/docs/CI.md +2 -2
- package/docs/CODEX.md +1 -1
- package/docs/CONFIG.md +186 -0
- package/docs/FORGE-PROTOCOL.md +101 -0
- package/docs/site/command-reference.md +31 -1
- package/docs/site/getting-started.md +9 -34
- package/docs/site/index.md +1 -1
- package/docs/spec/forge-protocol-v1.md +97 -5
- package/docs/spec/label-state-machine.md +6 -1
- package/package.json +3 -2
- package/packages/protocol/src/cli.js +374 -84
- package/packages/protocol/src/types.js +75 -1
- package/scripts/build-knowledge-index.mjs +1761 -0
- package/scripts/calibration.mjs +996 -0
- package/scripts/check-command-docs-drift.sh +4 -2
- package/scripts/danger-zones.mjs +789 -0
- package/scripts/doctor-pipeline-state.sh +88 -2
- package/scripts/select-fix-targets.sh +234 -0
- package/scripts/transition-label.sh +245 -6
- package/scripts/refresh-bot-token.sh +0 -97
|
@@ -1,135 +1,425 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* forge-protocol-conformance
|
|
3
|
+
* forge-protocol-conformance / forge-protocol CLI
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
* forge-protocol-conformance [fixtures-dir]
|
|
7
|
-
* node src/cli.js [fixtures-dir]
|
|
5
|
+
* Subcommands:
|
|
8
6
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* "valid": true,
|
|
17
|
-
* "sentinelState": "complete",
|
|
18
|
-
* "inlineValue": null,
|
|
19
|
-
* "fieldChecks": { "Verdict": "CONFIRMED" } // optional
|
|
20
|
-
* }
|
|
21
|
-
* }
|
|
7
|
+
* emit <TYPE> [--field KEY=VALUE]... [--b64]
|
|
8
|
+
* Emits a well-formed FORGE annotation body to stdout.
|
|
9
|
+
* Each --field KEY=VALUE pair is passed to emit() as a field.
|
|
10
|
+
* --b64 Produces the Base64url machine-surface form:
|
|
11
|
+
* <!-- FORGE:CARD: v1 sha:<sha8hex> b64:<base64url> -->
|
|
12
|
+
* (only valid for CARD type; TYPE is ignored when --b64 is passed)
|
|
13
|
+
* Exit code: 0 on success, 1 on error.
|
|
22
14
|
*
|
|
23
|
-
*
|
|
15
|
+
* parse [--type TYPE] [--field KEY]
|
|
16
|
+
* Reads a comment body from stdin.
|
|
17
|
+
* --type TYPE If given, filters to annotations of that type (case-insensitive).
|
|
18
|
+
* If absent, operates on the first annotation found.
|
|
19
|
+
* --field KEY If given, prints only the named field value (or inlineValue for
|
|
20
|
+
* inline-value types). Exits 1 if the field is missing.
|
|
21
|
+
* If absent, prints the full annotation as JSON.
|
|
22
|
+
* Exit code: 0 on success, 1 if no matching annotation found or field missing.
|
|
23
|
+
*
|
|
24
|
+
* [fixtures-dir] (legacy positional — no subcommand keyword)
|
|
25
|
+
* Conformance runner: reads fixtures from the given directory (default: ./fixtures/).
|
|
26
|
+
* Exit code: 0 if all fixtures pass, 1 if any fail.
|
|
27
|
+
*
|
|
28
|
+
* Usage examples:
|
|
29
|
+
* node src/cli.js emit INVESTIGATOR \
|
|
30
|
+
* --field Verdict=CONFIRMED --field Confidence=HIGH \
|
|
31
|
+
* --field Severity=MEDIUM --field "Task Type=Bug Fix" \
|
|
32
|
+
* --field "Decomposition Assessment=NO — simple."
|
|
33
|
+
*
|
|
34
|
+
* echo '<!-- FORGE:INVESTIGATOR -->...' | \
|
|
35
|
+
* node src/cli.js parse --type INVESTIGATOR --field Verdict
|
|
36
|
+
*
|
|
37
|
+
* node src/cli.js # conformance runner (legacy)
|
|
38
|
+
* node src/cli.js fixtures/ # conformance runner with explicit dir
|
|
24
39
|
*
|
|
25
40
|
* @license MIT
|
|
26
41
|
*/
|
|
27
42
|
|
|
28
43
|
import { readFileSync, readdirSync } from 'node:fs';
|
|
29
44
|
import { resolve, extname } from 'node:path';
|
|
45
|
+
import { createHash } from 'node:crypto';
|
|
30
46
|
import { parse } from './parse.js';
|
|
31
47
|
import { validate } from './validate.js';
|
|
48
|
+
import { emit } from './emit.js';
|
|
32
49
|
|
|
33
|
-
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
// Argv routing
|
|
52
|
+
//
|
|
53
|
+
// If argv[2] is a known subcommand keyword ('emit' or 'parse'), route to the
|
|
54
|
+
// corresponding handler. Otherwise fall through to the legacy conformance
|
|
55
|
+
// runner (argv[2] is either a fixtures directory path or absent).
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
34
57
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
const
|
|
58
|
+
const SUBCOMMANDS = new Set(['emit', 'parse']);
|
|
59
|
+
const args = process.argv.slice(2);
|
|
60
|
+
const subcommand = args[0];
|
|
38
61
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
62
|
+
if (SUBCOMMANDS.has(subcommand)) {
|
|
63
|
+
if (subcommand === 'emit') {
|
|
64
|
+
cmdEmit(args.slice(1));
|
|
65
|
+
} else {
|
|
66
|
+
cmdParse(args.slice(1));
|
|
67
|
+
}
|
|
68
|
+
} else {
|
|
69
|
+
// Legacy conformance runner: forward the full argv slice so the directory
|
|
70
|
+
// positional (if any) is still at index 0 of the slice passed in.
|
|
71
|
+
cmdConformance(args);
|
|
46
72
|
}
|
|
47
73
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
// emit subcommand
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
|
|
78
|
+
function cmdEmit(argv) {
|
|
79
|
+
let type = null;
|
|
80
|
+
const fields = {};
|
|
81
|
+
let b64 = false;
|
|
82
|
+
let i = 0;
|
|
83
|
+
|
|
84
|
+
while (i < argv.length) {
|
|
85
|
+
const arg = argv[i];
|
|
86
|
+
if (arg === '--field' || arg === '-f') {
|
|
87
|
+
if (i + 1 >= argv.length) {
|
|
88
|
+
die('--field requires KEY=VALUE');
|
|
89
|
+
}
|
|
90
|
+
const pair = argv[++i];
|
|
91
|
+
const eqIdx = pair.indexOf('=');
|
|
92
|
+
if (eqIdx === -1) {
|
|
93
|
+
die(`--field value must be KEY=VALUE, got: ${JSON.stringify(pair)}`);
|
|
94
|
+
}
|
|
95
|
+
const key = pair.slice(0, eqIdx);
|
|
96
|
+
const val = pair.slice(eqIdx + 1);
|
|
97
|
+
fields[key] = val;
|
|
98
|
+
} else if (arg === '--b64') {
|
|
99
|
+
b64 = true;
|
|
100
|
+
} else if (!type && !arg.startsWith('-')) {
|
|
101
|
+
type = arg;
|
|
102
|
+
} else {
|
|
103
|
+
die(`Unknown argument: ${JSON.stringify(arg)}`);
|
|
104
|
+
}
|
|
105
|
+
i++;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (!type && !b64) {
|
|
109
|
+
die('Usage: emit <TYPE> [--field KEY=VALUE]... [--b64]');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (b64) {
|
|
113
|
+
// Base64url machine-surface form for CARD (design decision 2026-07-08).
|
|
114
|
+
// Payload: canonical JSON (sorted keys, single line, UTF-8).
|
|
115
|
+
// The Base64url alphabet [A-Za-z0-9_-] cannot contain '>', '<', or '!' —
|
|
116
|
+
// so -->, --!>, and <!-- are structurally unrepresentable by construction.
|
|
117
|
+
const payload = Object.keys(fields).length > 0 ? { ...fields } : {};
|
|
118
|
+
if (type) payload.type = type.toUpperCase();
|
|
119
|
+
const canonical = canonicalJson(payload);
|
|
120
|
+
const b64url = toBase64url(canonical);
|
|
121
|
+
const sha8 = createHash('sha256').update(canonical, 'utf8').digest('hex').slice(0, 8);
|
|
122
|
+
// The colon-separator form "<!-- FORGE:CARD: v1 sha:X b64:Y -->" matches
|
|
123
|
+
// OPENING_TAG_RE so parse() can extract the inline value "v1 sha:X b64:Y".
|
|
124
|
+
// This keeps the canonical FORGE inline-value syntax (§3.4) while adding
|
|
125
|
+
// the sha8 integrity prefix and Base64url payload from the design decision.
|
|
126
|
+
process.stdout.write(`<!-- FORGE:CARD: v1 sha:${sha8} b64:${b64url} -->\n`);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
52
129
|
|
|
53
|
-
|
|
54
|
-
const fixturePath = resolve(fixturesDir, file);
|
|
55
|
-
let fixture;
|
|
130
|
+
let output;
|
|
56
131
|
try {
|
|
57
|
-
|
|
132
|
+
output = emit(type, fields);
|
|
58
133
|
} catch (err) {
|
|
59
|
-
|
|
60
|
-
continue;
|
|
134
|
+
die(err.message);
|
|
61
135
|
}
|
|
62
136
|
|
|
63
|
-
|
|
64
|
-
|
|
137
|
+
process.stdout.write(output + '\n');
|
|
138
|
+
}
|
|
65
139
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
}
|
|
140
|
+
// ---------------------------------------------------------------------------
|
|
141
|
+
// parse subcommand
|
|
142
|
+
// ---------------------------------------------------------------------------
|
|
70
143
|
|
|
71
|
-
|
|
144
|
+
function cmdParse(argv) {
|
|
145
|
+
let filterType = null;
|
|
146
|
+
let filterField = null;
|
|
147
|
+
let i = 0;
|
|
72
148
|
|
|
73
|
-
|
|
74
|
-
|
|
149
|
+
while (i < argv.length) {
|
|
150
|
+
const arg = argv[i];
|
|
151
|
+
if (arg === '--type' || arg === '-t') {
|
|
152
|
+
if (i + 1 >= argv.length) die('--type requires a value');
|
|
153
|
+
filterType = argv[++i].toUpperCase();
|
|
154
|
+
} else if (arg === '--field' || arg === '-f') {
|
|
155
|
+
if (i + 1 >= argv.length) die('--field requires a value');
|
|
156
|
+
filterField = argv[++i];
|
|
157
|
+
} else {
|
|
158
|
+
die(`Unknown argument: ${JSON.stringify(arg)}`);
|
|
159
|
+
}
|
|
160
|
+
i++;
|
|
161
|
+
}
|
|
75
162
|
|
|
76
|
-
|
|
163
|
+
// Read stdin. Use readFileSync(0) (fd 0) instead of '/dev/stdin' — the POSIX
|
|
164
|
+
// path does not exist on Windows (ENOENT, exit 2). See fix in #1594.
|
|
165
|
+
let body;
|
|
166
|
+
try {
|
|
167
|
+
body = readFileSync(0, 'utf8');
|
|
168
|
+
} catch (err) {
|
|
169
|
+
die(`Failed to read stdin: ${err.message}`);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const annotations = parse(body);
|
|
173
|
+
if (annotations.length === 0) {
|
|
174
|
+
die('No FORGE annotations found in stdin');
|
|
175
|
+
}
|
|
77
176
|
|
|
78
|
-
|
|
79
|
-
|
|
177
|
+
let ann;
|
|
178
|
+
if (filterType) {
|
|
179
|
+
ann = annotations.find(a => a.type === filterType);
|
|
180
|
+
if (!ann) {
|
|
181
|
+
die(`No annotation of type "${filterType}" found`);
|
|
182
|
+
}
|
|
80
183
|
} else {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
184
|
+
ann = annotations[0];
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// CARD Base64url machine-surface: decode the b64 payload and return its fields.
|
|
188
|
+
if (ann.type === 'CARD' && ann.inlineValue) {
|
|
189
|
+
const decoded = decodeCardInlineValue(ann.inlineValue);
|
|
190
|
+
if (decoded !== null) {
|
|
191
|
+
if (filterField) {
|
|
192
|
+
const val = decoded[filterField];
|
|
193
|
+
if (val === undefined) {
|
|
194
|
+
die(`Field "${filterField}" not found in CARD payload`);
|
|
195
|
+
}
|
|
196
|
+
process.stdout.write(String(val) + '\n');
|
|
197
|
+
} else {
|
|
198
|
+
process.stdout.write(JSON.stringify({ type: 'CARD', payload: decoded }, null, 2) + '\n');
|
|
199
|
+
}
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
// Fall through to generic inline-value handling if the CARD format is unrecognized.
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (filterField) {
|
|
206
|
+
// For inline-value annotations, 'value' or 'inlineValue' accesses the value.
|
|
207
|
+
if (ann.inlineValue !== null) {
|
|
208
|
+
if (filterField === 'value' || filterField === 'inlineValue') {
|
|
209
|
+
process.stdout.write(ann.inlineValue + '\n');
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
die(
|
|
213
|
+
`Annotation "${ann.type}" is an inline-value type; ` +
|
|
214
|
+
`only field name "value" or "inlineValue" is available (got "${filterField}")`,
|
|
85
215
|
);
|
|
86
216
|
}
|
|
217
|
+
const val = ann.fields[filterField];
|
|
218
|
+
if (val === undefined) {
|
|
219
|
+
die(`Field "${filterField}" not found in annotation "${ann.type}"`);
|
|
220
|
+
}
|
|
221
|
+
process.stdout.write(val + '\n');
|
|
222
|
+
} else {
|
|
223
|
+
// Output full annotation as JSON.
|
|
224
|
+
const out = {
|
|
225
|
+
type: ann.type,
|
|
226
|
+
sentinelState: ann.sentinelState,
|
|
227
|
+
isReserved: ann.isReserved,
|
|
228
|
+
isControl: ann.isControl,
|
|
229
|
+
inlineValue: ann.inlineValue,
|
|
230
|
+
fields: ann.fields,
|
|
231
|
+
};
|
|
232
|
+
process.stdout.write(JSON.stringify(out, null, 2) + '\n');
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// ---------------------------------------------------------------------------
|
|
237
|
+
// Conformance runner (legacy — no subcommand keyword)
|
|
238
|
+
// ---------------------------------------------------------------------------
|
|
87
239
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
240
|
+
function cmdConformance(argv) {
|
|
241
|
+
const fixturesDir = resolve(argv[0] ?? 'fixtures');
|
|
242
|
+
|
|
243
|
+
let passed = 0;
|
|
244
|
+
let failed = 0;
|
|
245
|
+
const failures = [];
|
|
246
|
+
|
|
247
|
+
let files;
|
|
248
|
+
try {
|
|
249
|
+
files = readdirSync(fixturesDir).filter(f => extname(f) === '.json').sort();
|
|
250
|
+
} catch (err) {
|
|
251
|
+
console.error(`Cannot read fixtures directory: ${fixturesDir}`);
|
|
252
|
+
console.error(err.message);
|
|
253
|
+
process.exit(1);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (files.length === 0) {
|
|
257
|
+
console.error(`No .json fixtures found in ${fixturesDir}`);
|
|
258
|
+
process.exit(1);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
for (const file of files) {
|
|
262
|
+
const fixturePath = resolve(fixturesDir, file);
|
|
263
|
+
let fixture;
|
|
264
|
+
try {
|
|
265
|
+
fixture = JSON.parse(readFileSync(fixturePath, 'utf8'));
|
|
266
|
+
} catch (err) {
|
|
267
|
+
console.error(`SKIP ${file} — cannot parse JSON: ${err.message}`);
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const { description, input, expect: expected } = fixture;
|
|
272
|
+
const label = `${file}: ${description ?? '(no description)'}`;
|
|
273
|
+
|
|
274
|
+
if (typeof input !== 'string') {
|
|
275
|
+
console.error(`SKIP ${label} — "input" must be a string`);
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const annotations = parse(input);
|
|
280
|
+
|
|
281
|
+
// Find the annotation matching the expected type
|
|
282
|
+
const ann = annotations.find(a => a.type === expected.type);
|
|
283
|
+
|
|
284
|
+
const fixtureErrors = [];
|
|
285
|
+
|
|
286
|
+
if (!ann) {
|
|
287
|
+
fixtureErrors.push(`Expected annotation type "${expected.type}" not found in parsed output`);
|
|
288
|
+
} else {
|
|
289
|
+
// Sentinel state check
|
|
290
|
+
if (expected.sentinelState !== undefined && ann.sentinelState !== expected.sentinelState) {
|
|
91
291
|
fixtureErrors.push(
|
|
92
|
-
`
|
|
292
|
+
`sentinelState: expected "${expected.sentinelState}", got "${ann.sentinelState}"`,
|
|
93
293
|
);
|
|
94
294
|
}
|
|
95
|
-
}
|
|
96
295
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
if (ann.fields[key] !== val) {
|
|
296
|
+
// Inline value check
|
|
297
|
+
if ('inlineValue' in expected) {
|
|
298
|
+
if (ann.inlineValue !== expected.inlineValue) {
|
|
101
299
|
fixtureErrors.push(
|
|
102
|
-
`
|
|
300
|
+
`inlineValue: expected ${JSON.stringify(expected.inlineValue)}, got ${JSON.stringify(ann.inlineValue)}`,
|
|
103
301
|
);
|
|
104
302
|
}
|
|
105
303
|
}
|
|
304
|
+
|
|
305
|
+
// Field checks
|
|
306
|
+
if (expected.fieldChecks) {
|
|
307
|
+
for (const [key, val] of Object.entries(expected.fieldChecks)) {
|
|
308
|
+
if (ann.fields[key] !== val) {
|
|
309
|
+
fixtureErrors.push(
|
|
310
|
+
`field "${key}": expected "${val}", got "${ann.fields[key] ?? '(missing)'}"`,
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// validate() result check
|
|
317
|
+
const { valid, errors } = validate(ann);
|
|
318
|
+
if (expected.valid === true && !valid) {
|
|
319
|
+
fixtureErrors.push(`Expected valid=true but got errors: ${errors.join('; ')}`);
|
|
320
|
+
} else if (expected.valid === false && valid) {
|
|
321
|
+
fixtureErrors.push(`Expected valid=false but annotation passed validation`);
|
|
322
|
+
}
|
|
106
323
|
}
|
|
107
324
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
325
|
+
if (fixtureErrors.length === 0) {
|
|
326
|
+
console.log(`ok ${label}`);
|
|
327
|
+
passed++;
|
|
328
|
+
} else {
|
|
329
|
+
console.log(`FAIL ${label}`);
|
|
330
|
+
for (const e of fixtureErrors) {
|
|
331
|
+
console.log(` ${e}`);
|
|
332
|
+
}
|
|
333
|
+
failures.push(label);
|
|
334
|
+
failed++;
|
|
114
335
|
}
|
|
115
336
|
}
|
|
116
337
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
for (const e of fixtureErrors) {
|
|
123
|
-
console.log(` ${e}`);
|
|
124
|
-
}
|
|
125
|
-
failures.push(label);
|
|
126
|
-
failed++;
|
|
338
|
+
console.log('');
|
|
339
|
+
console.log(`Results: ${passed} passed, ${failed} failed (${files.length} fixtures total)`);
|
|
340
|
+
|
|
341
|
+
if (failed > 0) {
|
|
342
|
+
process.exit(1);
|
|
127
343
|
}
|
|
128
344
|
}
|
|
129
345
|
|
|
130
|
-
|
|
131
|
-
|
|
346
|
+
// ---------------------------------------------------------------------------
|
|
347
|
+
// Helpers
|
|
348
|
+
// ---------------------------------------------------------------------------
|
|
132
349
|
|
|
133
|
-
|
|
350
|
+
function die(msg) {
|
|
351
|
+
process.stderr.write(`forge-protocol-cli: ${msg}\n`);
|
|
134
352
|
process.exit(1);
|
|
135
353
|
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Produce canonical JSON: sorted keys, single line, UTF-8.
|
|
357
|
+
* This is the payload that gets Base64url-encoded in the CARD format.
|
|
358
|
+
*/
|
|
359
|
+
function canonicalJson(obj) {
|
|
360
|
+
return JSON.stringify(sortKeysDeep(obj));
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function sortKeysDeep(val) {
|
|
364
|
+
if (Array.isArray(val)) return val.map(sortKeysDeep);
|
|
365
|
+
if (val !== null && typeof val === 'object') {
|
|
366
|
+
return Object.fromEntries(
|
|
367
|
+
Object.keys(val)
|
|
368
|
+
.sort()
|
|
369
|
+
.map(k => [k, sortKeysDeep(val[k])]),
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
return val;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Encode a UTF-8 string as Base64url (URL-safe alphabet, no padding).
|
|
377
|
+
* The Base64url alphabet [A-Za-z0-9_-] cannot contain '>', '<', or '!' —
|
|
378
|
+
* so -->, --!>, and <!-- are structurally unrepresentable by construction
|
|
379
|
+
* (design decision 2026-07-08: encoding beats escaping).
|
|
380
|
+
*
|
|
381
|
+
* @param {string} str — UTF-8 string to encode
|
|
382
|
+
* @returns {string} Base64url-encoded string without padding
|
|
383
|
+
*/
|
|
384
|
+
function toBase64url(str) {
|
|
385
|
+
return Buffer.from(str, 'utf8')
|
|
386
|
+
.toString('base64')
|
|
387
|
+
.replace(/\+/g, '-')
|
|
388
|
+
.replace(/\//g, '_')
|
|
389
|
+
.replace(/=/g, '');
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Decode a CARD inline value of the form "v1 sha:<sha8hex> b64:<base64url>"
|
|
394
|
+
* and return the parsed JSON object, or null if the format is unrecognized.
|
|
395
|
+
* Verifies the sha8 integrity prefix; returns null on corruption or tampering.
|
|
396
|
+
*
|
|
397
|
+
* @param {string} inlineValue — the parsed inlineValue from a CARD annotation
|
|
398
|
+
* @returns {object|null}
|
|
399
|
+
*/
|
|
400
|
+
function decodeCardInlineValue(inlineValue) {
|
|
401
|
+
const m = inlineValue.match(/^v1\s+sha:([0-9a-f]{8})\s+b64:([A-Za-z0-9_-]+)$/);
|
|
402
|
+
if (!m) return null;
|
|
403
|
+
const [, sha8, b64url] = m;
|
|
404
|
+
|
|
405
|
+
// Restore standard Base64 padding and swap the URL-safe alphabet back.
|
|
406
|
+
const b64std = b64url.replace(/-/g, '+').replace(/_/g, '/');
|
|
407
|
+
const padded = b64std + '='.repeat((4 - (b64std.length % 4)) % 4);
|
|
408
|
+
|
|
409
|
+
let canonical;
|
|
410
|
+
try {
|
|
411
|
+
canonical = Buffer.from(padded, 'base64').toString('utf8');
|
|
412
|
+
} catch {
|
|
413
|
+
return null;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// Integrity check: first 8 hex chars of SHA-256 must match the prefix.
|
|
417
|
+
const expectedSha8 = createHash('sha256').update(canonical, 'utf8').digest('hex').slice(0, 8);
|
|
418
|
+
if (expectedSha8 !== sha8) return null;
|
|
419
|
+
|
|
420
|
+
try {
|
|
421
|
+
return JSON.parse(canonical);
|
|
422
|
+
} catch {
|
|
423
|
+
return null;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
@@ -18,8 +18,10 @@ export const SentinelState = {
|
|
|
18
18
|
};
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
|
-
* All
|
|
21
|
+
* All 16 reserved annotation types from spec §4.
|
|
22
22
|
* Spec ref: §4.1 lifecycle, §4.2 cross-artifact, §4.3 control/error markers.
|
|
23
|
+
* Added CLAIM and CLAIM_RELEASED in forge#1736 — claims board for claim-level parallelism.
|
|
24
|
+
* Added AUTOPILOT_CYCLE in forge#1753 — durable cycle state for /autopilot.
|
|
23
25
|
*/
|
|
24
26
|
export const RESERVED_TYPES = {
|
|
25
27
|
// §4.1 Lifecycle annotations
|
|
@@ -92,6 +94,48 @@ export const RESERVED_TYPES = {
|
|
|
92
94
|
inlineValue: false,
|
|
93
95
|
requiredFields: [],
|
|
94
96
|
},
|
|
97
|
+
/**
|
|
98
|
+
* AUTOPILOT_CYCLE — durable record of one /autopilot execution cycle.
|
|
99
|
+
* Posted as a comment on the designated ops issue (label: autopilot-ops) at
|
|
100
|
+
* the end of every cycle so that cycle N+1 can read baseline metrics, compute
|
|
101
|
+
* deltas, and resume an interrupted cycle without re-executing committed phases.
|
|
102
|
+
*
|
|
103
|
+
* Required fields:
|
|
104
|
+
* cycle_id — unique cycle identifier (e.g. "20260708-1" — date + counter)
|
|
105
|
+
* timestamp — ISO-8601 UTC timestamp of cycle start
|
|
106
|
+
* baseline — snapshot of key metrics at cycle start (JSON-encoded string)
|
|
107
|
+
* phase_markers — comma-separated list of completed phase names for resume
|
|
108
|
+
*
|
|
109
|
+
* Completion sentinel: <!-- FORGE:AUTOPILOT_CYCLE:COMPLETE -->
|
|
110
|
+
* <!-- Added: forge#1753 — durable cycle state + baseline deltas + resume -->
|
|
111
|
+
*/
|
|
112
|
+
AUTOPILOT_CYCLE: {
|
|
113
|
+
type: 'AUTOPILOT_CYCLE',
|
|
114
|
+
category: Category.LIFECYCLE,
|
|
115
|
+
completionSentinel: 'FORGE:AUTOPILOT_CYCLE:COMPLETE',
|
|
116
|
+
partialSentinel: null,
|
|
117
|
+
inlineValue: false,
|
|
118
|
+
requiredFields: ['cycle_id', 'timestamp', 'baseline', 'phase_markers'],
|
|
119
|
+
},
|
|
120
|
+
/**
|
|
121
|
+
* CLAIM — an agent's active resource reservation, posted on the coordination issue
|
|
122
|
+
* (claims board) when the agent begins implementation under an orchestration batch.
|
|
123
|
+
* Required fields:
|
|
124
|
+
* Holder — the issue/run reference holding this claim (e.g. "#1736 / run-abc")
|
|
125
|
+
* Files — newline-separated list of claimed file paths
|
|
126
|
+
* Interfaces — preserved interface contracts (function signatures, API shapes)
|
|
127
|
+
* TTL — auto-expire condition ("terminal state of Holder issue")
|
|
128
|
+
* Released by posting a CLAIM_RELEASED control marker referencing the same Holder.
|
|
129
|
+
* <!-- Added: forge#1736 — claims board for claim-level parallelism -->
|
|
130
|
+
*/
|
|
131
|
+
CLAIM: {
|
|
132
|
+
type: 'CLAIM',
|
|
133
|
+
category: Category.LIFECYCLE,
|
|
134
|
+
completionSentinel: 'CLAIM:COMPLETE',
|
|
135
|
+
partialSentinel: null,
|
|
136
|
+
inlineValue: false,
|
|
137
|
+
requiredFields: ['Holder', 'Files', 'Interfaces', 'TTL'],
|
|
138
|
+
},
|
|
95
139
|
|
|
96
140
|
// §4.2 Cross-artifact annotations
|
|
97
141
|
KNOWLEDGE_GIST: {
|
|
@@ -165,6 +209,36 @@ export const RESERVED_TYPES = {
|
|
|
165
209
|
controlMarker: true,
|
|
166
210
|
requiredFields: [],
|
|
167
211
|
},
|
|
212
|
+
/**
|
|
213
|
+
* CLAIM_RELEASED — posted on the coordination issue when the Holder's issue reaches
|
|
214
|
+
* a terminal state (workflow:merged, workflow:invalid, needs-human). Signals that all
|
|
215
|
+
* files and interfaces declared in the preceding CLAIM annotation are no longer held.
|
|
216
|
+
* Consumers MUST treat any CLAIM without a subsequent CLAIM_RELEASED from the same
|
|
217
|
+
* Holder as expired when the Holder issue reaches a terminal state.
|
|
218
|
+
* <!-- Added: forge#1736 — claims board for claim-level parallelism -->
|
|
219
|
+
*/
|
|
220
|
+
CLAIM_RELEASED: {
|
|
221
|
+
type: 'CLAIM_RELEASED',
|
|
222
|
+
category: Category.CONTROL,
|
|
223
|
+
completionSentinel: null,
|
|
224
|
+
partialSentinel: null,
|
|
225
|
+
inlineValue: false,
|
|
226
|
+
controlMarker: true,
|
|
227
|
+
requiredFields: [],
|
|
228
|
+
},
|
|
229
|
+
|
|
230
|
+
// §4.2 Machine-surface cross-artifact annotation (Base64url encoded — design decision 2026-07-08)
|
|
231
|
+
// Format: <!-- FORGE:CARD: v1 sha:{sha8hex} b64:{base64url_of_canonical_json} -->
|
|
232
|
+
// The inline value carries the full encoding: "v1 sha:XXXXXXXX b64:YYYYYY..."
|
|
233
|
+
// This is an inline-value type — parse() extracts the full encoding as inlineValue.
|
|
234
|
+
CARD: {
|
|
235
|
+
type: 'CARD',
|
|
236
|
+
category: Category.CROSS_ARTIFACT,
|
|
237
|
+
completionSentinel: null,
|
|
238
|
+
partialSentinel: null,
|
|
239
|
+
inlineValue: true,
|
|
240
|
+
requiredFields: [],
|
|
241
|
+
},
|
|
168
242
|
};
|
|
169
243
|
|
|
170
244
|
/** Set of all reserved type strings for fast lookup */
|