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.
Files changed (97) hide show
  1. package/README.md +10 -33
  2. package/bin/cinema.mjs +23 -5
  3. package/bin/engine/invariants.mjs +361 -0
  4. package/bin/engine-cli.mjs +137 -11
  5. package/bin/env-detect.mjs +249 -0
  6. package/bin/forge-utils.mjs +364 -4
  7. package/bin/forgedock.mjs +1064 -135
  8. package/bin/hooks/pre-tool-use.mjs +182 -0
  9. package/bin/hooks/session-start.mjs +68 -3
  10. package/bin/init-detect.mjs +101 -12
  11. package/bin/journey.mjs +917 -16
  12. package/bin/labels.json +10 -0
  13. package/bin/recall.mjs +557 -0
  14. package/bin/registry.mjs +91 -3
  15. package/bin/runner.mjs +38 -2
  16. package/bin/tui.mjs +49 -3
  17. package/commands/adopt.md +1 -1
  18. package/commands/analytics.md +8 -8
  19. package/commands/audit.md +1 -1
  20. package/commands/autopilot.md +958 -77
  21. package/commands/ci-audit.md +1 -1
  22. package/commands/cleanup.md +139 -55
  23. package/commands/compat-audit.md +1 -1
  24. package/commands/deploy-info.md +1 -1
  25. package/commands/deploy-pr.md +1 -1
  26. package/commands/diagnose.md +1 -1
  27. package/commands/explain.md +1 -1
  28. package/commands/fix-ci.md +1 -1
  29. package/commands/forgedock-init.md +1 -1
  30. package/commands/incident-response.md +3 -3
  31. package/commands/issue.md +1 -1
  32. package/commands/optimize.md +586 -1
  33. package/commands/orchestrate/config.md +13 -3
  34. package/commands/orchestrate/phase-1-resolve.md +36 -26
  35. package/commands/orchestrate/phase-2-triage.md +1 -1
  36. package/commands/orchestrate/phase-2.5-synthesis.md +1 -1
  37. package/commands/orchestrate/phase-3-dependency.md +587 -48
  38. package/commands/orchestrate/phase-4-execution.md +1031 -57
  39. package/commands/orchestrate/phase-5-cleanup.md +1 -1
  40. package/commands/orchestrate/phase-6-report.md +248 -11
  41. package/commands/orchestrate/safety.md +2 -2
  42. package/commands/pipeline-health/phase-2-metrics.md +305 -1
  43. package/commands/pipeline-health/phase-4-proposals.md +127 -0
  44. package/commands/qa-sweep.md +1 -1
  45. package/commands/quality-gate.md +418 -1
  46. package/commands/recover-orphans.md +5 -5
  47. package/commands/replay.md +1 -1
  48. package/commands/review-pr-agents/api.md +2 -2
  49. package/commands/review-pr-agents/auth.md +2 -2
  50. package/commands/review-pr-agents/billing.md +2 -2
  51. package/commands/review-pr-agents/concurrency.md +2 -2
  52. package/commands/review-pr-agents/database.md +2 -2
  53. package/commands/review-pr-agents/frontend.md +2 -2
  54. package/commands/review-pr-agents/infra.md +2 -2
  55. package/commands/review-pr-agents/protocols.md +1 -1
  56. package/commands/review-pr-agents/scraper.md +2 -2
  57. package/commands/review-pr-agents/security.md +2 -2
  58. package/commands/review-pr-staging.md +41 -3
  59. package/commands/review-pr.md +462 -19
  60. package/commands/rollback.md +1 -1
  61. package/commands/scope.md +1 -1
  62. package/commands/security-audit.md +1 -1
  63. package/commands/spec-doctor.md +429 -0
  64. package/commands/test-gate.md +1 -1
  65. package/commands/validate.md +53 -1
  66. package/commands/work-on/build/architect.md +78 -1
  67. package/commands/work-on/build/context.md +360 -22
  68. package/commands/work-on/build/implement.md +3 -1
  69. package/commands/work-on/build/validate.md +29 -1
  70. package/commands/work-on/build.md +75 -6
  71. package/commands/work-on/close.md +624 -10
  72. package/commands/work-on/decompose.md +1 -1
  73. package/commands/work-on/investigate.md +235 -5
  74. package/commands/work-on/remediate.md +331 -0
  75. package/commands/work-on/review.md +1 -1
  76. package/commands/work-on-monolithic.md +1 -1
  77. package/commands/work-on.md +205 -66
  78. package/docs/CI.md +2 -2
  79. package/docs/CODEX.md +1 -1
  80. package/docs/CONFIG.md +186 -0
  81. package/docs/FORGE-PROTOCOL.md +101 -0
  82. package/docs/site/command-reference.md +31 -1
  83. package/docs/site/getting-started.md +9 -34
  84. package/docs/site/index.md +1 -1
  85. package/docs/spec/forge-protocol-v1.md +97 -5
  86. package/docs/spec/label-state-machine.md +6 -1
  87. package/package.json +3 -2
  88. package/packages/protocol/src/cli.js +374 -84
  89. package/packages/protocol/src/types.js +75 -1
  90. package/scripts/build-knowledge-index.mjs +1761 -0
  91. package/scripts/calibration.mjs +996 -0
  92. package/scripts/check-command-docs-drift.sh +4 -2
  93. package/scripts/danger-zones.mjs +789 -0
  94. package/scripts/doctor-pipeline-state.sh +88 -2
  95. package/scripts/select-fix-targets.sh +234 -0
  96. package/scripts/transition-label.sh +245 -6
  97. package/scripts/refresh-bot-token.sh +0 -97
@@ -1,135 +1,425 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * forge-protocol-conformance — FORGE Annotation Protocol conformance test runner.
3
+ * forge-protocol-conformance / forge-protocol CLI
4
4
  *
5
- * Usage:
6
- * forge-protocol-conformance [fixtures-dir]
7
- * node src/cli.js [fixtures-dir]
5
+ * Subcommands:
8
6
  *
9
- * Reads fixtures from the given directory (default: ./fixtures/).
10
- * Each fixture is a JSON file with:
11
- * {
12
- * "description": "human-readable test name",
13
- * "input": "comment body string to parse",
14
- * "expect": {
15
- * "type": "INVESTIGATOR",
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
- * Exit code: 0 if all fixtures pass, 1 if any fail.
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
- const fixturesDir = resolve(process.argv[2] ?? 'fixtures');
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
- let passed = 0;
36
- let failed = 0;
37
- const failures = [];
58
+ const SUBCOMMANDS = new Set(['emit', 'parse']);
59
+ const args = process.argv.slice(2);
60
+ const subcommand = args[0];
38
61
 
39
- let files;
40
- try {
41
- files = readdirSync(fixturesDir).filter(f => extname(f) === '.json').sort();
42
- } catch (err) {
43
- console.error(`Cannot read fixtures directory: ${fixturesDir}`);
44
- console.error(err.message);
45
- process.exit(1);
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
- if (files.length === 0) {
49
- console.error(`No .json fixtures found in ${fixturesDir}`);
50
- process.exit(1);
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
- for (const file of files) {
54
- const fixturePath = resolve(fixturesDir, file);
55
- let fixture;
130
+ let output;
56
131
  try {
57
- fixture = JSON.parse(readFileSync(fixturePath, 'utf8'));
132
+ output = emit(type, fields);
58
133
  } catch (err) {
59
- console.error(`SKIP ${file} — cannot parse JSON: ${err.message}`);
60
- continue;
134
+ die(err.message);
61
135
  }
62
136
 
63
- const { description, input, expect: expected } = fixture;
64
- const label = `${file}: ${description ?? '(no description)'}`;
137
+ process.stdout.write(output + '\n');
138
+ }
65
139
 
66
- if (typeof input !== 'string') {
67
- console.error(`SKIP ${label} — "input" must be a string`);
68
- continue;
69
- }
140
+ // ---------------------------------------------------------------------------
141
+ // parse subcommand
142
+ // ---------------------------------------------------------------------------
70
143
 
71
- const annotations = parse(input);
144
+ function cmdParse(argv) {
145
+ let filterType = null;
146
+ let filterField = null;
147
+ let i = 0;
72
148
 
73
- // Find the annotation matching the expected type
74
- const ann = annotations.find(a => a.type === expected.type);
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
- const fixtureErrors = [];
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
- if (!ann) {
79
- fixtureErrors.push(`Expected annotation type "${expected.type}" not found in parsed output`);
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
- // Sentinel state check
82
- if (expected.sentinelState !== undefined && ann.sentinelState !== expected.sentinelState) {
83
- fixtureErrors.push(
84
- `sentinelState: expected "${expected.sentinelState}", got "${ann.sentinelState}"`,
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
- // Inline value check
89
- if ('inlineValue' in expected) {
90
- if (ann.inlineValue !== expected.inlineValue) {
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
- `inlineValue: expected ${JSON.stringify(expected.inlineValue)}, got ${JSON.stringify(ann.inlineValue)}`,
292
+ `sentinelState: expected "${expected.sentinelState}", got "${ann.sentinelState}"`,
93
293
  );
94
294
  }
95
- }
96
295
 
97
- // Field checks
98
- if (expected.fieldChecks) {
99
- for (const [key, val] of Object.entries(expected.fieldChecks)) {
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
- `field "${key}": expected "${val}", got "${ann.fields[key] ?? '(missing)'}"`,
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
- // validate() result check
109
- const { valid, errors } = validate(ann);
110
- if (expected.valid === true && !valid) {
111
- fixtureErrors.push(`Expected valid=true but got errors: ${errors.join('; ')}`);
112
- } else if (expected.valid === false && valid) {
113
- fixtureErrors.push(`Expected valid=false but annotation passed validation`);
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
- if (fixtureErrors.length === 0) {
118
- console.log(`ok ${label}`);
119
- passed++;
120
- } else {
121
- console.log(`FAIL ${label}`);
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
- console.log('');
131
- console.log(`Results: ${passed} passed, ${failed} failed (${files.length} fixtures total)`);
346
+ // ---------------------------------------------------------------------------
347
+ // Helpers
348
+ // ---------------------------------------------------------------------------
132
349
 
133
- if (failed > 0) {
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 13 reserved annotation types from spec §4.
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 */