takt-marp 0.1.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/README.ja.md +108 -0
- package/README.md +108 -0
- package/bin/takt-marp.mjs +24 -0
- package/fixtures/marp-slide-workflow/_workflow-smoke/README.md +23 -0
- package/fixtures/marp-slide-workflow/_workflow-smoke/brief.md +44 -0
- package/marp.config.mjs +3 -0
- package/package.json +56 -0
- package/scripts/lib/takt-marp-cli.mjs +199 -0
- package/scripts/lib/takt-marp-project-init.mjs +81 -0
- package/scripts/lib/takt-marp-project-templates.mjs +93 -0
- package/scripts/lib/takt-marp-runtime-context.mjs +24 -0
- package/scripts/lib/takt-marp-slide-workflow.mjs +453 -0
- package/scripts/takt-marp-approve-slide-workflow-state.mjs +37 -0
- package/scripts/takt-marp-build-slide-artifact.mjs +151 -0
- package/scripts/takt-marp-check-slide-workflow-state.mjs +41 -0
- package/scripts/takt-marp-render-slide-workflow-evidence.mjs +70 -0
- package/scripts/takt-marp-run-slide-workflow.mjs +435 -0
- package/scripts/takt-marp-sync-project-templates.mjs +125 -0
- package/scripts/takt-marp-validate-global-install.mjs +391 -0
- package/scripts/takt-marp-validate-package-boundary.mjs +276 -0
- package/scripts/takt-marp-validate-slide-workflow-foundation.mjs +571 -0
- package/scripts/takt-marp-validate-slide-workflow-smoke.mjs +1935 -0
- package/scripts/takt-marp-verify-delivery-artifacts.mjs +181 -0
- package/scripts/takt-marp-verify-render-evidence-metadata.mjs +133 -0
- package/templates/project/facets/instructions/takt-marp-ai-antipattern-fix.md +47 -0
- package/templates/project/facets/instructions/takt-marp-ai-antipattern-review.md +37 -0
- package/templates/project/facets/instructions/takt-marp-compose-fix.md +25 -0
- package/templates/project/facets/instructions/takt-marp-compose-review.md +30 -0
- package/templates/project/facets/instructions/takt-marp-compose-slides.md +35 -0
- package/templates/project/facets/instructions/takt-marp-compose-work-summary.md +23 -0
- package/templates/project/facets/instructions/takt-marp-deliver-build.md +30 -0
- package/templates/project/facets/instructions/takt-marp-deliver-fix.md +25 -0
- package/templates/project/facets/instructions/takt-marp-deliver-verify.md +25 -0
- package/templates/project/facets/instructions/takt-marp-design-system.md +37 -0
- package/templates/project/facets/instructions/takt-marp-intake.md +15 -0
- package/templates/project/facets/instructions/takt-marp-normalize-brief.md +24 -0
- package/templates/project/facets/instructions/takt-marp-plan-fix.md +26 -0
- package/templates/project/facets/instructions/takt-marp-plan-review.md +24 -0
- package/templates/project/facets/instructions/takt-marp-plan-work-summary.md +24 -0
- package/templates/project/facets/instructions/takt-marp-plan.md +26 -0
- package/templates/project/facets/instructions/takt-marp-polish-fix.md +25 -0
- package/templates/project/facets/instructions/takt-marp-polish-inspect.md +25 -0
- package/templates/project/facets/instructions/takt-marp-render-evidence.md +35 -0
- package/templates/project/facets/instructions/takt-marp-supervise-command.md +58 -0
- package/templates/project/facets/instructions/takt-marp-visual-generate.md +26 -0
- package/templates/project/facets/knowledge/takt-marp-repo-conventions.md +119 -0
- package/templates/project/facets/output-contracts/takt-marp-ai-antipattern-fix.md +48 -0
- package/templates/project/facets/output-contracts/takt-marp-ai-antipattern-review.md +43 -0
- package/templates/project/facets/output-contracts/takt-marp-command-fix.md +32 -0
- package/templates/project/facets/output-contracts/takt-marp-command-review.md +32 -0
- package/templates/project/facets/output-contracts/takt-marp-command-work.md +42 -0
- package/templates/project/facets/output-contracts/takt-marp-normalized-brief.md +31 -0
- package/templates/project/facets/output-contracts/takt-marp-slide-plan.md +30 -0
- package/templates/project/facets/output-contracts/takt-marp-supervision.md +45 -0
- package/templates/project/facets/personas/takt-marp-slide-planner.md +24 -0
- package/templates/project/facets/personas/takt-marp-slide-qa.md +23 -0
- package/templates/project/facets/personas/takt-marp-slide-reviewer.md +22 -0
- package/templates/project/facets/personas/takt-marp-slide-reviser.md +22 -0
- package/templates/project/facets/personas/takt-marp-slide-supervisor.md +24 -0
- package/templates/project/facets/personas/takt-marp-slide-writer.md +22 -0
- package/templates/project/facets/policies/takt-marp-general-slide-quality.md +91 -0
- package/templates/project/facets/policies/takt-marp-slide-quality.md +73 -0
- package/templates/project/facets/policies/takt-marp-svg-first-visual.md +66 -0
- package/templates/project/facets/policies/takt-marp-worker-boundary.md +32 -0
- package/templates/project/workflows/takt-marp-slide-ai-quality-gate.yaml +125 -0
- package/templates/project/workflows/takt-marp-slide-compose.yaml +209 -0
- package/templates/project/workflows/takt-marp-slide-deliver.yaml +164 -0
- package/templates/project/workflows/takt-marp-slide-plan.yaml +213 -0
- package/templates/project/workflows/takt-marp-slide-polish.yaml +158 -0
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
4
|
+
import { mkdir, readdir } from "node:fs/promises";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import {
|
|
7
|
+
formatError,
|
|
8
|
+
parseArgs,
|
|
9
|
+
SlideWorkflowError,
|
|
10
|
+
} from "./lib/takt-marp-slide-workflow.mjs";
|
|
11
|
+
import { runtimeExecutablePath } from "./lib/takt-marp-runtime-context.mjs";
|
|
12
|
+
|
|
13
|
+
const ARTIFACT_OPTIONS = Object.freeze({
|
|
14
|
+
html: ["--html"],
|
|
15
|
+
pdf: ["--pdf", "--html"],
|
|
16
|
+
pptx: ["--pptx", "--html"],
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
function usage() {
|
|
20
|
+
return [
|
|
21
|
+
"Usage: node scripts/takt-marp-build-slide-artifact.mjs <html|pdf|pptx> [deck|slides/<deck>|slides/<deck>/SLIDES.md]",
|
|
22
|
+
"",
|
|
23
|
+
"When deck is omitted, all slides/*/SLIDES.md files are built.",
|
|
24
|
+
"",
|
|
25
|
+
"Examples:",
|
|
26
|
+
" npm run build:pdf -- my-talk",
|
|
27
|
+
" npm run build:html -- slides/my-talk",
|
|
28
|
+
" npm run build:pptx -- slides/my-talk/SLIDES.md",
|
|
29
|
+
].join("\n");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function main() {
|
|
33
|
+
const { positional, flags } = parseArgs(process.argv.slice(2));
|
|
34
|
+
if (flags.help) {
|
|
35
|
+
console.log(usage());
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const [artifact, target] = positional;
|
|
40
|
+
if (!ARTIFACT_OPTIONS[artifact]) {
|
|
41
|
+
throw new SlideWorkflowError(usage(), "INVALID_ARGS");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const targets = await resolveBuildTargets(target);
|
|
45
|
+
for (const item of targets) {
|
|
46
|
+
await buildArtifact(artifact, item);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function resolveBuildTargets(target) {
|
|
51
|
+
if (!target) {
|
|
52
|
+
return listDecksWithSlides();
|
|
53
|
+
}
|
|
54
|
+
return [resolveBuildTarget(target)];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function listDecksWithSlides() {
|
|
58
|
+
const slidesRoot = path.join(process.cwd(), "slides");
|
|
59
|
+
const entries = await readdir(slidesRoot, { withFileTypes: true });
|
|
60
|
+
const targets = entries
|
|
61
|
+
.filter((entry) => entry.isDirectory())
|
|
62
|
+
.filter((entry) => existsSync(path.join(slidesRoot, entry.name, "SLIDES.md")))
|
|
63
|
+
.map((entry) => resolveBuildTarget(entry.name))
|
|
64
|
+
.sort((left, right) => left.deckName.localeCompare(right.deckName));
|
|
65
|
+
|
|
66
|
+
if (targets.length === 0) {
|
|
67
|
+
throw new SlideWorkflowError("No slides/*/SLIDES.md files found.", "SLIDES_NOT_FOUND");
|
|
68
|
+
}
|
|
69
|
+
return targets;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function resolveBuildTarget(target) {
|
|
73
|
+
if (!target || path.isAbsolute(target)) {
|
|
74
|
+
throw new SlideWorkflowError(`Invalid deck target '${target}'.`, "INVALID_TARGET");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const normalized = path.posix.normalize(target.replaceAll(path.sep, "/"));
|
|
78
|
+
if (normalized === "." || normalized === ".." || normalized.startsWith("../")) {
|
|
79
|
+
throw new SlideWorkflowError(`Invalid deck target '${target}'.`, "INVALID_TARGET");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const parts = normalized.split("/");
|
|
83
|
+
const deckName = deckNameFromParts(parts, target);
|
|
84
|
+
const deckPath = path.join(process.cwd(), "slides", deckName);
|
|
85
|
+
const slidesPath = path.join(deckPath, "SLIDES.md");
|
|
86
|
+
if (!existsSync(slidesPath)) {
|
|
87
|
+
throw new SlideWorkflowError(`SLIDES.md not found: slides/${deckName}/SLIDES.md`, "SLIDES_NOT_FOUND");
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return Object.freeze({
|
|
91
|
+
deckName,
|
|
92
|
+
slidesPath,
|
|
93
|
+
distPath: path.join(process.cwd(), "dist", deckName),
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function deckNameFromParts(parts, original) {
|
|
98
|
+
if (parts.length === 1 && parts[0] && !parts[0].endsWith(".md")) {
|
|
99
|
+
return parts[0];
|
|
100
|
+
}
|
|
101
|
+
if (parts.length === 2 && parts[0] === "slides" && parts[1] && !parts[1].endsWith(".md")) {
|
|
102
|
+
return parts[1];
|
|
103
|
+
}
|
|
104
|
+
if (parts.length === 3 && parts[0] === "slides" && parts[1] && parts[2] === "SLIDES.md") {
|
|
105
|
+
return parts[1];
|
|
106
|
+
}
|
|
107
|
+
throw new SlideWorkflowError(
|
|
108
|
+
`Invalid deck target '${original}'. Expected deck, slides/<deck>, or slides/<deck>/SLIDES.md.`,
|
|
109
|
+
"INVALID_TARGET",
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function buildArtifact(artifact, target) {
|
|
114
|
+
await mkdir(target.distPath, { recursive: true });
|
|
115
|
+
const outputPath = path.join(target.distPath, `SLIDES.${artifact}`);
|
|
116
|
+
const marpPath = runtimeExecutablePath("marp");
|
|
117
|
+
const args = [
|
|
118
|
+
target.slidesPath,
|
|
119
|
+
...ARTIFACT_OPTIONS[artifact],
|
|
120
|
+
"--allow-local-files",
|
|
121
|
+
"--output",
|
|
122
|
+
outputPath,
|
|
123
|
+
];
|
|
124
|
+
const code = await run(marpPath, args);
|
|
125
|
+
if (code !== 0) {
|
|
126
|
+
throw new SlideWorkflowError(`Marp failed for slides/${target.deckName}/SLIDES.md`, "MARP_BUILD_FAILED");
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function run(command, args) {
|
|
131
|
+
return new Promise((resolve, reject) => {
|
|
132
|
+
const child = spawn(command, args, {
|
|
133
|
+
shell: process.platform === "win32",
|
|
134
|
+
stdio: "inherit",
|
|
135
|
+
});
|
|
136
|
+
child.on("error", (error) => {
|
|
137
|
+
reject(
|
|
138
|
+
new SlideWorkflowError(
|
|
139
|
+
`Failed to start Marp executable: ${command}. Run npm install and verify the @marp-team/marp-cli devDependency. ${error.message}`,
|
|
140
|
+
"MARP_EXECUTABLE_MISSING",
|
|
141
|
+
),
|
|
142
|
+
);
|
|
143
|
+
});
|
|
144
|
+
child.on("close", (code) => resolve(code ?? 1));
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
main().catch((error) => {
|
|
149
|
+
console.error(formatError(error));
|
|
150
|
+
process.exit(1);
|
|
151
|
+
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
checkRequiredState,
|
|
4
|
+
formatError,
|
|
5
|
+
parseArgs,
|
|
6
|
+
parseRequiredState,
|
|
7
|
+
resolveDeckTarget,
|
|
8
|
+
} from "./lib/takt-marp-slide-workflow.mjs";
|
|
9
|
+
|
|
10
|
+
function usage() {
|
|
11
|
+
return [
|
|
12
|
+
"Usage: node scripts/takt-marp-check-slide-workflow-state.mjs <target> --require <command>:<state>[:approved]",
|
|
13
|
+
"",
|
|
14
|
+
"Examples:",
|
|
15
|
+
" npm run slide:check-state -- \"slides/my-talk\" --require plan:planned:approved",
|
|
16
|
+
" npm run slide:check-state -- \"slides/my-talk\" --require polish:polished",
|
|
17
|
+
].join("\n");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function main() {
|
|
21
|
+
const { positional, flags } = parseArgs(process.argv.slice(2));
|
|
22
|
+
if (flags.help) {
|
|
23
|
+
console.log(usage());
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
const [target] = positional;
|
|
27
|
+
const requirementText = flags.require;
|
|
28
|
+
if (!target || !requirementText) {
|
|
29
|
+
throw new Error(usage());
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const targetInfo = resolveDeckTarget(target);
|
|
33
|
+
const requirement = parseRequiredState(requirementText);
|
|
34
|
+
await checkRequiredState(targetInfo, requirement);
|
|
35
|
+
console.log(`state ok: ${targetInfo.target} ${requirementText}`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
main().catch((error) => {
|
|
39
|
+
console.error(formatError(error));
|
|
40
|
+
process.exit(1);
|
|
41
|
+
});
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import {
|
|
5
|
+
formatError,
|
|
6
|
+
hasExecutable,
|
|
7
|
+
parseArgs,
|
|
8
|
+
resolveDeckTarget,
|
|
9
|
+
} from "./lib/takt-marp-slide-workflow.mjs";
|
|
10
|
+
|
|
11
|
+
function usage() {
|
|
12
|
+
return [
|
|
13
|
+
"Usage: node scripts/takt-marp-render-slide-workflow-evidence.mjs <target> --cycle <n>",
|
|
14
|
+
"",
|
|
15
|
+
"Example:",
|
|
16
|
+
" npm run slide:render-evidence -- \"slides/my-talk\" --cycle 1",
|
|
17
|
+
].join("\n");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function main() {
|
|
21
|
+
const { positional, flags } = parseArgs(process.argv.slice(2));
|
|
22
|
+
if (flags.help) {
|
|
23
|
+
console.log(usage());
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
const [target] = positional;
|
|
27
|
+
const cycle = Number(flags.cycle);
|
|
28
|
+
if (!target || !Number.isInteger(cycle) || cycle < 1) {
|
|
29
|
+
throw new Error(usage());
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const targetInfo = resolveDeckTarget(target);
|
|
33
|
+
const outputRoot = path.join(process.cwd(), ".takt", "render", targetInfo.deckName, `cycle-${cycle}`);
|
|
34
|
+
await mkdir(outputRoot, { recursive: true });
|
|
35
|
+
|
|
36
|
+
const metadata = {
|
|
37
|
+
deck: targetInfo.deckName,
|
|
38
|
+
cycle,
|
|
39
|
+
target: targetInfo.target,
|
|
40
|
+
html_png: { status: "pending", files: [] },
|
|
41
|
+
pdf: { status: "pending", file: null },
|
|
42
|
+
pdf_raster: hasExecutable("pdftoppm")
|
|
43
|
+
? { status: "pending", files: [] }
|
|
44
|
+
: { status: "degraded", reason: "pdftoppm not found", files: [] },
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const metadataPath = path.join(outputRoot, "metadata.json");
|
|
48
|
+
const markerPath = path.join(process.cwd(), ".takt", "render", "latest-render-evidence.json");
|
|
49
|
+
await writeFile(metadataPath, `${JSON.stringify(metadata, null, 2)}\n`, "utf8");
|
|
50
|
+
await writeFile(
|
|
51
|
+
markerPath,
|
|
52
|
+
`${JSON.stringify(
|
|
53
|
+
{
|
|
54
|
+
target: targetInfo.target,
|
|
55
|
+
deck: targetInfo.deckName,
|
|
56
|
+
cycle,
|
|
57
|
+
metadata_path: path.relative(process.cwd(), metadataPath).split(path.sep).join("/"),
|
|
58
|
+
},
|
|
59
|
+
null,
|
|
60
|
+
2,
|
|
61
|
+
)}\n`,
|
|
62
|
+
"utf8",
|
|
63
|
+
);
|
|
64
|
+
console.log(`render evidence metadata written: ${metadataPath}`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
main().catch((error) => {
|
|
68
|
+
console.error(formatError(error));
|
|
69
|
+
process.exit(1);
|
|
70
|
+
});
|
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { copyFile, mkdir, readFile, readdir, rename, stat, unlink, writeFile } from "node:fs/promises";
|
|
4
|
+
import { existsSync } from "node:fs";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import {
|
|
7
|
+
archiveCommandArtifacts,
|
|
8
|
+
assertCommandPrerequisites,
|
|
9
|
+
assertTaktExecutableAvailable,
|
|
10
|
+
assertWorkflowAvailable,
|
|
11
|
+
cleanGeneratedOutputs,
|
|
12
|
+
commandSupervisionResult,
|
|
13
|
+
downstreamCommands,
|
|
14
|
+
formatError,
|
|
15
|
+
isSuccessfulCommandState,
|
|
16
|
+
parseFrontMatter,
|
|
17
|
+
parseArgs,
|
|
18
|
+
requireCommand,
|
|
19
|
+
resolveDeckTarget,
|
|
20
|
+
SlideWorkflowError,
|
|
21
|
+
taktExecutablePath,
|
|
22
|
+
} from "./lib/takt-marp-slide-workflow.mjs";
|
|
23
|
+
|
|
24
|
+
function usage() {
|
|
25
|
+
return [
|
|
26
|
+
"Usage: node scripts/takt-marp-run-slide-workflow.mjs <command> <target> [--force] [--provider <name>]",
|
|
27
|
+
"",
|
|
28
|
+
"Commands: plan, compose, polish, deliver",
|
|
29
|
+
"Target: slides/<deck>",
|
|
30
|
+
"",
|
|
31
|
+
"Examples:",
|
|
32
|
+
" npm run slide:plan -- \"slides/my-talk\"",
|
|
33
|
+
" npm run slide:plan -- \"slides/my-talk\" --provider mock",
|
|
34
|
+
" npm run slide:compose -- \"slides/my-talk\" --force",
|
|
35
|
+
].join("\n");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function main() {
|
|
39
|
+
const { positional, flags } = parseArgs(process.argv.slice(2));
|
|
40
|
+
if (flags.help) {
|
|
41
|
+
console.log(usage());
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const [commandArg, target] = positional;
|
|
45
|
+
const command = requireCommand(commandArg);
|
|
46
|
+
const targetInfo = resolveDeckTarget(target);
|
|
47
|
+
|
|
48
|
+
await assertCommandPrerequisites(targetInfo, command);
|
|
49
|
+
assertWorkflowAvailable(command);
|
|
50
|
+
// Keep executable availability in preflight so failed setup cannot invalidate current artifacts.
|
|
51
|
+
assertTaktExecutableAvailable();
|
|
52
|
+
|
|
53
|
+
if (flags.force) {
|
|
54
|
+
await archiveCommandArtifacts(targetInfo, downstreamCommands(command), "force", { includeApprovals: true });
|
|
55
|
+
await cleanGeneratedOutputs(targetInfo);
|
|
56
|
+
} else if (isSuccessfulCommandState(targetInfo, command)) {
|
|
57
|
+
throw new SlideWorkflowError(
|
|
58
|
+
`Command '${command}' already reached successful state. Use --force to invalidate and rerun.`,
|
|
59
|
+
"RERUN_BLOCKED",
|
|
60
|
+
);
|
|
61
|
+
} else if ((await commandSupervisionResult(targetInfo, command)) === "rejected") {
|
|
62
|
+
await archiveCommandArtifacts(targetInfo, [command], "rejected-rerun");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
await writeCurrentWorkflowTarget(command, targetInfo);
|
|
66
|
+
const runSnapshotBefore = await snapshotTaktRuns(command);
|
|
67
|
+
const code = await runTakt(command, targetInfo.target, { provider: flags.provider });
|
|
68
|
+
if (code !== 0) {
|
|
69
|
+
process.exitCode = code;
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
await syncTaktReportsToDeck(command, targetInfo, runSnapshotBefore);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function writeCurrentWorkflowTarget(command, targetInfo) {
|
|
76
|
+
const markerPath = path.join(process.cwd(), ".takt", "workflow-current-target.json");
|
|
77
|
+
await mkdir(path.dirname(markerPath), { recursive: true });
|
|
78
|
+
await writeFile(
|
|
79
|
+
markerPath,
|
|
80
|
+
`${JSON.stringify(
|
|
81
|
+
{
|
|
82
|
+
command,
|
|
83
|
+
target: targetInfo.target,
|
|
84
|
+
deck: targetInfo.deckName,
|
|
85
|
+
started_at: new Date().toISOString(),
|
|
86
|
+
},
|
|
87
|
+
null,
|
|
88
|
+
2,
|
|
89
|
+
)}\n`,
|
|
90
|
+
"utf8",
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function runTakt(command, target, options = {}) {
|
|
95
|
+
const args = ["--pipeline", "--skip-git", "-w", `takt-marp-slide-${command}`, "-t", target];
|
|
96
|
+
if (options.provider) {
|
|
97
|
+
args.push("--provider", options.provider);
|
|
98
|
+
}
|
|
99
|
+
const child = spawn(taktExecutablePath(), args, {
|
|
100
|
+
shell: process.platform === "win32",
|
|
101
|
+
stdio: "inherit",
|
|
102
|
+
});
|
|
103
|
+
const code = await new Promise((resolve, reject) => {
|
|
104
|
+
child.on("error", (error) => {
|
|
105
|
+
reject(
|
|
106
|
+
new SlideWorkflowError(
|
|
107
|
+
`Failed to start TAKT executable: ${taktExecutablePath()}. Run npm install and verify the takt devDependency. ${error.message}`,
|
|
108
|
+
"TAKT_EXECUTABLE_MISSING",
|
|
109
|
+
),
|
|
110
|
+
);
|
|
111
|
+
});
|
|
112
|
+
child.on("close", resolve);
|
|
113
|
+
});
|
|
114
|
+
return code ?? 1;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function syncTaktReportsToDeck(command, targetInfo, runSnapshotBefore) {
|
|
118
|
+
const reportsDir = await createdTaktReportsDir(command, targetInfo, runSnapshotBefore);
|
|
119
|
+
if (!reportsDir) {
|
|
120
|
+
throw new SlideWorkflowError(
|
|
121
|
+
`TAKT completed but no matching report directory was found for ${command} ${targetInfo.target}.`,
|
|
122
|
+
"TAKT_REPORT_SYNC_MISSING",
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
await mkdir(targetInfo.reviewPath, { recursive: true });
|
|
127
|
+
const reportNameSet = commandReportNameSet(command);
|
|
128
|
+
const selectedRun = await selectedRunMetadata(reportsDir, command);
|
|
129
|
+
const reportCopies = await commandReportCopies(reportsDir, command, reportNameSet, {
|
|
130
|
+
target: targetInfo.target,
|
|
131
|
+
workflowRunId: selectedRun.workflow_run_id,
|
|
132
|
+
});
|
|
133
|
+
const sourceArtifactCopies = await commandSourceArtifactCopies(reportsDir, command, targetInfo);
|
|
134
|
+
await replaceDeckSourceArtifacts(sourceArtifactCopies);
|
|
135
|
+
await replaceDeckReports(targetInfo.reviewPath, reportNameSet, reportCopies);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function selectedRunMetadata(reportsDir, command) {
|
|
139
|
+
const reportPath = path.join(reportsDir, `${command}-supervision.md`);
|
|
140
|
+
const { frontMatter } = parseFrontMatter(await readFile(reportPath, "utf8"));
|
|
141
|
+
return frontMatter;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function commandReportNameSet(command) {
|
|
145
|
+
return new Set([
|
|
146
|
+
`${command}-work.md`,
|
|
147
|
+
`${command}-review.md`,
|
|
148
|
+
`${command}-inspect.md`,
|
|
149
|
+
`${command}-verify.md`,
|
|
150
|
+
`${command}-fix.md`,
|
|
151
|
+
`${command}-supervision.md`,
|
|
152
|
+
`${command}-loop-monitor.md`,
|
|
153
|
+
`${command}-ai-antipattern-review.md`,
|
|
154
|
+
`${command}-ai-antipattern-fix.md`,
|
|
155
|
+
]);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async function commandReportCopies(reportsDir, command, reportNameSet, selectedRun) {
|
|
159
|
+
const reportNames = await readdir(reportsDir);
|
|
160
|
+
const topLevelReportCopies = reportNames
|
|
161
|
+
.filter((name) => reportNameSet.has(name))
|
|
162
|
+
.map((reportName) => Object.freeze({ reportName, sourcePath: path.join(reportsDir, reportName) }));
|
|
163
|
+
return Object.freeze([
|
|
164
|
+
...topLevelReportCopies,
|
|
165
|
+
...(await aiGateReportCopies(reportsDir, command, selectedRun)),
|
|
166
|
+
]);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async function commandSourceArtifactCopies(reportsDir, command, targetInfo) {
|
|
170
|
+
const sourceArtifactNames = commandSourceArtifactNames(command);
|
|
171
|
+
const copies = [];
|
|
172
|
+
for (const artifactName of sourceArtifactNames) {
|
|
173
|
+
const sourcePath = path.join(reportsDir, artifactName);
|
|
174
|
+
if (!existsSync(sourcePath)) {
|
|
175
|
+
throw new SlideWorkflowError(
|
|
176
|
+
`TAKT completed but required source artifact was not found in reports: ${path.relative(process.cwd(), sourcePath)}`,
|
|
177
|
+
"TAKT_SOURCE_ARTIFACT_SYNC_MISSING",
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
copies.push(Object.freeze({
|
|
181
|
+
sourcePath,
|
|
182
|
+
finalPath: path.join(targetInfo.deckPath, artifactName),
|
|
183
|
+
}));
|
|
184
|
+
}
|
|
185
|
+
return Object.freeze(copies);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function commandSourceArtifactNames(command) {
|
|
189
|
+
if (command === "plan") {
|
|
190
|
+
return ["brief.normalized.md", "plan.md"];
|
|
191
|
+
}
|
|
192
|
+
return [];
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async function replaceDeckSourceArtifacts(sourceArtifactCopies) {
|
|
196
|
+
for (const { sourcePath, finalPath } of sourceArtifactCopies) {
|
|
197
|
+
await replaceFileAtomically(sourcePath, finalPath);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async function replaceFileAtomically(sourcePath, finalPath) {
|
|
202
|
+
await mkdir(path.dirname(finalPath), { recursive: true });
|
|
203
|
+
const tempSuffix = `${process.pid}-${Date.now()}`;
|
|
204
|
+
const tempPath = path.join(path.dirname(finalPath), `.${path.basename(finalPath)}.${tempSuffix}.tmp`);
|
|
205
|
+
const backupPath = path.join(path.dirname(finalPath), `.${path.basename(finalPath)}.${tempSuffix}.bak`);
|
|
206
|
+
let backupCreated = false;
|
|
207
|
+
try {
|
|
208
|
+
await copyFile(sourcePath, tempPath);
|
|
209
|
+
if (existsSync(finalPath)) {
|
|
210
|
+
await rename(finalPath, backupPath);
|
|
211
|
+
backupCreated = true;
|
|
212
|
+
}
|
|
213
|
+
await rename(tempPath, finalPath);
|
|
214
|
+
if (backupCreated && existsSync(backupPath)) {
|
|
215
|
+
await unlink(backupPath);
|
|
216
|
+
}
|
|
217
|
+
} catch (error) {
|
|
218
|
+
if (existsSync(tempPath)) {
|
|
219
|
+
await unlink(tempPath);
|
|
220
|
+
}
|
|
221
|
+
if (backupCreated) {
|
|
222
|
+
if (existsSync(finalPath)) {
|
|
223
|
+
await unlink(finalPath);
|
|
224
|
+
}
|
|
225
|
+
if (existsSync(backupPath)) {
|
|
226
|
+
await rename(backupPath, finalPath);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
throw error;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
async function aiGateReportCopies(reportsDir, command, selectedRun) {
|
|
234
|
+
const subworkflowsDir = path.join(reportsDir, "subworkflows");
|
|
235
|
+
if (!existsSync(subworkflowsDir)) {
|
|
236
|
+
return Object.freeze([]);
|
|
237
|
+
}
|
|
238
|
+
const copies = [];
|
|
239
|
+
const reportSteps = {
|
|
240
|
+
"ai-antipattern-review.md": "ai_antipattern_review",
|
|
241
|
+
"ai-antipattern-fix.md": "ai_antipattern_fix",
|
|
242
|
+
};
|
|
243
|
+
for (const [reportFileName, step] of Object.entries(reportSteps)) {
|
|
244
|
+
const sourcePath = await findSingleAiGateReportPath(subworkflowsDir, reportFileName, {
|
|
245
|
+
command,
|
|
246
|
+
target: selectedRun.target,
|
|
247
|
+
workflowRunId: selectedRun.workflowRunId,
|
|
248
|
+
step,
|
|
249
|
+
});
|
|
250
|
+
if (sourcePath) {
|
|
251
|
+
copies.push(Object.freeze({
|
|
252
|
+
reportName: `${command}-${reportFileName}`,
|
|
253
|
+
sourcePath,
|
|
254
|
+
}));
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
return Object.freeze(copies);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
async function findSingleAiGateReportPath(rootDir, reportFileName, expected) {
|
|
261
|
+
const found = [];
|
|
262
|
+
await collectReportPaths(rootDir, reportFileName, found);
|
|
263
|
+
const matching = [];
|
|
264
|
+
for (const reportPath of found) {
|
|
265
|
+
const report = await matchingAiGateReport(reportPath, expected);
|
|
266
|
+
if (report) {
|
|
267
|
+
matching.push(report);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
if (matching.length === 0) {
|
|
271
|
+
return null;
|
|
272
|
+
}
|
|
273
|
+
return [...matching].sort(
|
|
274
|
+
(left, right) => right.cycle - left.cycle || right.mtimeMs - left.mtimeMs || right.reportPath.localeCompare(left.reportPath),
|
|
275
|
+
)[0].reportPath;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
async function matchingAiGateReport(reportPath, expected) {
|
|
279
|
+
const { frontMatter } = parseFrontMatter(await readFile(reportPath, "utf8"));
|
|
280
|
+
if (
|
|
281
|
+
frontMatter.command === expected.command &&
|
|
282
|
+
frontMatter.target === expected.target &&
|
|
283
|
+
frontMatter.workflow_run_id === expected.workflowRunId &&
|
|
284
|
+
frontMatter.step === expected.step
|
|
285
|
+
) {
|
|
286
|
+
const reportStat = await stat(reportPath);
|
|
287
|
+
return Object.freeze({
|
|
288
|
+
reportPath,
|
|
289
|
+
cycle: Number(frontMatter.cycle ?? 0),
|
|
290
|
+
mtimeMs: reportStat.mtimeMs,
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
return null;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
async function collectReportPaths(directory, reportFileName, found) {
|
|
297
|
+
const entries = await readdir(directory, { withFileTypes: true });
|
|
298
|
+
for (const entry of entries) {
|
|
299
|
+
const entryPath = path.join(directory, entry.name);
|
|
300
|
+
if (entry.isDirectory()) {
|
|
301
|
+
await collectReportPaths(entryPath, reportFileName, found);
|
|
302
|
+
} else if (entry.isFile() && entry.name === reportFileName) {
|
|
303
|
+
found.push(entryPath);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
async function replaceDeckReports(reviewPath, reportNameSet, reportCopies) {
|
|
309
|
+
const syncedReportNames = reportCopies.map(({ reportName }) => reportName);
|
|
310
|
+
const tempReports = [];
|
|
311
|
+
const backupReports = [];
|
|
312
|
+
const tempSuffix = `${process.pid}-${Date.now()}`;
|
|
313
|
+
let replacementStarted = false;
|
|
314
|
+
|
|
315
|
+
try {
|
|
316
|
+
for (const { reportName, sourcePath } of reportCopies) {
|
|
317
|
+
const tempPath = path.join(reviewPath, `.${reportName}.${tempSuffix}.tmp`);
|
|
318
|
+
await copyFile(sourcePath, tempPath);
|
|
319
|
+
tempReports.push({ reportName, tempPath, finalPath: path.join(reviewPath, reportName) });
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
for (const { reportName, finalPath } of tempReports) {
|
|
323
|
+
if (existsSync(finalPath)) {
|
|
324
|
+
const backupPath = path.join(reviewPath, `.${reportName}.${tempSuffix}.bak`);
|
|
325
|
+
await rename(finalPath, backupPath);
|
|
326
|
+
backupReports.push({ backupPath, finalPath });
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
replacementStarted = true;
|
|
331
|
+
for (const { tempPath, finalPath } of tempReports) {
|
|
332
|
+
await rename(tempPath, finalPath);
|
|
333
|
+
}
|
|
334
|
+
} catch (error) {
|
|
335
|
+
for (const { tempPath } of tempReports) {
|
|
336
|
+
if (existsSync(tempPath)) {
|
|
337
|
+
await unlink(tempPath);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
if (replacementStarted) {
|
|
341
|
+
for (const { finalPath } of tempReports) {
|
|
342
|
+
if (existsSync(finalPath)) {
|
|
343
|
+
await unlink(finalPath);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
for (const { backupPath, finalPath } of backupReports.reverse()) {
|
|
348
|
+
if (existsSync(backupPath)) {
|
|
349
|
+
await rename(backupPath, finalPath);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
throw error;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
for (const { backupPath } of backupReports) {
|
|
356
|
+
if (existsSync(backupPath)) {
|
|
357
|
+
await unlink(backupPath);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
await cleanStaleDeckReports(reviewPath, reportNameSet, new Set(syncedReportNames));
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
async function cleanStaleDeckReports(reviewPath, reportNameSet, syncedReportNameSet) {
|
|
365
|
+
const reportNames = await readdir(reviewPath);
|
|
366
|
+
for (const reportName of reportNames.filter((name) => reportNameSet.has(name) && !syncedReportNameSet.has(name))) {
|
|
367
|
+
await unlink(path.join(reviewPath, reportName));
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
async function createdTaktReportsDir(command, targetInfo, runSnapshotBefore) {
|
|
372
|
+
const runsRoot = path.join(process.cwd(), ".takt", "runs");
|
|
373
|
+
const runNames = (await listTaktRunNames()).sort().reverse();
|
|
374
|
+
const candidates = [];
|
|
375
|
+
|
|
376
|
+
for (const runName of runNames) {
|
|
377
|
+
const reportsDir = path.join(runsRoot, runName, "reports");
|
|
378
|
+
const reportPath = path.join(reportsDir, `${command}-supervision.md`);
|
|
379
|
+
const reportMtime = await reportMtimeIfChangedSinceSnapshot(reportPath, runName, runSnapshotBefore);
|
|
380
|
+
if (reportMtime !== null && (await reportMatchesSuccessfulTarget(reportPath, command, targetInfo.target))) {
|
|
381
|
+
candidates.push({ reportsDir, reportMtime });
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
if (candidates.length === 0) {
|
|
386
|
+
return null;
|
|
387
|
+
}
|
|
388
|
+
if (candidates.length > 1) {
|
|
389
|
+
throw new SlideWorkflowError(
|
|
390
|
+
`TAKT completed but multiple matching report directories changed for ${command} ${targetInfo.target}. Refusing to sync an ambiguous run.`,
|
|
391
|
+
"TAKT_REPORT_SYNC_AMBIGUOUS",
|
|
392
|
+
);
|
|
393
|
+
}
|
|
394
|
+
return candidates[0].reportsDir;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
async function snapshotTaktRuns(command) {
|
|
398
|
+
const runsRoot = path.join(process.cwd(), ".takt", "runs");
|
|
399
|
+
const snapshot = new Map();
|
|
400
|
+
for (const runName of await listTaktRunNames()) {
|
|
401
|
+
const reportPath = path.join(runsRoot, runName, "reports", `${command}-supervision.md`);
|
|
402
|
+
snapshot.set(runName, existsSync(reportPath) ? (await stat(reportPath)).mtimeMs : null);
|
|
403
|
+
}
|
|
404
|
+
return snapshot;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
async function reportMtimeIfChangedSinceSnapshot(reportPath, runName, runSnapshotBefore) {
|
|
408
|
+
if (!existsSync(reportPath)) {
|
|
409
|
+
return null;
|
|
410
|
+
}
|
|
411
|
+
const reportMtime = (await stat(reportPath)).mtimeMs;
|
|
412
|
+
const previousMtime = runSnapshotBefore.get(runName);
|
|
413
|
+
return previousMtime === undefined || previousMtime === null || reportMtime > previousMtime ? reportMtime : null;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
async function listTaktRunNames() {
|
|
417
|
+
const runsRoot = path.join(process.cwd(), ".takt", "runs");
|
|
418
|
+
if (!existsSync(runsRoot)) {
|
|
419
|
+
return [];
|
|
420
|
+
}
|
|
421
|
+
return (await readdir(runsRoot, { withFileTypes: true })).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
async function reportMatchesSuccessfulTarget(reportPath, command, target) {
|
|
425
|
+
if (!existsSync(reportPath)) {
|
|
426
|
+
return false;
|
|
427
|
+
}
|
|
428
|
+
const { frontMatter } = parseFrontMatter(await readFile(reportPath, "utf8"));
|
|
429
|
+
return frontMatter.command === command && frontMatter.target === target && frontMatter.result === "passed";
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
main().catch((error) => {
|
|
433
|
+
console.error(formatError(error));
|
|
434
|
+
process.exit(1);
|
|
435
|
+
});
|