@warpgogol/forge 2.21.6 → 2.21.8

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 (41) hide show
  1. package/AGENTS.md +27 -1
  2. package/os/adr/adr-0000-template.md +8 -0
  3. package/os/adr/handlers/validate.test.ts +203 -0
  4. package/os/adr/handlers/validate.ts +54 -1
  5. package/os/adr/types.ts +7 -0
  6. package/os/compass/handlers/compass-inventory-handler.ts +11 -1
  7. package/os/compass/handlers/compass-inventory.ts +10 -0
  8. package/os/core/handlers/validate.ts +56 -5
  9. package/os/naming/naming-convention.ts +9 -0
  10. package/os/plugin/plugin.module.ts +1 -1
  11. package/os/rfc/acceptance.ts +133 -4
  12. package/os/rfc/handlers/implement-stamp.ts +14 -1
  13. package/os/rfc/handlers/validate-rules-rfc0997.test.ts +394 -0
  14. package/os/rfc/handlers/validate-rules-rfc1006.test.ts +478 -0
  15. package/os/rfc/handlers/validate-rules.ts +450 -9
  16. package/os/rfc/handlers/validate.ts +20 -2
  17. package/os/rfc/rfc-0000-template.md +24 -8
  18. package/os/rfc/rfc.module.ts +28 -0
  19. package/os/rfc/types.ts +72 -6
  20. package/os/rfc/verification-evidence.ts +5 -4
  21. package/os/rfc/verification-refresh.test.ts +320 -0
  22. package/os/rfc/verification-refresh.ts +216 -0
  23. package/os/session/handlers/save.ts +10 -0
  24. package/os/spec/spec-validate.test.ts +59 -0
  25. package/os/spec/spec-validate.ts +6 -4
  26. package/package.json +2 -1
  27. package/skills/fo/fo-handoff/SKILL.md +15 -6
  28. package/skills/fo/fo-idea-audit/SKILL.md +1 -1
  29. package/skills/fo/fo-idea-create-rfc/SKILL.md +1 -1
  30. package/skills/fo/fo-idea-create-rfc/acceptance-criteria-standard.md +75 -0
  31. package/skills/fo/fo-idea-implement/SKILL.md +3 -2
  32. package/src/compass/contract-registry.ts +25 -6
  33. package/src/index.ts +1 -1
  34. package/src/onboarding/doctor.ts +1 -1
  35. package/src/registry.ts +1 -1
  36. package/src/tests/acceptance-probe-kinds.test.ts +262 -0
  37. package/src/tests/plugin-manifest.test.ts +1 -1
  38. package/src/tests/session-handlers.test.ts +29 -0
  39. package/src/types/werkstatt-engine-shims.d.ts +0 -21
  40. package/src/types/werkstatt-shared-shims.d.ts +68 -147
  41. /package/src/plugin/{ForgePluginManifest.ts → forge-plugin-manifest.ts} +0 -0
package/AGENTS.md CHANGED
@@ -18,7 +18,7 @@ Packet 000 will add the portable `forge/program@1` control plane under `os/progr
18
18
  | Module | Commands | Source |
19
19
  | --- | --- | --- |
20
20
  | `forgeCoreModule` | `create`, `doctor`, `upgrade`, `forge.agents.generate`, `scaffold`, `port.scaffold`, `skill.validate`, `skill.list`, `port.validate`, `profile.validate`, `dev`, `build`, `validate`, `pinned.validate`, `pinned.init`, `package.health`, `docs.archive`, `forge.autonomy.validate` | `os/core/` |
21
- | `forgeRfcModule` | `rfc.list`, `rfc.validate`, `rfc.create`, etc. | `os/rfc/` |
21
+ | `forgeRfcModule` | `rfc.list`, `rfc.validate`, `rfc.create`, `rfc.verification.emit`, `rfc.verification.refresh`, etc. | `os/rfc/` |
22
22
  | `forgeWorkflowModule` | `workflow.lint`, `workflow.list`, `workflow.amend.list` | `os/workflow/` |
23
23
  | `forgeNamingModule` | `naming.convention.lint` | `os/naming/` |
24
24
  | `forgeCompassModule` | `compass.inventory`, `compass.validate`, `compass.summary.trim`, etc. (8 commands). All compass commands accept `--workpiece <path>` for scoping to a mission workpiece directory (RFC-0617). | `os/compass/` |
@@ -48,6 +48,8 @@ Skill packs MAY declare additional Compass contract blocks beyond the built-in `
48
48
 
49
49
  - **Verify `ecosystem.commit` output.** `ecosystem.commit` commits whatever is in the git index (staged files). If another agent staged files before your call, those files will be included in your commit instead of your own. After every `ecosystem.commit`, run `git show --stat HEAD` to verify the correct files were committed. If wrong files were committed, the missing files remain untracked/unstaged and must be committed separately.
50
50
  - **Wrap `loadForgeConfig` in try/catch in handlers that run in test workspaces.** `loadForgeConfig` throws when `forge.yaml` does not exist at the workspace root. Handlers that may be called from test workspaces without a `forge.yaml` (e.g. `runCompassValidation`) must guard the call with try/catch and skip pack-dependent logic when config is unavailable.
51
+ - **Register new top-level directories in `NAMING_CONVENTION_IGNORED_TOP_LEVEL`.** When a new top-level directory appears in the repo (e.g. `.adr-locks`, `.devin`, `.forge`, `.rfc-locks`, `patches`, `storage`), add it to `NAMING_CONVENTION_IGNORED_TOP_LEVEL` in `os/naming/naming-convention.ts`. Otherwise `naming.convention.lint` reports "unknown top-level directory" errors.
52
+ - **Add tool-generated/imported/historical dirs to `NAMING_CONVENTION_EXEMPT_DIRS`.** Directories containing tool-generated files (e.g. `docs/performance` with timestamp-based names), imported specification documents (e.g. `docs/specs` with `01-PBP-System-Specification.md`), and archived RFCs (e.g. `docs/rfcs/archive`) should be added to `NAMING_CONVENTION_EXEMPT_DIRS` in `os/naming/naming-convention.ts` to avoid false-positive kebab-case violations on files whose naming is controlled by external tools or conventions.
51
53
 
52
54
  ## Program packet control plane (RFC-0856)
53
55
 
@@ -151,6 +153,30 @@ YAML plain scalar values that **start with a backtick** (`` ` ``) must be double
151
153
 
152
154
  `rfc.implement.stamp` enforces RFC-IMP-02: every checked acceptance criterion (`- [x]`) MUST have an inline `(evidence: ...)` annotation. Parenthetical references without the `evidence:` keyword (e.g. `(promote.ts, 13 tests)`) do NOT satisfy the rule — the stamp fails with "checked criteria lack inline (evidence: ...) annotation". Always format as: `- [x] <criterion text> (evidence: <file paths, commands, or test counts>)`. Commit the annotated criteria before running `rfc.implement.stamp`.
153
155
 
156
+ ## RFC acceptance probes: criterion binding and coverage (RFC-0997)
157
+
158
+ RFC-0997 binds acceptance probes to criteria via the `criterion: "AC-N"` field and adds three validation rules for post-cutoff RFCs (createdAt >= `2026-09-01`):
159
+
160
+ - **V-35**: every probe SHALL declare `criterion` referencing an existing `AC-N` id. Missing, malformed, or dangling `criterion` is a blocking error.
161
+ - **V-36**: every top-level checklist line (`- [ ]` / `- [x]`) in `## Acceptance criteria` SHALL start with a unique `AC-N:` identifier. Duplicate or missing identifiers are blocking errors.
162
+ - **V-37**: checked criteria evidence SHALL resolve — `probe:AC-N` must reference a criterion with at least one bound probe; `test:<path>` and `<path>:<line>` must reference an existing file. **Evidence annotations MUST NOT contain em-dashes (—) or nested quotes** — the V-37 parser breaks on `test: path — "description"` and reports the file as non-existent. **The `<path>` token MUST be the file path only — no AC-N suffixes, no descriptions, no extra text after the path.** The parser treats everything after `test:` until end-of-parenthesis as the file path; `test: packages/forge/os/rfc/file.test.ts AC-1` is reported as non-existent because the parser looks for a file named `packages/forge/os/rfc/file.test.ts AC-1`. Use simple `(evidence: test:packages/forge/src/tests/file.test.ts)` without em-dashes, nested quotes, or AC-N suffixes. Discovered during RFC-0998 and RFC-0999.
163
+ - **RFC-IMP-08**: `rfc.implement.stamp` blocks stamping for post-cutoff RFCs of kind `architecture`, `contract`, or `command` that declare zero acceptance probes. Policy and deprecation kinds are exempt.
164
+ - **Coverage report**: `rfc.validate --json` and `rfc.acceptance.run` emit a non-blocking `coverage` block per post-cutoff RFC with `totalCriteria`, `probeBackedCriteria`, `coverageRatio`, `uncoveredCriteria`, `unboundProbes`.
165
+
166
+ ## RFC criteria content rules: phase separation, reject checklist, criterion versioning (RFC-1006)
167
+
168
+ RFC-1006 adds five validation rules for post-cutoff RFCs and ADRs (createdAt >= `2026-09-03`):
169
+
170
+ - **V-38**: `accepted`/`implemented` RFCs with unchecked `DR-N` items in `## Document readiness` section are blocking errors. Document readiness criteria (`DR-N`) are separated from system conformance criteria (`AC-N`).
171
+ - **V-39**: acceptance criteria containing `SHALL ... and ...` joining two verb phrases are non-atomic — split into separate criteria.
172
+ - **V-40**: acceptance criteria containing unbounded quantity triggers (`fast`, `scalable`, `low latency`, etc.) are blocking errors — state a specific number.
173
+ - **V-41**: acceptance criteria containing weasel verbs (`handle gracefully`, `robust`, `works correctly`, etc.) are blocking errors — replace with a specific observable behavior.
174
+ - **V-42**: malformed criterion supersession annotations (`> Superseded AC-N (YYYY-MM-DD): <reason>`) are blocking errors. Superseded criteria are excluded from the unchecked count.
175
+
176
+ V-39..V-41 also apply to post-cutoff `implemented` ADRs via `adr.validate`. Pre-cutoff RFCs and ADRs are exempt. The reject checklist is conservative — it uses closed trigger lists, not semantic analysis. See `packages/forge/skills/fo/fo-idea-create-rfc/acceptance-criteria-standard.md` for the canonical authoring guide.
177
+
178
+ - **Section extractors in `validate-rules.ts` MUST call `stripFencedCodeBlocks(body)` before regex-matching markdown sections.** RFC documents contain fenced code blocks with example headings and checklists. Without stripping, extractors like `extractDocumentReadinessSection` and `extractAcceptanceCriteriaSection` match headings inside code blocks, producing false-positive validation errors (e.g. V-38 reporting unchecked criteria from a code block example). The `stripFencedCodeBlocks` helper removes all ` ```...``` ` blocks before extraction. Any new section extractor added to `validate-rules.ts` must follow this pattern.
179
+
154
180
  ## Re-entrant werkstatt locks (RFC-0616)
155
181
 
156
182
  `acquireLock` and `releaseLock` in `os/werkstatt/handlers/lock.ts` are re-entrant by PID. When the same process re-acquires a lock it already holds, `acquireLock` increments the `depth` counter instead of throwing. `releaseLock` decrements `depth` and only deletes the lock file when `depth` reaches `1` or is `undefined`. The `depth` field is `.optional()` in `werkstattLockSchema` — old lock files without `depth` parse successfully and are treated as `depth=1` via `?? 1` fallbacks. Agents MUST NOT assume `acquireLock` always throws on an existing lock file — it only throws when a **different** live process holds the lock.
@@ -44,3 +44,11 @@ Forces and trade-offs that led to the decision: alternatives considered, constra
44
44
  ## Evolution
45
45
 
46
46
  How the decision may change: thresholds that trigger revisiting it, metrics to watch, and — for post-hoc ADRs — references to commits or PRs that implemented it.
47
+
48
+ ## Acceptance criteria
49
+
50
+ <!-- Optional. If present, each criterion is a falsifiable claim about an observable artifact,
51
+ written in EARS form with a stable AC-N: identifier. See packages/forge/skills/fo/fo-idea-create-rfc/acceptance-criteria-standard.md (RFC-0996).
52
+ When this section exists and the ADR is implemented, adr.validate enforces AV-17: all items must be checked with (evidence: ...). -->
53
+
54
+ - [ ] AC-1: THE <artifact> SHALL <observable property> (evidence: file: <path:line>)
@@ -230,3 +230,206 @@ describe("ADR-DIR-01: directory structure convention (RFC-0722)", () => {
230
230
  }
231
231
  });
232
232
  });
233
+
234
+ const ADR_BODY_WITH_CRITERIA = `
235
+ # ADR-9998: Test ADR with criteria
236
+
237
+ ## Context
238
+
239
+ Test context.
240
+
241
+ ## Decision
242
+
243
+ Test decision.
244
+
245
+ ## Consequences
246
+
247
+ Test consequences.
248
+
249
+ ## Evolution
250
+
251
+ Test evolution.
252
+
253
+ ## Acceptance criteria
254
+
255
+ - [ ] AC-1: THE system SHALL do X
256
+ - [ ] AC-2: THE system SHALL do Y
257
+ `;
258
+
259
+ const ADR_BODY_WITH_CHECKED_CRITERIA_NO_EVIDENCE = `
260
+ # ADR-9997: Test ADR with checked criteria no evidence
261
+
262
+ ## Context
263
+
264
+ Test context.
265
+
266
+ ## Decision
267
+
268
+ Test decision.
269
+
270
+ ## Consequences
271
+
272
+ Test consequences.
273
+
274
+ ## Evolution
275
+
276
+ Test evolution.
277
+
278
+ ## Acceptance criteria
279
+
280
+ - [x] AC-1: THE system SHALL do X
281
+ - [x] AC-2: THE system SHALL do Y
282
+ `;
283
+
284
+ const ADR_BODY_WITH_CHECKED_CRITERIA_WITH_EVIDENCE = `
285
+ # ADR-9996: Test ADR with checked criteria with evidence
286
+
287
+ ## Context
288
+
289
+ Test context.
290
+
291
+ ## Decision
292
+
293
+ Test decision.
294
+
295
+ ## Consequences
296
+
297
+ Test consequences.
298
+
299
+ ## Evolution
300
+
301
+ Test evolution.
302
+
303
+ ## Acceptance criteria
304
+
305
+ - [x] AC-1: THE system SHALL do X (evidence: test: src/test.test.ts)
306
+ - [x] AC-2: THE system SHALL do Y (evidence: file: src/index.ts:42)
307
+ `;
308
+
309
+ function createAdrFilePostCutoff(
310
+ workspaceRoot: string,
311
+ id: string,
312
+ status: string,
313
+ body: string,
314
+ extraFm: Record<string, unknown> = {},
315
+ ): void {
316
+ const adrDir = join(workspaceRoot, "docs", "adrs");
317
+ mkdirSync(adrDir, { recursive: true });
318
+ const slug = id.toLowerCase();
319
+ const fm = [
320
+ "---",
321
+ `id: ${id}`,
322
+ `title: "Test ADR"`,
323
+ `status: ${status}`,
324
+ `scope: package`,
325
+ `decider: human:test`,
326
+ `createdAt: 2026-09-15`,
327
+ `updatedAt: 2026-09-15`,
328
+ ...Object.entries(extraFm).map(([k, v]) => `${k}: ${v}`),
329
+ "---",
330
+ "",
331
+ body,
332
+ ].join("\n");
333
+ writeFileSync(join(adrDir, `${slug}-test.md`), fm);
334
+ }
335
+
336
+ describe("AV-17: acceptance criteria completeness for implemented post-cutoff ADRs (RFC-0996)", () => {
337
+ test("AC-6: post-cutoff implemented ADR with unchecked criteria → AV-17 error", async () => {
338
+ const dir = mkdtempSync(join(tmpdir(), "av17-unchecked-"));
339
+ try {
340
+ createAdrFilePostCutoff(dir, "ADR-9998", "implemented", ADR_BODY_WITH_CRITERIA, {
341
+ implementedAt: "2026-09-16",
342
+ });
343
+ const violations = await runValidate(dir, "ADR-9998");
344
+ const av17 = filterRule(violations, "AV-17");
345
+ expect(av17).toHaveLength(1);
346
+ expect(av17[0]!.severity).toBe("error");
347
+ expect(av17[0]!.message).toContain("2 unchecked acceptance criteria");
348
+ expect(av17[0]!.message).toContain("implemented");
349
+ } finally {
350
+ rmSync(dir, { recursive: true, force: true });
351
+ }
352
+ });
353
+
354
+ test("AC-7: post-cutoff implemented ADR with checked criteria but no evidence → AV-17 error", async () => {
355
+ const dir = mkdtempSync(join(tmpdir(), "av17-noevidence-"));
356
+ try {
357
+ createAdrFilePostCutoff(
358
+ dir,
359
+ "ADR-9997",
360
+ "implemented",
361
+ ADR_BODY_WITH_CHECKED_CRITERIA_NO_EVIDENCE,
362
+ {
363
+ implementedAt: "2026-09-16",
364
+ },
365
+ );
366
+ const violations = await runValidate(dir, "ADR-9997");
367
+ const av17 = filterRule(violations, "AV-17");
368
+ expect(av17).toHaveLength(2);
369
+ expect(av17.every((v) => v.severity === "error")).toBe(true);
370
+ expect(av17.every((v) => v.message.includes("without (evidence: ...)"))).toBe(true);
371
+ } finally {
372
+ rmSync(dir, { recursive: true, force: true });
373
+ }
374
+ });
375
+
376
+ test("AC-8a: pre-cutoff implemented ADR with unchecked criteria → no AV-17", async () => {
377
+ const dir = mkdtempSync(join(tmpdir(), "av17-precutoff-"));
378
+ try {
379
+ createAdrFile(dir, "ADR-9998", "implemented", ADR_BODY_WITH_CRITERIA, {
380
+ implementedAt: "2026-01-15",
381
+ });
382
+ const violations = await runValidate(dir, "ADR-9998");
383
+ const av17 = filterRule(violations, "AV-17");
384
+ expect(av17).toHaveLength(0);
385
+ } finally {
386
+ rmSync(dir, { recursive: true, force: true });
387
+ }
388
+ });
389
+
390
+ test("AC-8b: post-cutoff implemented ADR without criteria section → no AV-17", async () => {
391
+ const dir = mkdtempSync(join(tmpdir(), "av17-nosection-"));
392
+ try {
393
+ createAdrFilePostCutoff(dir, "ADR-9999", "implemented", ADR_BODY, {
394
+ implementedAt: "2026-09-16",
395
+ });
396
+ const violations = await runValidate(dir, "ADR-9999");
397
+ const av17 = filterRule(violations, "AV-17");
398
+ expect(av17).toHaveLength(0);
399
+ } finally {
400
+ rmSync(dir, { recursive: true, force: true });
401
+ }
402
+ });
403
+
404
+ test("AC-8c: post-cutoff accepted (not implemented) ADR with unchecked criteria → no AV-17", async () => {
405
+ const dir = mkdtempSync(join(tmpdir(), "av17-notimplemented-"));
406
+ try {
407
+ createAdrFilePostCutoff(dir, "ADR-9998", "accepted", ADR_BODY_WITH_CRITERIA);
408
+ const violations = await runValidate(dir, "ADR-9998");
409
+ const av17 = filterRule(violations, "AV-17");
410
+ expect(av17).toHaveLength(0);
411
+ } finally {
412
+ rmSync(dir, { recursive: true, force: true });
413
+ }
414
+ });
415
+
416
+ test("post-cutoff implemented ADR with all criteria checked and evidence → no AV-17", async () => {
417
+ const dir = mkdtempSync(join(tmpdir(), "av17-clean-"));
418
+ try {
419
+ createAdrFilePostCutoff(
420
+ dir,
421
+ "ADR-9996",
422
+ "implemented",
423
+ ADR_BODY_WITH_CHECKED_CRITERIA_WITH_EVIDENCE,
424
+ {
425
+ implementedAt: "2026-09-16",
426
+ },
427
+ );
428
+ const violations = await runValidate(dir, "ADR-9996");
429
+ const av17 = filterRule(violations, "AV-17");
430
+ expect(av17).toHaveLength(0);
431
+ } finally {
432
+ rmSync(dir, { recursive: true, force: true });
433
+ }
434
+ });
435
+ });
@@ -35,8 +35,10 @@ import {
35
35
  ADR_REQUIRED_SECTIONS,
36
36
  ADR_SCOPES,
37
37
  ADR_STATUSES,
38
+ ADR_ACCEPTANCE_CRITERIA_CUTOFF,
38
39
  } from "../types.ts";
39
- import { RFC_DIR } from "../../rfc/types.ts";
40
+ import { RFC_DIR, RFC_CRITERIA_CONTENT_CUTOFF } from "../../rfc/types.ts";
41
+ import { evaluateAcceptanceCriteria } from "../../rfc/handlers/validate-rules.ts";
40
42
 
41
43
  async function loadRfcIds(workspaceRoot: string): Promise<Set<string>> {
42
44
  const rfcIds = new Set<string>();
@@ -426,6 +428,57 @@ async function validateSingleAdr(
426
428
  }
427
429
  }
428
430
  }
431
+
432
+ // AV-17: acceptance criteria completeness for implemented post-cutoff ADRs (RFC-0996)
433
+ if (status === "implemented" && createdAt >= ADR_ACCEPTANCE_CRITERIA_CUTOFF) {
434
+ const evaluation = evaluateAcceptanceCriteria(body);
435
+ if (evaluation.totalUnchecked > 0) {
436
+ addViolation(
437
+ adrId,
438
+ relFile,
439
+ "AV-17",
440
+ `${adrId} has ${evaluation.totalUnchecked} unchecked acceptance criteria in "## Acceptance criteria" section but status is "implemented".`,
441
+ );
442
+ }
443
+ for (const line of evaluation.checkedWithoutEvidence) {
444
+ const snippet = line.length > 80 ? line.slice(0, 77) + "..." : line;
445
+ addViolation(
446
+ adrId,
447
+ relFile,
448
+ "AV-17",
449
+ `${adrId} has checked criterion without (evidence: ...) annotation: "${snippet}".`,
450
+ );
451
+ }
452
+ }
453
+
454
+ // V-39..V-41: reject checklist for post-cutoff implemented ADRs (RFC-1006)
455
+ if (status === "implemented" && createdAt >= RFC_CRITERIA_CONTENT_CUTOFF) {
456
+ const evaluation = evaluateAcceptanceCriteria(body);
457
+ for (const v of evaluation.nonAtomicViolations) {
458
+ addViolation(
459
+ adrId,
460
+ relFile,
461
+ "V-39",
462
+ `${adrId} acceptance criterion ${v.acId} is non-atomic (contains "and" joining two behaviors): "${v.line}". Split into separate criteria.`,
463
+ );
464
+ }
465
+ for (const v of evaluation.unboundedQuantityViolations) {
466
+ addViolation(
467
+ adrId,
468
+ relFile,
469
+ "V-40",
470
+ `${adrId} acceptance criterion ${v.acId} contains unbounded quantity "${v.trigger}": state a specific number or reference the decision that will set it.`,
471
+ );
472
+ }
473
+ for (const v of evaluation.weaselVerbViolations) {
474
+ addViolation(
475
+ adrId,
476
+ relFile,
477
+ "V-41",
478
+ `${adrId} acceptance criterion ${v.acId} contains weasel verb "${v.trigger}": replace with a specific observable behavior.`,
479
+ );
480
+ }
481
+ }
429
482
  }
430
483
 
431
484
  function execGitLog(workspaceRoot: string, args: string[]): Promise<string> {
package/os/adr/types.ts CHANGED
@@ -16,6 +16,7 @@ statuses, scopes, frontmatter shape, validation results, and list output.
16
16
  <item>Post-refactor hardening: document that an ADR may be superseded by a broader RFC.</item>
17
17
  <item>RFC-0521: migrated from packages/os/site-kernel/src/adr/ to packages/forge/os/adr/.</item>
18
18
  <item>RFC-0727: add AdrImplementStamp types for atomic ADR status transition.</item>
19
+ <item>RFC-0996: add AV-17 acceptance criteria completeness check for implemented post-cutoff ADRs.</item>
19
20
  </CHANGE_SUMMARY>
20
21
  */
21
22
 
@@ -103,6 +104,12 @@ export const ADR_REQUIRED_SECTIONS = [
103
104
  "Evolution",
104
105
  ] as const;
105
106
 
107
+ /**
108
+ * RFC-0996: cutoff date for AV-17 (acceptance criteria completeness on implemented ADRs).
109
+ * ADRs created on or after this date are subject to AV-17. Pre-cutoff ADRs are exempt.
110
+ */
111
+ export const ADR_ACCEPTANCE_CRITERIA_CUTOFF = "2026-09-01";
112
+
106
113
  export const ADR_KNOWN_KEYS: readonly string[] = [
107
114
  "id",
108
115
  "title",
@@ -48,9 +48,12 @@ function extractBlockContentForPlugin(source: string, tagName: string): string |
48
48
 
49
49
  const INVENTORY_OUTPUT_PATH = "docs/compass-inventory.xml";
50
50
 
51
- function getExpectedCommentSyntax(filePath: string): "block" | "hash" | "semicolon" | null {
51
+ function getExpectedCommentSyntax(
52
+ filePath: string,
53
+ ): "block" | "hash" | "semicolon" | "html" | null {
52
54
  if (filePath.endsWith(".gd")) return "hash";
53
55
  if (filePath.endsWith(".tscn") || filePath.endsWith(".tres")) return "semicolon";
56
+ if (filePath.endsWith(".md")) return "html";
54
57
  if (
55
58
  filePath.endsWith(".ts") ||
56
59
  filePath.endsWith(".tsx") ||
@@ -96,6 +99,13 @@ function checkCommentSyntax(source: string, filePath: string): string | null {
96
99
  }
97
100
  }
98
101
  }
102
+ } else if (syntax === "html") {
103
+ const blockStart = source.indexOf("<!--");
104
+ const blockEnd = source.lastIndexOf("-->");
105
+ const tagPos = source.indexOf("<MODULE_CONTRACT>");
106
+ if (blockStart === -1 || blockEnd === -1 || blockStart > tagPos || tagPos > blockEnd) {
107
+ return "MODULE_CONTRACT must be inside a <!-- ... --> HTML comment for .md files";
108
+ }
99
109
  }
100
110
  return null;
101
111
  }
@@ -15,6 +15,7 @@ for Compass source-file inventory.</purpose>
15
15
  <item>Post-refactor hardening: detect nested packages/os workspaces before deriving Compass layer and workspace name.</item>
16
16
  <item>RFC-0556: moved canonical implementation from @warpgogol/site-kernel to @warpgogol/forge for autonomous mode.</item>
17
17
  <item>Game extensions: added .cs, .tscn, .tres, .gd to SOURCE_EXTENSIONS; createCompassInventoryEntries now reads forge.yaml compass.fileExtensions at runtime and merges with hardcoded set.</item>
18
+ <item>Added .md to SOURCE_EXTENSIONS for SKILL.md Compass coverage; detectAuthoringStatus excludes non-SKILL.md markdown files.</item>
18
19
  </CHANGE_SUMMARY>
19
20
  */
20
21
 
@@ -58,6 +59,7 @@ const SOURCE_EXTENSIONS = new Set([
58
59
  ".tscn",
59
60
  ".tres",
60
61
  ".gd",
62
+ ".md",
61
63
  ]);
62
64
  const IGNORED_DIRECTORY_NAMES = new Set([
63
65
  ".git",
@@ -266,6 +268,7 @@ function detectLayer(relativePath: string): string {
266
268
  )
267
269
  return "test";
268
270
  if (relativePath.startsWith("src/")) return "source";
271
+ if (relativePath.endsWith("SKILL.md")) return "skill";
269
272
  return "other";
270
273
  }
271
274
 
@@ -386,6 +389,13 @@ function detectAuthoringStatus(
386
389
  };
387
390
  }
388
391
 
392
+ if (relativePath.endsWith(".md") && !relativePath.endsWith("SKILL.md")) {
393
+ return {
394
+ authoringStatus: "excluded",
395
+ exclusionReason: "non-skill-markdown",
396
+ };
397
+ }
398
+
389
399
  const isRootFile = !relativePath.includes("/");
390
400
  if (
391
401
  isRootFile &&
@@ -3,12 +3,13 @@
3
3
  <purpose>forge.validate — execute validate commands for all artifacts declared in the active stack profile. Supports --dry-run, --json, --artifact filtering, and violation parsing.</purpose>
4
4
  <non-goals>
5
5
  <item>Do not implement dev or build logic — those are separate handlers.</item>
6
- <item>Do not import from @warpgogol/* — this module is portable.</item>
6
+ <item>Do not import from @warpgogol/* — compass handler is intra-package within @warpgogol/forge.</item>
7
7
  </non-goals>
8
8
  </MODULE_CONTRACT>
9
9
  <CHANGE_SUMMARY>
10
10
  <item>RFC-0674: initial forge.validate handler with profile resolution, --dry-run, and per-artifact execution.</item>
11
11
  <item>RFC-0677: extended with --artifact filtering, violation parsing (outputFormat: json/plain), passed/allPassed fields.</item>
12
+ <item>Integrated compass.validate as in-process step after artifact validations for automatic Compass enforcement.</item>
12
13
  </CHANGE_SUMMARY>
13
14
  */
14
15
 
@@ -20,6 +21,7 @@ import type {
20
21
  ForgeRuntimeContext,
21
22
  } from "../../../src/types.ts";
22
23
  import { resolveActiveProfile, resolveLifecycleFlags } from "./profile-resolve.ts";
24
+ import { runCompassValidation } from "../../compass/handlers/compass-inventory-handler.ts";
23
25
 
24
26
  const execAsync = promisify(exec);
25
27
 
@@ -41,11 +43,25 @@ export interface ForgeValidateArtifactResult {
41
43
  stderr: string;
42
44
  }
43
45
 
46
+ export interface ForgeValidateCompassResult {
47
+ passed: boolean;
48
+ checkedFiles: number;
49
+ failures: number;
50
+ diagnostics: Array<{
51
+ ruleId: string;
52
+ severity: string;
53
+ file: string;
54
+ message: string;
55
+ fix: string;
56
+ }>;
57
+ }
58
+
44
59
  export interface ForgeValidateResult {
45
60
  command: "forge.validate";
46
61
  profileId: string;
47
62
  artifacts: ForgeValidateArtifactResult[];
48
63
  allPassed: boolean;
64
+ compass?: ForgeValidateCompassResult;
49
65
  }
50
66
 
51
67
  export function parseViolations(
@@ -185,6 +201,12 @@ export async function runValidate(
185
201
  stderr: "",
186
202
  })),
187
203
  allPassed: true,
204
+ compass: {
205
+ passed: true,
206
+ checkedFiles: 0,
207
+ failures: 0,
208
+ diagnostics: [],
209
+ },
188
210
  },
189
211
  summary: `[dry-run] ${commands.length} validate command(s) resolved`,
190
212
  };
@@ -245,7 +267,37 @@ export async function runValidate(
245
267
  }
246
268
  }
247
269
 
248
- const allPassed = results.every((r) => r.passed);
270
+ const artifactsAllPassed = results.every((r) => r.passed);
271
+
272
+ let compassResult: ForgeValidateCompassResult | undefined;
273
+ try {
274
+ const compassResponse = await runCompassValidation({ argv: [], flags: {} }, context);
275
+ if (compassResponse.data) {
276
+ compassResult = {
277
+ passed: (compassResponse.exitCode ?? 0) === 0,
278
+ checkedFiles: compassResponse.data.checkedFiles,
279
+ failures: compassResponse.data.failures,
280
+ diagnostics: compassResponse.data.diagnostics,
281
+ };
282
+ }
283
+ } catch {
284
+ // compass.validate not available in this workspace — skip
285
+ }
286
+
287
+ const allPassed = artifactsAllPassed && (compassResult?.passed ?? true);
288
+
289
+ const summaryParts: string[] = [
290
+ allPassed
291
+ ? `forge.validate: all ${results.length} artifact(s) passed`
292
+ : `forge.validate: ${results.filter((r) => !r.passed).length} artifact(s) failed`,
293
+ ];
294
+ if (compassResult) {
295
+ summaryParts.push(
296
+ compassResult.passed
297
+ ? `compass: OK (${compassResult.checkedFiles} files)`
298
+ : `compass: ${compassResult.failures} failure(s)`,
299
+ );
300
+ }
249
301
 
250
302
  return {
251
303
  data: {
@@ -253,10 +305,9 @@ export async function runValidate(
253
305
  profileId: profile.id,
254
306
  artifacts: results,
255
307
  allPassed,
308
+ compass: compassResult,
256
309
  },
257
310
  exitCode: allPassed ? 0 : 1,
258
- summary: allPassed
259
- ? `forge.validate: all ${results.length} artifact(s) passed`
260
- : `forge.validate: ${results.filter((r) => !r.passed).length} artifact(s) failed`,
311
+ summary: summaryParts.join("; "),
261
312
  };
262
313
  }
@@ -63,6 +63,12 @@ const NAMING_CONVENTION_IGNORED_TOP_LEVEL = new Set([
63
63
  ".windsurf",
64
64
  ".cache",
65
65
  "tmp",
66
+ ".adr-locks",
67
+ ".rfc-locks",
68
+ ".devin",
69
+ ".forge",
70
+ "patches",
71
+ "storage",
66
72
  ]);
67
73
 
68
74
  // Tool-mandated filenames exempt from kebab-case (Docker, Caddy use these exact names).
@@ -93,6 +99,9 @@ const NAMING_CONVENTION_EXEMPT_KEYWORDS = ["config", "module"] as const;
93
99
  // Set of directory paths that are exempt from naming convention (generated files)
94
100
  const NAMING_CONVENTION_EXEMPT_DIRS = new Set([
95
101
  "components/icons/gen", // Generated icon components — naming controlled by generator
102
+ "docs/performance", // Tool-generated screenshots and Lighthouse reports with timestamps
103
+ "docs/specs", // Imported specification documents with their own naming convention
104
+ "docs/rfcs/archive", // Archived RFCs — historical artifacts, cannot be renamed
96
105
  ]);
97
106
 
98
107
  /**
@@ -21,7 +21,7 @@ import type {
21
21
  ForgeRuntimeContext,
22
22
  } from "../../src/types.ts";
23
23
  import { loadForgeConfig } from "../../src/config/forge-config.ts";
24
- import { forgePluginManifestSchema } from "../../src/plugin/ForgePluginManifest.ts";
24
+ import { forgePluginManifestSchema } from "../../src/plugin/forge-plugin-manifest.ts";
25
25
 
26
26
  export interface PluginValidateResult {
27
27
  command: "forge.plugin.validate";