takt: update-report-path-variable
This commit is contained in:
parent
f7181fc00c
commit
79227dffd1
4
package-lock.json
generated
4
package-lock.json
generated
@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "takt",
|
||||
"version": "0.2.5",
|
||||
"version": "0.2.3",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "takt",
|
||||
"version": "0.2.5",
|
||||
"version": "0.2.3",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/claude-agent-sdk": "^0.2.19",
|
||||
|
||||
@ -374,8 +374,8 @@ describe('instruction-builder', () => {
|
||||
|
||||
const result = buildInstruction(step, context);
|
||||
|
||||
expect(result).toContain('- Report Directory: .takt/reports/20260129-test/');
|
||||
expect(result).toContain('- Report File: .takt/reports/20260129-test/00-plan.md');
|
||||
expect(result).toContain('- Report Directory: 20260129-test/');
|
||||
expect(result).toContain('- Report File: 20260129-test/00-plan.md');
|
||||
expect(result).not.toContain('Report Files:');
|
||||
});
|
||||
|
||||
@ -393,13 +393,29 @@ describe('instruction-builder', () => {
|
||||
|
||||
const result = buildInstruction(step, context);
|
||||
|
||||
expect(result).toContain('- Report Directory: .takt/reports/20260129-test/');
|
||||
expect(result).toContain('- Report Directory: 20260129-test/');
|
||||
expect(result).toContain('- Report Files:');
|
||||
expect(result).toContain(' - Scope: .takt/reports/20260129-test/01-scope.md');
|
||||
expect(result).toContain(' - Decisions: .takt/reports/20260129-test/02-decisions.md');
|
||||
expect(result).toContain(' - Scope: 20260129-test/01-scope.md');
|
||||
expect(result).toContain(' - Decisions: 20260129-test/02-decisions.md');
|
||||
expect(result).not.toContain('Report File:');
|
||||
});
|
||||
|
||||
it('should include report file when report is ReportObjectConfig', () => {
|
||||
const step = createMinimalStep('Do work');
|
||||
step.name = 'plan';
|
||||
step.report = { name: '00-plan.md' };
|
||||
const context = createMinimalContext({
|
||||
reportDir: '20260129-test',
|
||||
language: 'en',
|
||||
});
|
||||
|
||||
const result = buildInstruction(step, context);
|
||||
|
||||
expect(result).toContain('- Report Directory: 20260129-test/');
|
||||
expect(result).toContain('- Report File: 20260129-test/00-plan.md');
|
||||
expect(result).not.toContain('Report Files:');
|
||||
});
|
||||
|
||||
it('should NOT include report info when reportDir is undefined', () => {
|
||||
const step = createMinimalStep('Do work');
|
||||
step.report = '00-plan.md';
|
||||
@ -437,6 +453,164 @@ describe('instruction-builder', () => {
|
||||
|
||||
expect(result).toContain('- Step Iteration: 3(このステップの実行回数)');
|
||||
});
|
||||
|
||||
it('should NOT include .takt/reports/ prefix in report paths', () => {
|
||||
const step = createMinimalStep('Do work');
|
||||
step.report = '00-plan.md';
|
||||
const context = createMinimalContext({
|
||||
reportDir: '20260129-test',
|
||||
language: 'en',
|
||||
});
|
||||
|
||||
const result = buildInstruction(step, context);
|
||||
|
||||
expect(result).not.toContain('.takt/reports/');
|
||||
});
|
||||
});
|
||||
|
||||
describe('ReportObjectConfig order/format injection', () => {
|
||||
it('should inject order before instruction_template', () => {
|
||||
const step = createMinimalStep('Do work');
|
||||
step.report = {
|
||||
name: '00-plan.md',
|
||||
order: '**Output:** Write to {report:00-plan.md}',
|
||||
};
|
||||
const context = createMinimalContext({
|
||||
reportDir: '20260129-test',
|
||||
language: 'en',
|
||||
});
|
||||
|
||||
const result = buildInstruction(step, context);
|
||||
|
||||
const orderIdx = result.indexOf('**Output:** Write to 20260129-test/00-plan.md');
|
||||
const instructionsIdx = result.indexOf('## Instructions');
|
||||
expect(orderIdx).toBeGreaterThan(-1);
|
||||
expect(instructionsIdx).toBeGreaterThan(orderIdx);
|
||||
});
|
||||
|
||||
it('should inject format after instruction_template', () => {
|
||||
const step = createMinimalStep('Do work');
|
||||
step.report = {
|
||||
name: '00-plan.md',
|
||||
format: '**Format:**\n```markdown\n# Plan\n```',
|
||||
};
|
||||
const context = createMinimalContext({
|
||||
reportDir: '20260129-test',
|
||||
language: 'en',
|
||||
});
|
||||
|
||||
const result = buildInstruction(step, context);
|
||||
|
||||
const instructionsIdx = result.indexOf('## Instructions');
|
||||
const formatIdx = result.indexOf('**Format:**');
|
||||
expect(formatIdx).toBeGreaterThan(instructionsIdx);
|
||||
});
|
||||
|
||||
it('should inject both order before and format after instruction_template', () => {
|
||||
const step = createMinimalStep('Do work');
|
||||
step.report = {
|
||||
name: '00-plan.md',
|
||||
order: '**Output:** Write to {report:00-plan.md}',
|
||||
format: '**Format:**\n```markdown\n# Plan\n```',
|
||||
};
|
||||
const context = createMinimalContext({
|
||||
reportDir: '20260129-test',
|
||||
language: 'en',
|
||||
});
|
||||
|
||||
const result = buildInstruction(step, context);
|
||||
|
||||
const orderIdx = result.indexOf('**Output:** Write to 20260129-test/00-plan.md');
|
||||
const instructionsIdx = result.indexOf('## Instructions');
|
||||
const formatIdx = result.indexOf('**Format:**');
|
||||
expect(orderIdx).toBeGreaterThan(-1);
|
||||
expect(instructionsIdx).toBeGreaterThan(orderIdx);
|
||||
expect(formatIdx).toBeGreaterThan(instructionsIdx);
|
||||
});
|
||||
|
||||
it('should replace {report:filename} in order text', () => {
|
||||
const step = createMinimalStep('Do work');
|
||||
step.report = {
|
||||
name: '00-plan.md',
|
||||
order: 'Output to {report:00-plan.md} file.',
|
||||
};
|
||||
const context = createMinimalContext({
|
||||
reportDir: '20260129-test',
|
||||
language: 'en',
|
||||
});
|
||||
|
||||
const result = buildInstruction(step, context);
|
||||
|
||||
expect(result).toContain('Output to 20260129-test/00-plan.md file.');
|
||||
expect(result).not.toContain('{report:00-plan.md}');
|
||||
});
|
||||
|
||||
it('should not inject order/format when report is a simple string', () => {
|
||||
const step = createMinimalStep('Do work');
|
||||
step.report = '00-plan.md';
|
||||
const context = createMinimalContext({
|
||||
reportDir: '20260129-test',
|
||||
language: 'en',
|
||||
});
|
||||
|
||||
const result = buildInstruction(step, context);
|
||||
|
||||
// Should contain instructions normally
|
||||
expect(result).toContain('## Instructions');
|
||||
expect(result).toContain('Do work');
|
||||
// The instruction should appear right after Additional User Inputs, not after any order section
|
||||
const additionalIdx = result.indexOf('## Additional User Inputs');
|
||||
const instructionsIdx = result.indexOf('## Instructions');
|
||||
expect(additionalIdx).toBeGreaterThan(-1);
|
||||
expect(instructionsIdx).toBeGreaterThan(additionalIdx);
|
||||
});
|
||||
|
||||
it('should not inject order/format when report is ReportConfig[]', () => {
|
||||
const step = createMinimalStep('Do work');
|
||||
step.report = [
|
||||
{ label: 'Scope', path: '01-scope.md' },
|
||||
];
|
||||
const context = createMinimalContext({
|
||||
reportDir: '20260129-test',
|
||||
language: 'en',
|
||||
});
|
||||
|
||||
const result = buildInstruction(step, context);
|
||||
|
||||
// Just verify normal behavior without extra sections
|
||||
expect(result).toContain('## Instructions');
|
||||
expect(result).toContain('Do work');
|
||||
});
|
||||
|
||||
it('should replace {report:filename} in instruction_template too', () => {
|
||||
const step = createMinimalStep('Write to {report:00-plan.md}');
|
||||
const context = createMinimalContext({
|
||||
reportDir: '20260129-test',
|
||||
language: 'en',
|
||||
});
|
||||
|
||||
const result = buildInstruction(step, context);
|
||||
|
||||
expect(result).toContain('Write to 20260129-test/00-plan.md');
|
||||
expect(result).not.toContain('{report:00-plan.md}');
|
||||
});
|
||||
|
||||
it('should replace {step_iteration} in order/format text', () => {
|
||||
const step = createMinimalStep('Do work');
|
||||
step.report = {
|
||||
name: '00-plan.md',
|
||||
order: 'Append ## Iteration {step_iteration} section',
|
||||
};
|
||||
const context = createMinimalContext({
|
||||
reportDir: '20260129-test',
|
||||
stepIteration: 3,
|
||||
language: 'en',
|
||||
});
|
||||
|
||||
const result = buildInstruction(step, context);
|
||||
|
||||
expect(result).toContain('Append ## Iteration 3 section');
|
||||
});
|
||||
});
|
||||
|
||||
describe('auto-injected User Request and Additional User Inputs sections', () => {
|
||||
|
||||
@ -8,7 +8,7 @@ import { readFileSync, existsSync, readdirSync, statSync } from 'node:fs';
|
||||
import { join, dirname, basename } from 'node:path';
|
||||
import { parse as parseYaml } from 'yaml';
|
||||
import { WorkflowConfigRawSchema } from '../models/schemas.js';
|
||||
import type { WorkflowConfig, WorkflowStep, WorkflowRule, ReportConfig } from '../models/types.js';
|
||||
import type { WorkflowConfig, WorkflowStep, WorkflowRule, ReportConfig, ReportObjectConfig } from '../models/types.js';
|
||||
import { getGlobalWorkflowsDir } from './paths.js';
|
||||
|
||||
/** Get builtin workflow by name */
|
||||
@ -54,6 +54,13 @@ function extractAgentDisplayName(agentPath: string): string {
|
||||
return filename;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a raw report value is the object form (has 'name' property).
|
||||
*/
|
||||
function isReportObject(raw: unknown): raw is { name: string; order?: string; format?: string } {
|
||||
return typeof raw === 'object' && raw !== null && !Array.isArray(raw) && 'name' in raw;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize the raw report field from YAML into internal format.
|
||||
*
|
||||
@ -62,16 +69,23 @@ function extractAgentDisplayName(agentPath: string): string {
|
||||
* report: → ReportConfig[] (multiple files)
|
||||
* - Scope: 01-scope.md
|
||||
* - Decisions: 02-decisions.md
|
||||
* report: → ReportObjectConfig (object form)
|
||||
* name: 00-plan.md
|
||||
* order: ...
|
||||
* format: ...
|
||||
*
|
||||
* Array items are parsed as single-key objects: [{Scope: "01-scope.md"}, ...]
|
||||
*/
|
||||
function normalizeReport(
|
||||
raw: string | Record<string, string>[] | undefined,
|
||||
): string | ReportConfig[] | undefined {
|
||||
raw: string | Record<string, string>[] | { name: string; order?: string; format?: string } | undefined,
|
||||
): string | ReportConfig[] | ReportObjectConfig | undefined {
|
||||
if (raw == null) return undefined;
|
||||
if (typeof raw === 'string') return raw;
|
||||
if (isReportObject(raw)) {
|
||||
return { name: raw.name, order: raw.order, format: raw.format };
|
||||
}
|
||||
// Convert [{Scope: "01-scope.md"}, ...] to [{label: "Scope", path: "01-scope.md"}, ...]
|
||||
return raw.flatMap((entry) =>
|
||||
return (raw as Record<string, string>[]).flatMap((entry) =>
|
||||
Object.entries(entry).map(([label, path]) => ({ label, path })),
|
||||
);
|
||||
}
|
||||
|
||||
@ -3,6 +3,7 @@ export type {
|
||||
AgentType,
|
||||
Status,
|
||||
ReportConfig,
|
||||
ReportObjectConfig,
|
||||
AgentResponse,
|
||||
SessionState,
|
||||
WorkflowStep,
|
||||
|
||||
@ -27,20 +27,48 @@ export const StatusSchema = z.enum([
|
||||
/** Permission mode schema for tool execution */
|
||||
export const PermissionModeSchema = z.enum(['default', 'acceptEdits', 'bypassPermissions']);
|
||||
|
||||
/**
|
||||
* Report object schema (new structured format).
|
||||
*
|
||||
* YAML format:
|
||||
* report:
|
||||
* name: 00-plan.md
|
||||
* order: |
|
||||
* **レポート出力:** {report:00-plan.md} に出力してください。
|
||||
* format: |
|
||||
* **レポートフォーマット:**
|
||||
* ```markdown
|
||||
* ...
|
||||
* ```
|
||||
*/
|
||||
export const ReportObjectSchema = z.object({
|
||||
/** Report file name */
|
||||
name: z.string().min(1),
|
||||
/** Instruction prepended before instruction_template (e.g., output destination) */
|
||||
order: z.string().optional(),
|
||||
/** Instruction appended after instruction_template (e.g., output format) */
|
||||
format: z.string().optional(),
|
||||
});
|
||||
|
||||
/**
|
||||
* Report field schema.
|
||||
*
|
||||
* YAML formats:
|
||||
* report: 00-plan.md # single file
|
||||
* report: 00-plan.md # single file (string)
|
||||
* report: # multiple files (label: path map entries)
|
||||
* - Scope: 01-scope.md
|
||||
* - Decisions: 02-decisions.md
|
||||
* report: # object form (name + order + format)
|
||||
* name: 00-plan.md
|
||||
* order: ...
|
||||
* format: ...
|
||||
*
|
||||
* Array items are parsed as single-key objects: [{Scope: "01-scope.md"}, ...]
|
||||
*/
|
||||
export const ReportFieldSchema = z.union([
|
||||
z.string().min(1),
|
||||
z.array(z.record(z.string(), z.string())).min(1),
|
||||
ReportObjectSchema,
|
||||
]);
|
||||
|
||||
/** Rule-based transition schema (new unified format) */
|
||||
|
||||
@ -50,7 +50,7 @@ export interface WorkflowRule {
|
||||
appendix?: string;
|
||||
}
|
||||
|
||||
/** Report file configuration for a workflow step */
|
||||
/** Report file configuration for a workflow step (label: path pair) */
|
||||
export interface ReportConfig {
|
||||
/** Display label (e.g., "Scope", "Decisions") */
|
||||
label: string;
|
||||
@ -58,6 +58,16 @@ export interface ReportConfig {
|
||||
path: string;
|
||||
}
|
||||
|
||||
/** Report object configuration with order/format instructions */
|
||||
export interface ReportObjectConfig {
|
||||
/** Report file name (e.g., "00-plan.md") */
|
||||
name: string;
|
||||
/** Instruction prepended before instruction_template (e.g., output destination) */
|
||||
order?: string;
|
||||
/** Instruction appended after instruction_template (e.g., output format) */
|
||||
format?: string;
|
||||
}
|
||||
|
||||
/** Permission mode for tool execution */
|
||||
export type PermissionMode = 'default' | 'acceptEdits' | 'bypassPermissions';
|
||||
|
||||
@ -81,8 +91,8 @@ export interface WorkflowStep {
|
||||
instructionTemplate: string;
|
||||
/** Rules for step routing */
|
||||
rules?: WorkflowRule[];
|
||||
/** Report file configuration. Single string for one file, array for multiple. */
|
||||
report?: string | ReportConfig[];
|
||||
/** Report file configuration. Single string, array of label:path, or object with order/format. */
|
||||
report?: string | ReportConfig[] | ReportObjectConfig;
|
||||
passPreviousResponse: boolean;
|
||||
}
|
||||
|
||||
|
||||
@ -8,7 +8,7 @@
|
||||
* 3. Appending auto-generated status rules from workflow rules
|
||||
*/
|
||||
|
||||
import type { WorkflowStep, WorkflowRule, AgentResponse, Language, ReportConfig } from '../models/types.js';
|
||||
import type { WorkflowStep, WorkflowRule, AgentResponse, Language, ReportConfig, ReportObjectConfig } from '../models/types.js';
|
||||
import { getGitDiff } from '../agents/runner.js';
|
||||
|
||||
/**
|
||||
@ -216,6 +216,13 @@ function escapeTemplateChars(str: string): string {
|
||||
return str.replace(/\{/g, '{').replace(/\}/g, '}');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a report config is the object form (ReportObjectConfig).
|
||||
*/
|
||||
function isReportObjectConfig(report: string | ReportConfig[] | ReportObjectConfig): report is ReportObjectConfig {
|
||||
return typeof report === 'object' && !Array.isArray(report) && 'name' in report;
|
||||
}
|
||||
|
||||
/** Localized strings for auto-injected sections */
|
||||
const SECTION_STRINGS = {
|
||||
en: {
|
||||
@ -268,16 +275,19 @@ function renderWorkflowContext(
|
||||
|
||||
// Report info (only if step has report config AND reportDir is available)
|
||||
if (step.report && context.reportDir) {
|
||||
lines.push(`- ${s.reportDirectory}: .takt/reports/${context.reportDir}/`);
|
||||
lines.push(`- ${s.reportDirectory}: ${context.reportDir}/`);
|
||||
|
||||
if (typeof step.report === 'string') {
|
||||
// Single file
|
||||
lines.push(`- ${s.reportFile}: .takt/reports/${context.reportDir}/${step.report}`);
|
||||
// Single file (string form)
|
||||
lines.push(`- ${s.reportFile}: ${context.reportDir}/${step.report}`);
|
||||
} else if (isReportObjectConfig(step.report)) {
|
||||
// Object form (name + order + format)
|
||||
lines.push(`- ${s.reportFile}: ${context.reportDir}/${step.report.name}`);
|
||||
} else {
|
||||
// Multiple files
|
||||
// Multiple files (ReportConfig[] form)
|
||||
lines.push(`- ${s.reportFiles}:`);
|
||||
for (const file of step.report as ReportConfig[]) {
|
||||
lines.push(` - ${file.label}: .takt/reports/${context.reportDir}/${file.path}`);
|
||||
lines.push(` - ${file.label}: ${context.reportDir}/${file.path}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -341,6 +351,13 @@ function replaceTemplatePlaceholders(
|
||||
result = result.replace(/\{report_dir\}/g, context.reportDir);
|
||||
}
|
||||
|
||||
// Replace {report:filename} with reportDir/filename
|
||||
if (context.reportDir) {
|
||||
result = result.replace(/\{report:([^}]+)\}/g, (_match, filename: string) => {
|
||||
return `${context.reportDir}/${filename}`;
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@ -403,7 +420,13 @@ export function buildInstruction(
|
||||
sections.push(`${s.additionalUserInputs}\n${escapeTemplateChars(userInputsStr)}`);
|
||||
}
|
||||
|
||||
// 6. Instructions header + instruction_template content
|
||||
// 6a. Report order (prepended before instruction_template, from ReportObjectConfig)
|
||||
if (step.report && isReportObjectConfig(step.report) && step.report.order) {
|
||||
const processedOrder = replaceTemplatePlaceholders(step.report.order.trimEnd(), step, context);
|
||||
sections.push(processedOrder);
|
||||
}
|
||||
|
||||
// 6b. Instructions header + instruction_template content
|
||||
const processedTemplate = replaceTemplatePlaceholders(
|
||||
step.instructionTemplate,
|
||||
step,
|
||||
@ -411,6 +434,12 @@ export function buildInstruction(
|
||||
);
|
||||
sections.push(`${s.instructions}\n${processedTemplate}`);
|
||||
|
||||
// 6c. Report format (appended after instruction_template, from ReportObjectConfig)
|
||||
if (step.report && isReportObjectConfig(step.report) && step.report.format) {
|
||||
const processedFormat = replaceTemplatePlaceholders(step.report.format.trimEnd(), step, context);
|
||||
sections.push(processedFormat);
|
||||
}
|
||||
|
||||
// 7. Status rules (auto-generated from rules)
|
||||
if (step.rules && step.rules.length > 0) {
|
||||
const statusHeader = renderStatusRulesHeader(language);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user