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.
- package/CHANGELOG.md +26 -0
- package/README.ja.md +7 -5
- package/README.md +7 -4
- package/docs/guide/cli-reference.md +3 -0
- package/docs/templates/agent-context/CLAUDE.md.template.md +29 -0
- package/docs/templates/ci/agent-context-refresh.yml +59 -0
- package/docs/templates/ci/aidlc-gate.yml +97 -0
- package/docs/templates/ci/consistency-check.yml +117 -0
- package/docs/templates/hooks/commit-msg +13 -0
- package/docs/templates/hooks/pre-commit +62 -0
- package/package.json +2 -1
- package/scripts/harness/ci-governance/application/dto/check-agent-context-input.ts +8 -0
- package/scripts/harness/ci-governance/application/dto/check-agent-context-output.ts +17 -0
- package/scripts/harness/ci-governance/application/dto/refresh-agent-context-input.ts +8 -0
- package/scripts/harness/ci-governance/application/dto/refresh-agent-context-output.ts +15 -0
- package/scripts/harness/ci-governance/application/dto/refresh-claude-md-input.ts +8 -0
- package/scripts/harness/ci-governance/application/dto/refresh-claude-md-output.ts +13 -0
- package/scripts/harness/ci-governance/application/usecases/check-agent-context-usecase.ts +48 -0
- package/scripts/harness/ci-governance/application/usecases/refresh-agent-context-usecase.ts +31 -0
- package/scripts/harness/ci-governance/application/usecases/refresh-claude-md-usecase.ts +73 -0
- package/scripts/harness/ci-governance/composition-root.ts +23 -1
- package/scripts/harness/ci-governance/domain/aggregates/ci-template.ts +1 -1
- package/scripts/harness/ci-governance/domain/ports/agent-context-document-port.ts +17 -0
- package/scripts/harness/ci-governance/domain/services/claude-md-composer.ts +39 -0
- package/scripts/harness/ci-governance/domain/services/template-generator.ts +1 -0
- package/scripts/harness/ci-governance/domain/types/template-type.ts +2 -1
- package/scripts/harness/ci-governance/infrastructure/adapters/agent-context-file-adapter.ts +55 -0
- package/scripts/harness/ci-governance/infrastructure/adapters/yaml-template-renderer-adapter.ts +16 -31
- package/scripts/harness/ci-governance/presentation/handlers/check-agent-context-handler.ts +33 -0
- package/scripts/harness/ci-governance/presentation/handlers/generate-ci-template-handler.ts +19 -1
- package/scripts/harness/ci-governance/presentation/handlers/refresh-agent-context-handler.ts +44 -0
- package/scripts/harness/ci-governance/presentation/handlers/refresh-claude-md-handler.ts +38 -0
- package/scripts/harness/main.ts +57 -8
- package/scripts/harness/setup/skill-deployer.ts +52 -2
- package/scripts/harness/traceability-model/domain/value-objects/work-item-frontmatter.ts +3 -1
- 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
|
+
}
|
|
@@ -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
|
+
}
|
package/scripts/harness/ci-governance/infrastructure/adapters/yaml-template-renderer-adapter.ts
CHANGED
|
@@ -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
|
|
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
|
+
}
|