phasegate 0.160.19 → 0.160.21

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 (25) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/README.md +4 -4
  3. package/docs/guide/installation.md +5 -5
  4. package/docs/guide/setup-artifacts.md +3 -2
  5. package/docs/templates/personal/hooks/pre-commit +16 -0
  6. package/package.json +1 -1
  7. package/scripts/harness/config-foundation/application/mappers/validator-system-config-mapper.ts +19 -2
  8. package/scripts/harness/config-foundation/infrastructure/schemas/harness-config-v3.schema.json +1 -3
  9. package/scripts/harness/installation/application/bundled-skill-selection.ts +46 -0
  10. package/scripts/harness/installation/application/checks/check-utils.ts +11 -0
  11. package/scripts/harness/installation/application/checks/claude-skills-symlink-check.ts +5 -3
  12. package/scripts/harness/installation/application/checks/codex-skills-symlink-check.ts +5 -3
  13. package/scripts/harness/installation/application/usecases/run-install.ts +65 -26
  14. package/scripts/harness/installation/application/usecases/run-reconcile.ts +97 -10
  15. package/scripts/harness/installation/application/usecases/run-uninstall.ts +36 -5
  16. package/scripts/harness/main.ts +40 -18
  17. package/scripts/harness/validator-system/application/dto/validation-result-contract.ts +1 -0
  18. package/scripts/harness/validator-system/application/mappers/validation-result-contract-mapper.ts +1 -0
  19. package/scripts/harness/validator-system/application/use-cases/run-l4-validators-usecase.ts +32 -3
  20. package/scripts/harness/validator-system/composition-root.ts +15 -1
  21. package/scripts/harness/validator-system/domain/services/l4/consistency-check-service.ts +78 -1
  22. package/scripts/harness/validator-system/domain/value-objects/validation-result.ts +7 -0
  23. package/scripts/harness/validator-system/infrastructure/adapters/file-system-work-item-reflection-adapter.ts +102 -0
  24. package/scripts/harness/validator-system/presentation/formatters/agent-validation-result-formatter.ts +3 -0
  25. package/scripts/harness/validator-system/presentation/formatters/human-validation-result-formatter.ts +3 -0
@@ -22,6 +22,7 @@ export interface ValidationResultRawProps {
22
22
  readonly errors: readonly HarnessErrorLike[];
23
23
  readonly durationMs: number;
24
24
  readonly skipped: boolean;
25
+ readonly skipReason?: string;
25
26
  }
26
27
 
27
28
  export class ValidationResult {
@@ -30,6 +31,7 @@ export class ValidationResult {
30
31
  readonly errors: readonly HarnessErrorLike[];
31
32
  readonly durationMs: number;
32
33
  readonly skipped: boolean;
34
+ readonly skipReason?: string;
33
35
 
34
36
  private constructor(props: ValidationResultRawProps) {
35
37
  this.validatorId = props.validatorId;
@@ -37,6 +39,7 @@ export class ValidationResult {
37
39
  this.errors = Object.freeze([...props.errors]);
38
40
  this.durationMs = props.durationMs;
39
41
  this.skipped = props.skipped;
42
+ this.skipReason = props.skipReason;
40
43
  Object.freeze(this);
41
44
  }
42
45
 
@@ -71,6 +74,10 @@ export class ValidationResult {
71
74
  return new ValidationResult({ validatorId, passed: true, errors: [], durationMs: 0, skipped: true });
72
75
  }
73
76
 
77
+ static skipWithReason(validatorId: ValidatorId, skipReason: string): ValidationResult {
78
+ return new ValidationResult({ validatorId, passed: true, errors: [], durationMs: 0, skipped: true, skipReason });
79
+ }
80
+
74
81
  hasErrors(): boolean {
75
82
  return this.errors.length > 0;
76
83
  }
@@ -0,0 +1,102 @@
1
+ /**
2
+ * @layer infrastructure
3
+ * @unit validator-system
4
+ * @work-item-id WI-217
5
+ */
6
+ import { readFile, readdir } from 'node:fs/promises';
7
+ import { join, relative } from 'node:path';
8
+ import type {
9
+ WorkItemReflectionPort,
10
+ WorkItemReflectionSnapshot,
11
+ } from '../../domain/services/l4/consistency-check-service.js';
12
+
13
+ const DESCRIPTION_FILE = 'description.md';
14
+ const FRONTMATTER_PATTERN = /^---\r?\n([\s\S]*?)\r?\n---/;
15
+ const WORK_ITEM_ANNOTATION_PATTERN = /@work-item-id\s+([^<\r\n]+)/g;
16
+
17
+ function normalizePath(value: string): string {
18
+ return value.replace(/\\/g, '/');
19
+ }
20
+
21
+ async function listFiles(root: string): Promise<string[]> {
22
+ try {
23
+ const entries = await readdir(root, { withFileTypes: true });
24
+ const files: string[] = [];
25
+ for (const entry of entries) {
26
+ const path = join(root, entry.name);
27
+ if (entry.isDirectory()) {
28
+ files.push(...(await listFiles(path)));
29
+ } else if (entry.isFile()) {
30
+ files.push(path);
31
+ }
32
+ }
33
+ return files;
34
+ } catch {
35
+ return [];
36
+ }
37
+ }
38
+
39
+ function extractFrontmatterValue(markdown: string, key: string): string | undefined {
40
+ const frontmatter = FRONTMATTER_PATTERN.exec(markdown)?.[1];
41
+ if (!frontmatter) return undefined;
42
+ const pattern = new RegExp(`^${key}:\\s*(.+?)\\s*$`, 'm');
43
+ return pattern.exec(frontmatter)?.[1]?.replace(/^["']|["']$/g, '').trim();
44
+ }
45
+
46
+ function extractWorkItemRefs(markdown: string): string[] {
47
+ const refs = new Set<string>();
48
+ for (const match of markdown.matchAll(WORK_ITEM_ANNOTATION_PATTERN)) {
49
+ const raw = match[1].replace(/-->.*/, '');
50
+ for (const token of raw.split(/[,\s]+/).map((part) => part.trim()).filter(Boolean)) {
51
+ if (/^[A-Za-z][A-Za-z0-9_-]*-\d+(?:-\d+)*$/.test(token)) {
52
+ refs.add(token);
53
+ }
54
+ }
55
+ }
56
+ return [...refs];
57
+ }
58
+
59
+ export class FileSystemWorkItemReflectionAdapter implements WorkItemReflectionPort {
60
+ constructor(private readonly projectRoot: string) {}
61
+
62
+ async collect(input: {
63
+ readonly inceptionRoot: string;
64
+ readonly designRoot: string;
65
+ }): Promise<WorkItemReflectionSnapshot> {
66
+ const inceptionRoot = join(this.projectRoot, input.inceptionRoot);
67
+ const designRoot = join(this.projectRoot, input.designRoot);
68
+ const descriptionFiles = (await listFiles(inceptionRoot))
69
+ .filter((path) => path.endsWith(`/${DESCRIPTION_FILE}`));
70
+
71
+ if (descriptionFiles.length === 0) {
72
+ return {
73
+ workItems: [],
74
+ productRefs: [],
75
+ skipReason: `no work item descriptions found under ${input.inceptionRoot}`,
76
+ };
77
+ }
78
+
79
+ const workItems: Array<WorkItemReflectionSnapshot['workItems'][number]> = [];
80
+ for (const path of descriptionFiles) {
81
+ const markdown = await readFile(path, 'utf8');
82
+ const id = extractFrontmatterValue(markdown, 'id');
83
+ if (!id) continue;
84
+ workItems.push({
85
+ id,
86
+ path: normalizePath(relative(this.projectRoot, path)),
87
+ type: extractFrontmatterValue(markdown, 'type'),
88
+ });
89
+ }
90
+
91
+ const productRefs: Array<WorkItemReflectionSnapshot['productRefs'][number]> = [];
92
+ const productFiles = (await listFiles(designRoot)).filter((path) => path.endsWith('.md'));
93
+ for (const path of productFiles) {
94
+ const markdown = await readFile(path, 'utf8');
95
+ for (const id of extractWorkItemRefs(markdown)) {
96
+ productRefs.push({ id, path: normalizePath(relative(this.projectRoot, path)) });
97
+ }
98
+ }
99
+
100
+ return { workItems, productRefs };
101
+ }
102
+ }
@@ -18,6 +18,9 @@ export class AgentValidationResultFormatter {
18
18
  lines.push(`VALIDATOR: ${result.validatorId}`);
19
19
  lines.push(`STATUS: ${result.skipped ? 'SKIPPED' : result.passed ? 'PASSED' : 'FAILED'}`);
20
20
  lines.push(`DURATION: ${result.durationMs}ms`);
21
+ if (result.skipped && result.skipReason) {
22
+ lines.push(`SKIP_REASON: ${result.skipReason}`);
23
+ }
21
24
  if (result.errors.length > 0) {
22
25
  lines.push('ERRORS:');
23
26
  for (const error of result.errors) {
@@ -25,6 +25,9 @@ export class HumanValidationResultFormatter {
25
25
  ? 'FAIL'
26
26
  : 'WARN';
27
27
  lines.push(`[${status}] ${result.validatorId} (${result.durationMs}ms)`);
28
+ if (result.skipped && result.skipReason) {
29
+ lines.push(` → ${result.skipReason}`);
30
+ }
28
31
  for (const error of result.errors) {
29
32
  lines.push(` ⚠ ${error.message}`);
30
33
  lines.push(` → ${error.suggestion}`);