phasegate 0.135.0 → 0.137.0

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 (36) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.ja.md +7 -5
  3. package/README.md +7 -4
  4. package/docs/guide/cli-reference.md +3 -0
  5. package/docs/templates/agent-context/CLAUDE.md.template.md +29 -0
  6. package/docs/templates/ci/agent-context-refresh.yml +59 -0
  7. package/docs/templates/ci/aidlc-gate.yml +97 -0
  8. package/docs/templates/ci/consistency-check.yml +117 -0
  9. package/docs/templates/hooks/commit-msg +13 -0
  10. package/docs/templates/hooks/pre-commit +62 -0
  11. package/package.json +2 -1
  12. package/scripts/harness/ci-governance/application/dto/check-agent-context-input.ts +8 -0
  13. package/scripts/harness/ci-governance/application/dto/check-agent-context-output.ts +17 -0
  14. package/scripts/harness/ci-governance/application/dto/refresh-agent-context-input.ts +8 -0
  15. package/scripts/harness/ci-governance/application/dto/refresh-agent-context-output.ts +15 -0
  16. package/scripts/harness/ci-governance/application/dto/refresh-claude-md-input.ts +8 -0
  17. package/scripts/harness/ci-governance/application/dto/refresh-claude-md-output.ts +13 -0
  18. package/scripts/harness/ci-governance/application/usecases/check-agent-context-usecase.ts +48 -0
  19. package/scripts/harness/ci-governance/application/usecases/refresh-agent-context-usecase.ts +31 -0
  20. package/scripts/harness/ci-governance/application/usecases/refresh-claude-md-usecase.ts +73 -0
  21. package/scripts/harness/ci-governance/composition-root.ts +23 -1
  22. package/scripts/harness/ci-governance/domain/aggregates/ci-template.ts +1 -1
  23. package/scripts/harness/ci-governance/domain/ports/agent-context-document-port.ts +17 -0
  24. package/scripts/harness/ci-governance/domain/services/claude-md-composer.ts +39 -0
  25. package/scripts/harness/ci-governance/domain/services/template-generator.ts +1 -0
  26. package/scripts/harness/ci-governance/domain/types/template-type.ts +2 -1
  27. package/scripts/harness/ci-governance/infrastructure/adapters/agent-context-file-adapter.ts +55 -0
  28. package/scripts/harness/ci-governance/infrastructure/adapters/yaml-template-renderer-adapter.ts +16 -31
  29. package/scripts/harness/ci-governance/presentation/handlers/check-agent-context-handler.ts +33 -0
  30. package/scripts/harness/ci-governance/presentation/handlers/generate-ci-template-handler.ts +19 -1
  31. package/scripts/harness/ci-governance/presentation/handlers/refresh-agent-context-handler.ts +44 -0
  32. package/scripts/harness/ci-governance/presentation/handlers/refresh-claude-md-handler.ts +38 -0
  33. package/scripts/harness/main.ts +57 -8
  34. package/scripts/harness/setup/skill-deployer.ts +52 -2
  35. package/scripts/harness/traceability-model/domain/value-objects/work-item-frontmatter.ts +3 -1
  36. package/scripts/harness/validator-system/infrastructure/adapters/phase-dependency-phase-gate-policy-adapter.ts +4 -0
@@ -0,0 +1,31 @@
1
+ /**
2
+ * @layer application
3
+ * @unit ci-governance
4
+ */
5
+
6
+ import type { MigrateAgentsMdUseCase } from './migrate-agents-md-usecase.js';
7
+ import type { RefreshClaudeMdUseCase } from './refresh-claude-md-usecase.js';
8
+ import type { RefreshAgentContextInput } from '../dto/refresh-agent-context-input.js';
9
+ import type { RefreshAgentContextOutput } from '../dto/refresh-agent-context-output.js';
10
+
11
+ export class RefreshAgentContextUseCase {
12
+ constructor(
13
+ private readonly migrateAgentsMdUseCase: MigrateAgentsMdUseCase,
14
+ private readonly refreshClaudeMdUseCase: RefreshClaudeMdUseCase,
15
+ ) {}
16
+
17
+ async execute(input: RefreshAgentContextInput): Promise<RefreshAgentContextOutput> {
18
+ const [agentsMd, claudeMd] = await Promise.all([
19
+ this.migrateAgentsMdUseCase.execute({ dryRun: input.dryRun }),
20
+ this.refreshClaudeMdUseCase.execute({ dryRun: input.dryRun }),
21
+ ]);
22
+ const errors = [...agentsMd.errors, ...claudeMd.errors];
23
+ return {
24
+ success: agentsMd.success && claudeMd.success,
25
+ applied: !input.dryRun && agentsMd.success && claudeMd.success,
26
+ agentsMd,
27
+ claudeMd,
28
+ errors,
29
+ };
30
+ }
31
+ }
@@ -0,0 +1,73 @@
1
+ /**
2
+ * @layer application
3
+ * @unit ci-governance
4
+ */
5
+
6
+ import type { AgentContextDocumentPort } from '../../domain/ports/agent-context-document-port.js';
7
+ import type { ClaudeMdComposer } from '../../domain/services/claude-md-composer.js';
8
+ import type { RefreshClaudeMdInput } from '../dto/refresh-claude-md-input.js';
9
+ import type { RefreshClaudeMdOutput } from '../dto/refresh-claude-md-output.js';
10
+
11
+ const CLAUDE_MD_PATH = 'CLAUDE.md';
12
+ const CLAUDE_MD_TEMPLATE_PATH = 'docs/templates/agent-context/CLAUDE.md.template.md';
13
+
14
+ const PHASEGATE_COMMANDS = [
15
+ 'phasegate init --with-ci',
16
+ 'phasegate ci:auto-refresh-agent-context --dry-run',
17
+ 'phasegate ci:auto-refresh-agent-context --apply',
18
+ 'phasegate refresh-claude-md --apply',
19
+ 'phasegate p2:check-agent-context',
20
+ 'phasegate phasegate:check-ready',
21
+ ];
22
+
23
+ const PHASE_PRESETS = ['minimal', 'standard', 'full', 'custom'];
24
+
25
+ export class RefreshClaudeMdUseCase {
26
+ constructor(
27
+ private readonly documentPort: AgentContextDocumentPort,
28
+ private readonly composer: ClaudeMdComposer,
29
+ ) {}
30
+
31
+ async execute(input: RefreshClaudeMdInput): Promise<RefreshClaudeMdOutput> {
32
+ try {
33
+ const [template, existing, skills] = await Promise.all([
34
+ this.documentPort.readHarnessTemplate(CLAUDE_MD_TEMPLATE_PATH),
35
+ this.documentPort.readProjectFile(CLAUDE_MD_PATH),
36
+ this.documentPort.listHarnessSkillNames(),
37
+ ]);
38
+ const nextContent = this.composer.compose(template, existing, {
39
+ commands: PHASEGATE_COMMANDS,
40
+ skills,
41
+ presets: PHASE_PRESETS,
42
+ });
43
+ const changed = existing !== nextContent;
44
+
45
+ if (!input.dryRun && changed) {
46
+ await this.documentPort.writeProjectFile(CLAUDE_MD_PATH, nextContent);
47
+ }
48
+
49
+ return {
50
+ success: true,
51
+ path: CLAUDE_MD_PATH,
52
+ changed,
53
+ applied: !input.dryRun && changed,
54
+ preview: nextContent,
55
+ errors: [],
56
+ };
57
+ } catch (error) {
58
+ return {
59
+ success: false,
60
+ path: CLAUDE_MD_PATH,
61
+ changed: false,
62
+ applied: false,
63
+ preview: '',
64
+ errors: [
65
+ {
66
+ code: 'AGENT_CONTEXT_CLAUDE_REFRESH_FAILED',
67
+ message: error instanceof Error ? error.message : String(error),
68
+ },
69
+ ],
70
+ };
71
+ }
72
+ }
73
+ }
@@ -9,6 +9,7 @@ import { TemplateGenerator } from './domain/services/template-generator.js';
9
9
  import { RepetitionDetector } from './domain/services/repetition-detector.js';
10
10
  import { PointerValidator } from './domain/services/pointer-validator.js';
11
11
  import { LessonAggregator } from './domain/services/lesson-aggregator.js';
12
+ import { ClaudeMdComposer } from './domain/services/claude-md-composer.js';
12
13
 
13
14
  import { GenerateCiTemplateUseCase } from './application/usecases/generate-ci-template-usecase.js';
14
15
  import { RenderCiTemplateUseCase } from './application/usecases/render-ci-template-usecase.js';
@@ -16,6 +17,9 @@ import { RecordErrorOccurrenceUseCase } from './application/usecases/record-erro
16
17
  import { CheckEscalationUseCase } from './application/usecases/check-escalation-usecase.js';
17
18
  import { ResetRepetitionUseCase } from './application/usecases/reset-repetition-usecase.js';
18
19
  import { MigrateAgentsMdUseCase } from './application/usecases/migrate-agents-md-usecase.js';
20
+ import { RefreshAgentContextUseCase } from './application/usecases/refresh-agent-context-usecase.js';
21
+ import { RefreshClaudeMdUseCase } from './application/usecases/refresh-claude-md-usecase.js';
22
+ import { CheckAgentContextUseCase } from './application/usecases/check-agent-context-usecase.js';
19
23
  import { AggregateLessonsUseCase } from './application/usecases/aggregate-lessons-usecase.js';
20
24
  import { ValidatePointersUseCase } from './application/usecases/validate-pointers-usecase.js';
21
25
 
@@ -26,12 +30,16 @@ import { EscalationLogExecutorAdapter } from './infrastructure/adapters/escalati
26
30
  import { YamlTemplateRendererAdapter } from './infrastructure/adapters/yaml-template-renderer-adapter.js';
27
31
  import { FileSystemExistenceAdapter } from './infrastructure/adapters/file-system-existence-adapter.js';
28
32
  import { AgentsMdFileAdapter } from './infrastructure/adapters/agents-md-file-adapter.js';
33
+ import { AgentContextFileAdapter } from './infrastructure/adapters/agent-context-file-adapter.js';
29
34
  import { LessonArtifactFileReaderAdapter } from './infrastructure/adapters/lesson-artifact-file-reader-adapter.js';
30
35
  import { HarnessApiCommandExistenceAdapter } from './infrastructure/adapters/harness-api-command-existence-adapter.js';
31
36
  import { AdrFoundationExistenceAdapter } from './infrastructure/adapters/adr-foundation-existence-adapter.js';
32
37
 
33
38
  import { GenerateCiTemplateHandler } from './presentation/handlers/generate-ci-template-handler.js';
34
39
  import { MigrateAgentsMdHandler } from './presentation/handlers/migrate-agents-md-handler.js';
40
+ import { RefreshAgentContextHandler } from './presentation/handlers/refresh-agent-context-handler.js';
41
+ import { RefreshClaudeMdHandler } from './presentation/handlers/refresh-claude-md-handler.js';
42
+ import { CheckAgentContextHandler } from './presentation/handlers/check-agent-context-handler.js';
35
43
  import { CheckRepetitionHandler } from './presentation/handlers/check-repetition-handler.js';
36
44
  import { CreateBaselineHandler } from './presentation/handlers/create-baseline-handler.js';
37
45
 
@@ -48,6 +56,9 @@ import { ScaffoldDesignHandler } from './presentation/handlers/scaffold-design-h
48
56
  export interface CiGovernanceCompositionRoot {
49
57
  generateCiTemplateHandler: GenerateCiTemplateHandler;
50
58
  migrateAgentsMdHandler: MigrateAgentsMdHandler;
59
+ refreshAgentContextHandler: RefreshAgentContextHandler;
60
+ refreshClaudeMdHandler: RefreshClaudeMdHandler;
61
+ checkAgentContextHandler: CheckAgentContextHandler;
51
62
  checkRepetitionHandler: CheckRepetitionHandler;
52
63
  createBaselineHandler: CreateBaselineHandler;
53
64
  scaffoldDesignHandler: ScaffoldDesignHandler;
@@ -70,11 +81,12 @@ export function buildCiGovernance(
70
81
  const presetConfigAdapter = new PresetConfigAdapter();
71
82
  const errorRepetitionRepository = new ErrorRepetitionJsonRepository(baseDir);
72
83
  const escalationExecutorPort = new EscalationLogExecutorAdapter();
73
- const templateRendererPort = new YamlTemplateRendererAdapter();
84
+ const templateRendererPort = new YamlTemplateRendererAdapter(harnessRoot);
74
85
  const fileExistencePort = new FileSystemExistenceAdapter(baseDir);
75
86
  const commandExistencePort = new HarnessApiCommandExistenceAdapter();
76
87
  const adrExistencePort = new AdrFoundationExistenceAdapter();
77
88
  const agentsMdPort = new AgentsMdFileAdapter(baseDir);
89
+ const agentContextDocumentPort = new AgentContextFileAdapter(baseDir, harnessRoot);
78
90
  const lessonArtifactReaderPort = new LessonArtifactFileReaderAdapter(baseDir);
79
91
 
80
92
  // Domain services
@@ -82,6 +94,7 @@ export function buildCiGovernance(
82
94
  const repetitionDetector = new RepetitionDetector(errorRepetitionRepository);
83
95
  const pointerValidator = new PointerValidator(commandExistencePort, fileExistencePort, adrExistencePort);
84
96
  const lessonAggregator = new LessonAggregator();
97
+ const claudeMdComposer = new ClaudeMdComposer();
85
98
 
86
99
  // Use cases
87
100
  const generateCiTemplateUseCase = new GenerateCiTemplateUseCase(templateGenerator);
@@ -95,6 +108,9 @@ export function buildCiGovernance(
95
108
  lessonAggregator,
96
109
  pointerValidator,
97
110
  );
111
+ const refreshClaudeMdUseCase = new RefreshClaudeMdUseCase(agentContextDocumentPort, claudeMdComposer);
112
+ const refreshAgentContextUseCase = new RefreshAgentContextUseCase(migrateAgentsMdUseCase, refreshClaudeMdUseCase);
113
+ const checkAgentContextUseCase = new CheckAgentContextUseCase(agentContextDocumentPort);
98
114
  const aggregateLessonsUseCase = new AggregateLessonsUseCase(lessonArtifactReaderPort, lessonAggregator);
99
115
  const validatePointersUseCase = new ValidatePointersUseCase(agentsMdPort, pointerValidator);
100
116
 
@@ -122,6 +138,9 @@ export function buildCiGovernance(
122
138
  renderCiTemplateUseCase,
123
139
  );
124
140
  const migrateAgentsMdHandler = new MigrateAgentsMdHandler(migrateAgentsMdUseCase);
141
+ const refreshAgentContextHandler = new RefreshAgentContextHandler(refreshAgentContextUseCase);
142
+ const refreshClaudeMdHandler = new RefreshClaudeMdHandler(refreshClaudeMdUseCase);
143
+ const checkAgentContextHandler = new CheckAgentContextHandler(checkAgentContextUseCase);
125
144
  const checkRepetitionHandler = new CheckRepetitionHandler(checkEscalationUseCase);
126
145
  const createBaselineHandler = new CreateBaselineHandler(createBaselineUseCase);
127
146
  const scaffoldDesignHandler = new ScaffoldDesignHandler(scaffoldDesignUseCase);
@@ -129,6 +148,9 @@ export function buildCiGovernance(
129
148
  return {
130
149
  generateCiTemplateHandler,
131
150
  migrateAgentsMdHandler,
151
+ refreshAgentContextHandler,
152
+ refreshClaudeMdHandler,
153
+ checkAgentContextHandler,
132
154
  checkRepetitionHandler,
133
155
  createBaselineHandler,
134
156
  scaffoldDesignHandler,
@@ -29,7 +29,7 @@ export class CiTemplate {
29
29
  if (!isTemplateType(templateType)) {
30
30
  throw new CiGovernanceDomainError(
31
31
  'CI_TEMPLATE_INVALID_TYPE',
32
- `INV-1: templateType must be one of 'aidlc-gate', 'consistency-check', 'pre-commit'. Got: ${templateType}`,
32
+ `INV-1: templateType must be one of 'aidlc-gate', 'consistency-check', 'pre-commit', 'agent-context-refresh'. Got: ${templateType}`,
33
33
  );
34
34
  }
35
35
  if (!presetRef || presetRef.trim() === '') {
@@ -0,0 +1,17 @@
1
+ /**
2
+ * @layer domain
3
+ * @unit ci-governance
4
+ */
5
+
6
+ export interface AgentContextDocumentStat {
7
+ readonly exists: boolean;
8
+ readonly ageInDays: number | null;
9
+ }
10
+
11
+ export interface AgentContextDocumentPort {
12
+ readProjectFile(relativePath: string): Promise<string | null>;
13
+ writeProjectFile(relativePath: string, content: string): Promise<void>;
14
+ readHarnessTemplate(relativePath: string): Promise<string>;
15
+ statProjectFile(relativePath: string): Promise<AgentContextDocumentStat>;
16
+ listHarnessSkillNames(): Promise<string[]>;
17
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * @layer domain
3
+ * @unit ci-governance
4
+ */
5
+
6
+ export interface ClaudeMdTemplateValues {
7
+ readonly commands: readonly string[];
8
+ readonly skills: readonly string[];
9
+ readonly presets: readonly string[];
10
+ }
11
+
12
+ const USER_SECTION_START = '<!-- phasegate:user-section:start -->';
13
+ const USER_SECTION_END = '<!-- phasegate:user-section:end -->';
14
+ const DEFAULT_USER_SECTION = 'プロジェクト固有の指示をここに記載してください。';
15
+
16
+ export class ClaudeMdComposer {
17
+ compose(template: string, existing: string | null, values: ClaudeMdTemplateValues): string {
18
+ const userSection = this.extractUserSection(existing) ?? DEFAULT_USER_SECTION;
19
+ return template
20
+ .replace('{{PHASEGATE_COMMANDS}}', this.toList(values.commands))
21
+ .replace('{{PHASEGATE_SKILLS}}', this.toList(values.skills))
22
+ .replace('{{PHASEGATE_PRESETS}}', this.toList(values.presets))
23
+ .replace('{{PHASEGATE_USER_SECTION}}', userSection)
24
+ .replace(/\n{3,}/g, '\n\n')
25
+ .trimEnd() + '\n';
26
+ }
27
+
28
+ private extractUserSection(existing: string | null): string | null {
29
+ if (existing === null) return null;
30
+ const start = existing.indexOf(USER_SECTION_START);
31
+ const end = existing.indexOf(USER_SECTION_END);
32
+ if (start === -1 || end === -1 || end < start) return existing.trim();
33
+ return existing.slice(start + USER_SECTION_START.length, end).trim();
34
+ }
35
+
36
+ private toList(values: readonly string[]): string {
37
+ return values.map((value) => `- \`${value}\``).join('\n');
38
+ }
39
+ }
@@ -17,6 +17,7 @@ const TRIGGER_CONDITION_MAP: Record<TemplateType, TriggerCondition> = {
17
17
  'aidlc-gate': 'pull_request',
18
18
  'consistency-check': 'schedule',
19
19
  'pre-commit': 'pre-commit',
20
+ 'agent-context-refresh': 'schedule',
20
21
  };
21
22
 
22
23
  export class TemplateGenerator {
@@ -1,12 +1,13 @@
1
1
  // @unit ci-governance
2
2
  // @layer domain
3
3
 
4
- export type TemplateType = 'aidlc-gate' | 'consistency-check' | 'pre-commit';
4
+ export type TemplateType = 'aidlc-gate' | 'consistency-check' | 'pre-commit' | 'agent-context-refresh';
5
5
 
6
6
  export const TEMPLATE_TYPES: readonly TemplateType[] = [
7
7
  'aidlc-gate',
8
8
  'consistency-check',
9
9
  'pre-commit',
10
+ 'agent-context-refresh',
10
11
  ] as const;
11
12
 
12
13
  export function isTemplateType(value: unknown): value is TemplateType {
@@ -0,0 +1,55 @@
1
+ /**
2
+ * @layer infrastructure
3
+ * @unit ci-governance
4
+ */
5
+
6
+ import { mkdir, readdir, readFile, stat, writeFile } from 'node:fs/promises';
7
+ import { dirname, join } from 'node:path';
8
+ import type { AgentContextDocumentPort, AgentContextDocumentStat } from '../../domain/ports/agent-context-document-port.js';
9
+
10
+ export class AgentContextFileAdapter implements AgentContextDocumentPort {
11
+ constructor(
12
+ private readonly projectRoot: string,
13
+ private readonly harnessRoot: string,
14
+ ) {}
15
+
16
+ async readProjectFile(relativePath: string): Promise<string | null> {
17
+ try {
18
+ return await readFile(join(this.projectRoot, relativePath), 'utf-8');
19
+ } catch {
20
+ return null;
21
+ }
22
+ }
23
+
24
+ async writeProjectFile(relativePath: string, content: string): Promise<void> {
25
+ const targetPath = join(this.projectRoot, relativePath);
26
+ await mkdir(dirname(targetPath), { recursive: true });
27
+ await writeFile(targetPath, content, 'utf-8');
28
+ }
29
+
30
+ async readHarnessTemplate(relativePath: string): Promise<string> {
31
+ return await readFile(join(this.harnessRoot, relativePath), 'utf-8');
32
+ }
33
+
34
+ async statProjectFile(relativePath: string): Promise<AgentContextDocumentStat> {
35
+ try {
36
+ const fileStat = await stat(join(this.projectRoot, relativePath));
37
+ const ageInDays = Math.floor((Date.now() - fileStat.mtime.getTime()) / 86_400_000);
38
+ return { exists: true, ageInDays };
39
+ } catch {
40
+ return { exists: false, ageInDays: null };
41
+ }
42
+ }
43
+
44
+ async listHarnessSkillNames(): Promise<string[]> {
45
+ try {
46
+ const entries = await readdir(join(this.harnessRoot, 'skills'), { withFileTypes: true });
47
+ return entries
48
+ .filter((entry) => entry.isDirectory())
49
+ .map((entry) => entry.name)
50
+ .sort();
51
+ } catch {
52
+ return [];
53
+ }
54
+ }
55
+ }
@@ -5,6 +5,8 @@
5
5
  * TemplateRendererPort実装(YAML書き出し)
6
6
  */
7
7
 
8
+ import { readFile } from 'node:fs/promises';
9
+ import { join } from 'node:path';
8
10
  import type { TemplateRendererPort, TemplateRenderOutput } from '../../domain/ports/template-renderer-port.js';
9
11
  import type { CiTemplate } from '../../domain/aggregates/ci-template.js';
10
12
 
@@ -12,42 +14,25 @@ const OUTPUT_PATH_MAP: Record<string, string> = {
12
14
  'aidlc-gate': '.github/workflows/aidlc-gate.yml',
13
15
  'consistency-check': '.github/workflows/consistency-check.yml',
14
16
  'pre-commit': '.husky/pre-commit',
17
+ 'agent-context-refresh': '.github/workflows/agent-context-refresh.yml',
18
+ };
19
+
20
+ const TEMPLATE_PATH_MAP: Record<string, string> = {
21
+ 'aidlc-gate': 'docs/templates/ci/aidlc-gate.yml',
22
+ 'consistency-check': 'docs/templates/ci/consistency-check.yml',
23
+ 'pre-commit': 'docs/templates/hooks/pre-commit',
24
+ 'agent-context-refresh': 'docs/templates/ci/agent-context-refresh.yml',
15
25
  };
16
26
 
17
27
  export class YamlTemplateRendererAdapter implements TemplateRendererPort {
28
+ constructor(private readonly harnessRoot: string = process.cwd()) {}
29
+
18
30
  async render(ciTemplate: CiTemplate): Promise<TemplateRenderOutput> {
19
31
  const outputPath = OUTPUT_PATH_MAP[ciTemplate.templateType] ?? '';
20
- const content = this.generateContent(ciTemplate);
32
+ const templatePath = TEMPLATE_PATH_MAP[ciTemplate.templateType];
33
+ const content = templatePath === undefined
34
+ ? `# ${ciTemplate.templateType} template (not configured)`
35
+ : await readFile(join(this.harnessRoot, templatePath), 'utf-8');
21
36
  return { outputPath, content };
22
37
  }
23
-
24
- private generateContent(ciTemplate: CiTemplate): string {
25
- const { templateType, config } = ciTemplate;
26
-
27
- if (!config) {
28
- return `# ${templateType} template (not configured)`;
29
- }
30
-
31
- if (templateType === 'pre-commit') {
32
- return [
33
- '#!/bin/sh',
34
- '. "$(dirname "$0")/_/husky.sh"',
35
- '',
36
- `npx phasegate lint --validators ${config.targetValidatorIds.join(',')}`,
37
- ].join('\n');
38
- }
39
-
40
- return [
41
- `name: ${templateType}`,
42
- `on:`,
43
- ` ${config.triggerCondition === 'pull_request' ? 'pull_request' : 'schedule'}:`,
44
- config.triggerCondition === 'schedule' ? ' - cron: "0 2 * * *"' : '',
45
- `jobs:`,
46
- ` validate:`,
47
- ` runs-on: ubuntu-latest`,
48
- ` steps:`,
49
- ` - uses: actions/checkout@v4`,
50
- ` - run: npx phasegate lint --validators ${config.targetValidatorIds.join(',')}`,
51
- ].filter((l) => l !== '').join('\n');
52
- }
53
38
  }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * @layer presentation
3
+ * @unit ci-governance
4
+ */
5
+
6
+ import type { CheckAgentContextUseCase } from '../../application/usecases/check-agent-context-usecase.js';
7
+
8
+ export interface CheckAgentContextHandlerArgs {
9
+ thresholdDays?: number;
10
+ format?: 'human' | 'json';
11
+ }
12
+
13
+ export class CheckAgentContextHandler {
14
+ constructor(private readonly useCase: CheckAgentContextUseCase) {}
15
+
16
+ async handle(args: CheckAgentContextHandlerArgs): Promise<{ exitCode: number; output: string }> {
17
+ const result = await this.useCase.execute({ thresholdDays: args.thresholdDays });
18
+
19
+ if (args.format === 'json') {
20
+ return { exitCode: result.passed ? 0 : 1, output: JSON.stringify(result, null, 2) };
21
+ }
22
+
23
+ const lines = [
24
+ `Agent context freshness: ${result.passed ? 'PASS' : 'FAIL'}`,
25
+ `Threshold: ${result.thresholdDays} days`,
26
+ ...result.findings.map((finding) => {
27
+ const age = finding.ageInDays === null ? 'missing' : `${finding.ageInDays} days`;
28
+ return `- ${finding.path}: ${finding.status} (${age}) ${finding.message}`;
29
+ }),
30
+ ];
31
+ return { exitCode: result.passed ? 0 : 1, output: lines.join('\n') };
32
+ }
33
+ }
@@ -31,7 +31,25 @@ export class GenerateCiTemplateHandler {
31
31
  ) {}
32
32
 
33
33
  async handle(args: GenerateCiTemplateHandlerArgs): Promise<GenerateCiTemplateHandlerResult> {
34
- const { presetId, templateType, format = 'human' } = args;
34
+ const { presetId, templateType, render = false, format = 'human' } = args;
35
+
36
+ if (render) {
37
+ const result = await this.renderUseCase.execute({
38
+ presetId,
39
+ templateType: templateType as TemplateType,
40
+ });
41
+ const hasErrors = result.errors.length > 0;
42
+ const output = format === 'json'
43
+ ? JSON.stringify(result, null, 2)
44
+ : hasErrors
45
+ ? result.errors.map((err) => `[${err.code}] ${err.message}`).join('\n')
46
+ : result.content;
47
+
48
+ return {
49
+ exitCode: hasErrors ? 1 : 0,
50
+ output,
51
+ };
52
+ }
35
53
 
36
54
  const result = await this.generateUseCase.execute({
37
55
  presetId,
@@ -0,0 +1,44 @@
1
+ /**
2
+ * @layer presentation
3
+ * @unit ci-governance
4
+ */
5
+
6
+ import type { RefreshAgentContextUseCase } from '../../application/usecases/refresh-agent-context-usecase.js';
7
+
8
+ export interface RefreshAgentContextHandlerArgs {
9
+ dryRun?: boolean;
10
+ apply?: boolean;
11
+ format?: 'human' | 'json';
12
+ }
13
+
14
+ export class RefreshAgentContextHandler {
15
+ constructor(private readonly useCase: RefreshAgentContextUseCase) {}
16
+
17
+ async handle(args: RefreshAgentContextHandlerArgs): Promise<{ exitCode: number; output: string }> {
18
+ const dryRun = args.apply === true ? false : (args.dryRun ?? true);
19
+ const result = await this.useCase.execute({ dryRun });
20
+
21
+ if (args.format === 'json') {
22
+ return { exitCode: result.success ? 0 : 1, output: JSON.stringify(result, null, 2) };
23
+ }
24
+
25
+ if (!result.success) {
26
+ return {
27
+ exitCode: 1,
28
+ output: ['Agent context refresh failed', ...result.errors.map((error) => `[${error.code}] ${error.message}`)].join('\n'),
29
+ };
30
+ }
31
+
32
+ const mode = dryRun ? 'dry-run' : 'apply';
33
+ const preview = dryRun ? `\n\nCLAUDE.md preview:\n${result.claudeMd.preview}` : '';
34
+ return {
35
+ exitCode: 0,
36
+ output: [
37
+ `Agent context refresh ${mode} complete`,
38
+ `AGENTS.md added pointers: ${result.agentsMd.addedPointers}`,
39
+ `CLAUDE.md changed: ${result.claudeMd.changed}`,
40
+ `Applied: ${result.applied}`,
41
+ ].join('\n') + preview,
42
+ };
43
+ }
44
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * @layer presentation
3
+ * @unit ci-governance
4
+ */
5
+
6
+ import type { RefreshClaudeMdUseCase } from '../../application/usecases/refresh-claude-md-usecase.js';
7
+
8
+ export interface RefreshClaudeMdHandlerArgs {
9
+ dryRun?: boolean;
10
+ apply?: boolean;
11
+ format?: 'human' | 'json';
12
+ }
13
+
14
+ export class RefreshClaudeMdHandler {
15
+ constructor(private readonly useCase: RefreshClaudeMdUseCase) {}
16
+
17
+ async handle(args: RefreshClaudeMdHandlerArgs): Promise<{ exitCode: number; output: string }> {
18
+ const dryRun = args.apply === true ? false : (args.dryRun ?? true);
19
+ const result = await this.useCase.execute({ dryRun });
20
+
21
+ if (args.format === 'json') {
22
+ return { exitCode: result.success ? 0 : 1, output: JSON.stringify(result, null, 2) };
23
+ }
24
+
25
+ if (!result.success) {
26
+ return {
27
+ exitCode: 1,
28
+ output: ['CLAUDE.md refresh failed', ...result.errors.map((error) => `[${error.code}] ${error.message}`)].join('\n'),
29
+ };
30
+ }
31
+
32
+ const mode = dryRun ? 'dry-run' : 'apply';
33
+ return {
34
+ exitCode: 0,
35
+ output: `CLAUDE.md refresh ${mode}: changed=${result.changed}, applied=${result.applied}`,
36
+ };
37
+ }
38
+ }