@workflow-cannon/workspace-kit 0.8.0 → 0.10.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 (44) hide show
  1. package/README.md +21 -5
  2. package/dist/cli/run-command.d.ts +11 -0
  3. package/dist/cli/run-command.js +143 -0
  4. package/dist/cli.js +19 -146
  5. package/dist/core/config-cli.js +5 -5
  6. package/dist/core/config-metadata.js +3 -3
  7. package/dist/core/index.d.ts +1 -1
  8. package/dist/core/index.js +1 -1
  9. package/dist/core/policy.d.ts +11 -1
  10. package/dist/core/policy.js +41 -22
  11. package/dist/core/transcript-completion-hook.js +41 -3
  12. package/dist/core/workspace-kit-config.d.ts +2 -1
  13. package/dist/core/workspace-kit-config.js +4 -29
  14. package/dist/modules/approvals/index.js +2 -2
  15. package/dist/modules/documentation/runtime.js +30 -6
  16. package/dist/modules/improvement/index.js +15 -3
  17. package/dist/modules/improvement/transcript-sync-runtime.d.ts +5 -0
  18. package/dist/modules/improvement/transcript-sync-runtime.js +17 -0
  19. package/package.json +2 -1
  20. package/src/modules/documentation/README.md +39 -0
  21. package/src/modules/documentation/RULES.md +70 -0
  22. package/src/modules/documentation/config.md +14 -0
  23. package/src/modules/documentation/index.ts +120 -0
  24. package/src/modules/documentation/instructions/document-project.md +44 -0
  25. package/src/modules/documentation/instructions/documentation-maintainer.md +81 -0
  26. package/src/modules/documentation/instructions/generate-document.md +44 -0
  27. package/src/modules/documentation/runtime.ts +870 -0
  28. package/src/modules/documentation/schemas/documentation-schema.md +54 -0
  29. package/src/modules/documentation/state.md +8 -0
  30. package/src/modules/documentation/templates/AGENTS.md +84 -0
  31. package/src/modules/documentation/templates/ARCHITECTURE.md +71 -0
  32. package/src/modules/documentation/templates/PRINCIPLES.md +122 -0
  33. package/src/modules/documentation/templates/RELEASING.md +96 -0
  34. package/src/modules/documentation/templates/ROADMAP.md +131 -0
  35. package/src/modules/documentation/templates/SECURITY.md +53 -0
  36. package/src/modules/documentation/templates/SUPPORT.md +40 -0
  37. package/src/modules/documentation/templates/TERMS.md +61 -0
  38. package/src/modules/documentation/templates/runbooks/consumer-cadence.md +55 -0
  39. package/src/modules/documentation/templates/runbooks/parity-validation-flow.md +68 -0
  40. package/src/modules/documentation/templates/runbooks/release-channels.md +30 -0
  41. package/src/modules/documentation/templates/workbooks/phase2-config-policy-workbook.md +42 -0
  42. package/src/modules/documentation/templates/workbooks/task-engine-workbook.md +42 -0
  43. package/src/modules/documentation/templates/workbooks/transcript-automation-baseline.md +68 -0
  44. package/src/modules/documentation/types.ts +51 -0
@@ -2,6 +2,7 @@ import { existsSync, mkdirSync, statSync, unlinkSync, writeFileSync } from "node
2
2
  import path from "node:path";
3
3
  import { spawn } from "node:child_process";
4
4
  const LOCK_REL = ".workspace-kit/improvement/transcript-hook.lock";
5
+ const EVENT_LOG_REL = ".workspace-kit/improvement/transcript-hook-events.jsonl";
5
6
  const LOCK_STALE_MS = 120_000;
6
7
  export function readAfterTaskCompletedHook(effective) {
7
8
  const imp = effective.improvement;
@@ -39,31 +40,62 @@ function hookLockBusy(workspacePath) {
39
40
  return false;
40
41
  }
41
42
  }
43
+ function appendHookEvent(workspacePath, event, details) {
44
+ const fp = path.join(workspacePath, EVENT_LOG_REL);
45
+ try {
46
+ mkdirSync(path.dirname(fp), { recursive: true });
47
+ writeFileSync(fp, `${JSON.stringify({
48
+ schemaVersion: 1,
49
+ timestamp: new Date().toISOString(),
50
+ event,
51
+ ...details
52
+ })}\n`, { encoding: "utf8", flag: "a" });
53
+ }
54
+ catch {
55
+ // Observability must never block task transitions.
56
+ }
57
+ }
42
58
  /**
43
59
  * After a task reaches `completed`, optionally spawn sync/ingest without blocking the transition (T274).
44
60
  * Ingest requires WORKSPACE_KIT_POLICY_APPROVAL in the parent env or falls back to sync.
45
61
  */
46
62
  export function maybeSpawnTranscriptHookAfterCompletion(workspacePath, effective) {
47
63
  const mode = readAfterTaskCompletedHook(effective);
48
- if (mode === "off")
64
+ if (mode === "off") {
65
+ appendHookEvent(workspacePath, "skipped", { reason: "hook-mode-off" });
49
66
  return;
50
- if (hookLockBusy(workspacePath))
67
+ }
68
+ if (hookLockBusy(workspacePath)) {
69
+ appendHookEvent(workspacePath, "skipped", { reason: "lock-busy", mode });
51
70
  return;
71
+ }
52
72
  let subcommand = "sync-transcripts";
53
73
  if (mode === "ingest" && process.env.WORKSPACE_KIT_POLICY_APPROVAL?.trim()) {
54
74
  subcommand = "ingest-transcripts";
55
75
  }
56
76
  const cli = resolveWorkspaceKitCli(workspacePath);
57
- if (!cli)
77
+ if (!cli) {
78
+ appendHookEvent(workspacePath, "failed", {
79
+ reason: "cli-not-found",
80
+ mode,
81
+ subcommand
82
+ });
58
83
  return;
84
+ }
59
85
  const fp = lockPath(workspacePath);
60
86
  try {
61
87
  mkdirSync(path.dirname(fp), { recursive: true });
62
88
  writeFileSync(fp, `${JSON.stringify({ at: new Date().toISOString(), subcommand })}\n`, "utf8");
63
89
  }
64
90
  catch {
91
+ appendHookEvent(workspacePath, "failed", {
92
+ reason: "lock-write-failed",
93
+ mode,
94
+ subcommand
95
+ });
65
96
  return;
66
97
  }
98
+ appendHookEvent(workspacePath, "started", { mode, subcommand });
67
99
  const child = spawn(process.execPath, [cli, "run", subcommand, "{}"], {
68
100
  cwd: workspacePath,
69
101
  detached: true,
@@ -78,6 +110,7 @@ export function maybeSpawnTranscriptHookAfterCompletion(workspacePath, effective
78
110
  catch {
79
111
  /* ignore */
80
112
  }
113
+ appendHookEvent(workspacePath, "completed", { mode, subcommand });
81
114
  });
82
115
  child.on("error", () => {
83
116
  try {
@@ -86,5 +119,10 @@ export function maybeSpawnTranscriptHookAfterCompletion(workspacePath, effective
86
119
  catch {
87
120
  /* ignore */
88
121
  }
122
+ appendHookEvent(workspacePath, "failed", {
123
+ reason: "spawn-error",
124
+ mode,
125
+ subcommand
126
+ });
89
127
  });
90
128
  }
@@ -12,7 +12,8 @@ export declare function getProjectConfigPath(workspacePath: string): string;
12
12
  export declare const KIT_CONFIG_DEFAULTS: Record<string, unknown>;
13
13
  /**
14
14
  * Static module-level defaults keyed by module id (merged in registry startup order).
15
- * Each value is deep-merged into the aggregate before project/env/invocation.
15
+ * Keep true defaults in KIT_CONFIG_DEFAULTS as the canonical source; use this map
16
+ * only when a module contributes additional non-default config structure.
16
17
  */
17
18
  export declare const MODULE_CONFIG_CONTRIBUTIONS: Record<string, Record<string, unknown>>;
18
19
  export declare function deepMerge(target: Record<string, unknown>, source: Record<string, unknown>): Record<string, unknown>;
@@ -21,7 +21,7 @@ export const KIT_CONFIG_DEFAULTS = {
21
21
  },
22
22
  improvement: {
23
23
  transcripts: {
24
- sourcePath: ".cursor/agent-transcripts",
24
+ sourcePath: "",
25
25
  archivePath: "agent-transcripts",
26
26
  maxFilesPerSync: 5000,
27
27
  maxBytesPerFile: 50_000_000,
@@ -40,37 +40,12 @@ export const KIT_CONFIG_DEFAULTS = {
40
40
  };
41
41
  /**
42
42
  * Static module-level defaults keyed by module id (merged in registry startup order).
43
- * Each value is deep-merged into the aggregate before project/env/invocation.
43
+ * Keep true defaults in KIT_CONFIG_DEFAULTS as the canonical source; use this map
44
+ * only when a module contributes additional non-default config structure.
44
45
  */
45
46
  export const MODULE_CONFIG_CONTRIBUTIONS = {
46
- "task-engine": {
47
- tasks: {
48
- storeRelativePath: ".workspace-kit/tasks/state.json"
49
- }
50
- },
51
- documentation: {
52
- documentation: {}
53
- },
54
47
  approvals: {},
55
- planning: {},
56
- improvement: {
57
- transcripts: {
58
- sourcePath: ".cursor/agent-transcripts",
59
- archivePath: "agent-transcripts",
60
- maxFilesPerSync: 5000,
61
- maxBytesPerFile: 50_000_000,
62
- maxTotalScanBytes: 500_000_000,
63
- discoveryPaths: []
64
- },
65
- cadence: {
66
- minIntervalMinutes: 15,
67
- skipIfNoNewTranscripts: true,
68
- maxRecommendationCandidatesPerRun: 500
69
- },
70
- hooks: {
71
- afterTaskCompleted: "off"
72
- }
73
- }
48
+ planning: {}
74
49
  };
75
50
  export function deepMerge(target, source) {
76
51
  const out = { ...target };
@@ -1,4 +1,4 @@
1
- import { resolveActor } from "../../core/policy.js";
1
+ import { resolveActorWithFallback } from "../../core/policy.js";
2
2
  import { runReviewItem } from "./review-runtime.js";
3
3
  export const approvalsModule = {
4
4
  registration: {
@@ -40,7 +40,7 @@ export const approvalsModule = {
40
40
  const args = command.args ?? {};
41
41
  const actor = typeof args.actor === "string" && args.actor.trim().length > 0
42
42
  ? args.actor.trim()
43
- : ctx.resolvedActor ?? resolveActor(ctx.workspacePath, args, process.env);
43
+ : ctx.resolvedActor ?? (await resolveActorWithFallback(ctx.workspacePath, args, process.env));
44
44
  const taskId = typeof args.taskId === "string" ? args.taskId : "";
45
45
  const decision = args.decision;
46
46
  if (decision !== "accept" && decision !== "decline" && decision !== "accept_edited") {
@@ -2,6 +2,7 @@ import { mkdir, readFile, writeFile } from "node:fs/promises";
2
2
  import { existsSync } from "node:fs";
3
3
  import { dirname, resolve, sep } from "node:path";
4
4
  import { readdir } from "node:fs/promises";
5
+ import { fileURLToPath } from "node:url";
5
6
  function isPathWithinRoot(path, root) {
6
7
  return path === root || path.startsWith(`${root}${sep}`);
7
8
  }
@@ -12,8 +13,30 @@ function parseDefaultValue(fileContent, key, fallback) {
12
13
  return match?.[1] ?? fallback;
13
14
  }
14
15
  async function loadRuntimeConfig(workspacePath) {
15
- const configPath = resolve(workspacePath, "src/modules/documentation/config.md");
16
- const configContent = await readFile(configPath, "utf8");
16
+ const runtimeSourceRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..", "..", "..");
17
+ const sourceRoots = [workspacePath, runtimeSourceRoot];
18
+ let sourceRoot = workspacePath;
19
+ let configContent;
20
+ for (const candidateRoot of sourceRoots) {
21
+ const candidate = resolve(candidateRoot, "src/modules/documentation/config.md");
22
+ if (!existsSync(candidate)) {
23
+ continue;
24
+ }
25
+ configContent = await readFile(candidate, "utf8");
26
+ sourceRoot = candidateRoot;
27
+ break;
28
+ }
29
+ if (!configContent) {
30
+ return {
31
+ aiRoot: "/.ai",
32
+ humanRoot: "docs/maintainers",
33
+ templatesRoot: "src/modules/documentation/templates",
34
+ instructionsRoot: "src/modules/documentation/instructions",
35
+ schemasRoot: "src/modules/documentation/schemas",
36
+ maxValidationAttempts: 3,
37
+ sourceRoot
38
+ };
39
+ }
17
40
  const aiRoot = parseDefaultValue(configContent, "sources.aiRoot", "/.ai");
18
41
  const humanRoot = parseDefaultValue(configContent, "sources.humanRoot", "docs/maintainers");
19
42
  const templatesRoot = parseDefaultValue(configContent, "sources.templatesRoot", "src/modules/documentation/templates");
@@ -27,7 +50,8 @@ async function loadRuntimeConfig(workspacePath) {
27
50
  templatesRoot,
28
51
  instructionsRoot,
29
52
  schemasRoot,
30
- maxValidationAttempts: Number.isFinite(maxValidationAttempts) ? maxValidationAttempts : 3
53
+ maxValidationAttempts: Number.isFinite(maxValidationAttempts) ? maxValidationAttempts : 3,
54
+ sourceRoot
31
55
  };
32
56
  }
33
57
  function parseAiRecordLine(line) {
@@ -441,7 +465,7 @@ export async function generateDocument(args, ctx) {
441
465
  const conflicts = [];
442
466
  const aiRoot = resolve(ctx.workspacePath, config.aiRoot.replace(/^\//, ""));
443
467
  const humanRoot = resolve(ctx.workspacePath, config.humanRoot.replace(/^\//, ""));
444
- const templatePath = resolve(ctx.workspacePath, config.templatesRoot, documentType);
468
+ const templatePath = resolve(config.sourceRoot, config.templatesRoot, documentType);
445
469
  const aiOutputPath = resolve(aiRoot, documentType);
446
470
  const humanOutputPath = resolve(humanRoot, documentType);
447
471
  if (!isPathWithinRoot(aiOutputPath, aiRoot) || !isPathWithinRoot(humanOutputPath, humanRoot)) {
@@ -493,7 +517,7 @@ export async function generateDocument(args, ctx) {
493
517
  };
494
518
  }
495
519
  }
496
- const schemaPath = resolve(ctx.workspacePath, config.schemasRoot, "documentation-schema.md");
520
+ const schemaPath = resolve(config.sourceRoot, config.schemasRoot, "documentation-schema.md");
497
521
  if (existsSync(schemaPath)) {
498
522
  filesRead.push(schemaPath);
499
523
  await readFile(schemaPath, "utf8");
@@ -663,7 +687,7 @@ export async function generateDocument(args, ctx) {
663
687
  }
664
688
  export async function generateAllDocuments(args, ctx) {
665
689
  const config = await loadRuntimeConfig(ctx.workspacePath);
666
- const templatesDir = resolve(ctx.workspacePath, config.templatesRoot);
690
+ const templatesDir = resolve(config.sourceRoot, config.templatesRoot);
667
691
  async function listTemplateFiles(dir, baseDir) {
668
692
  const entries = await readdir(dir, { withFileTypes: true });
669
693
  const files = [];
@@ -58,13 +58,25 @@ export const improvementModule = {
58
58
  const transcriptsRoot = typeof args.transcriptsRoot === "string" ? args.transcriptsRoot : undefined;
59
59
  const fromTag = typeof args.fromTag === "string" ? args.fromTag : undefined;
60
60
  const toTag = typeof args.toTag === "string" ? args.toTag : undefined;
61
+ const syncArgs = {
62
+ sourcePath: typeof args.sourcePath === "string" ? args.sourcePath : undefined,
63
+ archivePath: transcriptsRoot
64
+ };
61
65
  try {
62
- const result = await runGenerateRecommendations(ctx, { transcriptsRoot, fromTag, toTag });
66
+ const state = await loadImprovementState(ctx.workspacePath);
67
+ const sync = await runSyncTranscripts(ctx, syncArgs, state);
68
+ state.lastSyncRunAt = new Date().toISOString();
69
+ await saveImprovementState(ctx.workspacePath, state);
70
+ const result = await runGenerateRecommendations(ctx, {
71
+ transcriptsRoot: sync.archivePath,
72
+ fromTag,
73
+ toTag
74
+ });
63
75
  return {
64
76
  ok: true,
65
77
  code: "recommendations-generated",
66
- message: `Created ${result.created.length} improvement task(s); skipped ${result.skipped} duplicate(s)`,
67
- data: result
78
+ message: `After sync (${sync.copied} copied): created ${result.created.length} improvement task(s); skipped ${result.skipped} duplicate(s)`,
79
+ data: { sync, ...result }
68
80
  };
69
81
  }
70
82
  catch (e) {
@@ -47,6 +47,11 @@ type ImprovementTranscriptConfig = {
47
47
  discoveryPaths: string[];
48
48
  };
49
49
  export declare function resolveImprovementTranscriptConfig(ctx: ModuleLifecycleContext, args: TranscriptSyncArgs): ImprovementTranscriptConfig;
50
+ /**
51
+ * Cursor stores agent transcripts under `~/.cursor/projects/<slug>/agent-transcripts`, where `slug`
52
+ * is the workspace root with path separators replaced by hyphens (drive letter included on Windows).
53
+ */
54
+ export declare function buildCursorProjectsAgentTranscriptsPath(workspacePath: string): string;
50
55
  export declare function resolveTranscriptSourceRoot(workspacePath: string, cfg: ImprovementTranscriptConfig, args: TranscriptSyncArgs): Promise<{
51
56
  root: string;
52
57
  discoveredFrom: string;
@@ -1,4 +1,5 @@
1
1
  import fs from "node:fs/promises";
2
+ import os from "node:os";
2
3
  import path from "node:path";
3
4
  import { createHash, randomUUID } from "node:crypto";
4
5
  const DEFAULT_DISCOVERY_PATHS = [".cursor/agent-transcripts", ".vscode/agent-transcripts"];
@@ -58,6 +59,16 @@ async function pathExists(abs) {
58
59
  return false;
59
60
  }
60
61
  }
62
+ /**
63
+ * Cursor stores agent transcripts under `~/.cursor/projects/<slug>/agent-transcripts`, where `slug`
64
+ * is the workspace root with path separators replaced by hyphens (drive letter included on Windows).
65
+ */
66
+ export function buildCursorProjectsAgentTranscriptsPath(workspacePath) {
67
+ const home = os.homedir();
68
+ const resolved = path.resolve(workspacePath);
69
+ const slug = resolved.split(path.sep).filter((s) => s.length > 0).join("-");
70
+ return path.join(home, ".cursor", "projects", slug, "agent-transcripts");
71
+ }
61
72
  export async function resolveTranscriptSourceRoot(workspacePath, cfg, args) {
62
73
  const sourcePathArg = typeof args.sourcePath === "string" ? args.sourcePath.trim() : "";
63
74
  if (sourcePathArg) {
@@ -76,6 +87,12 @@ export async function resolveTranscriptSourceRoot(workspacePath, cfg, args) {
76
87
  return { root: abs, discoveredFrom: rel, tried };
77
88
  }
78
89
  }
90
+ const cursorGlobal = buildCursorProjectsAgentTranscriptsPath(workspacePath);
91
+ const cursorLabel = path.join("~", ".cursor", "projects", path.basename(path.dirname(cursorGlobal)), "agent-transcripts");
92
+ tried.push(cursorLabel);
93
+ if (await pathExists(cursorGlobal)) {
94
+ return { root: cursorGlobal, discoveredFrom: "cursor-global-project-agent-transcripts", tried };
95
+ }
79
96
  const fallback = path.resolve(workspacePath, ".cursor/agent-transcripts");
80
97
  return { root: fallback, discoveredFrom: ".cursor/agent-transcripts (fallback)", tried };
81
98
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@workflow-cannon/workspace-kit",
3
- "version": "0.8.0",
3
+ "version": "0.10.0",
4
4
  "private": false,
5
5
  "packageManager": "pnpm@10.0.0",
6
6
  "license": "MIT",
@@ -42,6 +42,7 @@
42
42
  },
43
43
  "files": [
44
44
  "dist",
45
+ "src/modules/documentation",
45
46
  "package.json"
46
47
  ]
47
48
  }
@@ -0,0 +1,39 @@
1
+ # Documentation Module
2
+
3
+ Translates between canonical AI-optimized documentation and human-readable maintainership docs.
4
+
5
+ Primary responsibilities:
6
+
7
+ - maintain parity between `/.ai` and configured human docs roots (default: `docs/maintainers`)
8
+ - execute instruction-file driven documentation generation
9
+ - record deterministic documentation generation state and evidence
10
+
11
+ See `src/modules/documentation/RULES.md` for the canonical usage order and validation rules.
12
+
13
+ ## Callable commands
14
+
15
+ Registered on the documentation module and dispatched through `src/core/module-command-router.ts`:
16
+
17
+ - `document-project` — batch: generate **all** project docs from the template library. Outputs AI docs to `.ai/` (preserving existing) and human docs to `docs/maintainers/` (overwriting). Continues through failures; reports batch summary. See `instructions/document-project.md`.
18
+ - `generate-document` — single: generate **one** document by `documentType`. See `instructions/generate-document.md`.
19
+
20
+ ## Shipped templates
21
+
22
+ Files under `templates/`; `documentType` is the filename (basename). Keep this list aligned with `instructions/document-project.md` **Inputs**:
23
+
24
+ - `AGENTS.md`
25
+ - `ARCHITECTURE.md`
26
+ - `PRINCIPLES.md`
27
+ - `RELEASING.md`
28
+ - `ROADMAP.md`
29
+ - `SECURITY.md`
30
+ - `SUPPORT.md`
31
+ - `TERMS.md`
32
+ - `runbooks/parity-validation-flow.md`
33
+ - `runbooks/consumer-cadence.md`
34
+ - `runbooks/release-channels.md`
35
+ - `workbooks/transcript-automation-baseline.md`
36
+ - `workbooks/phase2-config-policy-workbook.md`
37
+ - `workbooks/task-engine-workbook.md`
38
+
39
+ Adding a new template: add `templates/<Name>.md`, extend the **Inputs** list in `instructions/document-project.md`, and add the same line here.
@@ -0,0 +1,70 @@
1
+ # Documentation Module Rules
2
+
3
+ Single entrypoint for how to use the documentation module.
4
+
5
+ ## Precedence Order
6
+
7
+ When guidance conflicts, apply this order:
8
+
9
+ 1. `/.ai/PRINCIPLES.md` (global governance and approval gates)
10
+ 2. `/.ai/module-build.md` (module development and validation rules)
11
+ 3. `src/modules/documentation/config.md` (module path policy and generation behavior)
12
+ 4. `src/modules/documentation/instructions/document-project.md` (document generation workflow)
13
+ 5. `src/modules/documentation/instructions/documentation-maintainer.md` (AI-doc generation policy)
14
+ 6. `src/modules/documentation/schemas/documentation-schema.md` (record schema contract)
15
+ 7. `src/modules/documentation/templates/*.md` (document-type generation templates)
16
+ 8. `docs/maintainers/module-build-guide.md` (human-readable companion guidance)
17
+
18
+ ## Usage Model
19
+
20
+ - Choose an instruction entry from `instructions/` for the operation you want.
21
+ - Discover available callable module operations through `src/core/module-command-router.ts` command listing.
22
+ - Load module config first and restrict writes to configured document paths.
23
+ - If a matching template exists, use it as the structure contract.
24
+ - For templates with `{{{ ... }}}` blocks, treat block content as generation instructions, not output text.
25
+ - Generate AI-surface content first, then generate human-surface content from that result plus project context.
26
+
27
+ ## Command Contracts
28
+
29
+ ### `document-project(options)` — batch
30
+
31
+ Generates all project docs by iterating every `.md` template in `sources.templatesRoot`.
32
+
33
+ - Default behavior: **preserve AI docs** (`overwriteAi: false`), **overwrite human docs** (`overwriteHuman: true`), continue on individual failure.
34
+ - Returns batch summary with total/succeeded/failed/skipped counts plus per-document results.
35
+
36
+ ### `generate-document(documentType, options)` — single
37
+
38
+ Generates one document pair (AI + human) for the given `documentType`.
39
+
40
+ - `documentType`: required string basename resolving to `<templatesRoot>/<documentType>`.
41
+ - `options`:
42
+ - `dryRun?: boolean` (default `false`) - compute outputs/validations without writing files
43
+ - `overwrite?: boolean` (default `true`) - allow replacing existing files (both surfaces)
44
+ - `overwriteAi?: boolean` - override `overwrite` for AI surface only
45
+ - `overwriteHuman?: boolean` - override `overwrite` for human surface only
46
+ - `strict?: boolean` (default `true`) - fail on unresolved warnings (validation/conflict/coverage)
47
+ - `maxValidationAttempts?: number` (default from config) - override retry limit
48
+ - `allowWithoutTemplate?: boolean` (default `false`) - continue without template only when explicitly confirmed
49
+ - Shipped template basenames are listed in `instructions/generate-document.md` and `instructions/document-project.md`.
50
+
51
+ ### Shared semantics
52
+
53
+ - Both commands read paths from module config (`sources.aiRoot`, `sources.humanRoot`, `sources.templatesRoot`, `sources.instructionsRoot`, `sources.schemasRoot`).
54
+ - Both enforce write boundaries to configured output roots only.
55
+ - Both execute AI generation first, then human generation from AI output plus project context.
56
+ - Both return evidence objects containing files read/written, validations, retries, warnings/conflicts, and timestamp.
57
+
58
+ ## Required Validation
59
+
60
+ - Validate AI output against `schemas/documentation-schema.md`.
61
+ - Verify section coverage for templated documents and ensure no unresolved `{{{` blocks remain.
62
+ - Detect conflicts against higher-precedence sources and stop/prompt when required.
63
+ - Emit run evidence (inputs, outputs, validation results, timestamp).
64
+
65
+ ## Missing Template Behavior
66
+
67
+ - If a template for the requested document type is missing:
68
+ - warn the user
69
+ - ask whether to continue without a template
70
+ - continue only with explicit confirmation
@@ -0,0 +1,14 @@
1
+ # Documentation Module Config
2
+
3
+ Defines module-level configuration keys used by the documentation module.
4
+
5
+ - `sources.aiRoot`: canonical AI docs root (default: `/.ai`)
6
+ - `sources.humanRoot`: human docs root (default: `docs/maintainers`)
7
+ - `sources.templatesRoot`: document template root (default: `src/modules/documentation/templates`)
8
+ - `sources.instructionsRoot`: instruction root (default: `src/modules/documentation/instructions`)
9
+ - `sources.schemasRoot`: schema root (default: `src/modules/documentation/schemas`)
10
+ - `generation.enforceParity`: fail generation when AI and human surfaces diverge
11
+ - `generation.enforceSectionCoverage`: fail when expected template sections are missing in output
12
+ - `generation.resolveOrRetryOnValidationError`: auto-resolve issues or retry generation before returning failure
13
+ - `generation.maxValidationAttempts`: maximum validate/retry attempts (default: `3`)
14
+ - `generation.allowTemplatelessFallback`: when template is missing, require user confirmation before continuing
@@ -0,0 +1,120 @@
1
+ import type { WorkflowModule } from "../../contracts/module-contract.js";
2
+ import { generateDocument, generateAllDocuments } from "./runtime.js";
3
+ export type {
4
+ DocumentationBatchResult,
5
+ DocumentationConflict,
6
+ DocumentationGenerateOptions,
7
+ DocumentationGenerateResult,
8
+ DocumentationGenerationEvidence,
9
+ DocumentationValidationIssue
10
+ } from "./types.js";
11
+ import type { DocumentationGenerateOptions } from "./types.js";
12
+
13
+ function parseOptions(raw: Record<string, unknown>): DocumentationGenerateOptions {
14
+ return {
15
+ dryRun: typeof raw.dryRun === "boolean" ? raw.dryRun : undefined,
16
+ overwrite: typeof raw.overwrite === "boolean" ? raw.overwrite : undefined,
17
+ overwriteAi: typeof raw.overwriteAi === "boolean" ? raw.overwriteAi : undefined,
18
+ overwriteHuman: typeof raw.overwriteHuman === "boolean" ? raw.overwriteHuman : undefined,
19
+ strict: typeof raw.strict === "boolean" ? raw.strict : undefined,
20
+ maxValidationAttempts:
21
+ typeof raw.maxValidationAttempts === "number" ? raw.maxValidationAttempts : undefined,
22
+ allowWithoutTemplate:
23
+ typeof raw.allowWithoutTemplate === "boolean" ? raw.allowWithoutTemplate : undefined,
24
+ };
25
+ }
26
+
27
+ export const documentationModule: WorkflowModule = {
28
+ registration: {
29
+ id: "documentation",
30
+ version: "0.2.0",
31
+ contractVersion: "1",
32
+ capabilities: ["documentation"],
33
+ dependsOn: [],
34
+ enabledByDefault: true,
35
+ config: {
36
+ path: "src/modules/documentation/config.md",
37
+ format: "md",
38
+ description: "Documentation module configuration contract."
39
+ },
40
+ state: {
41
+ path: "src/modules/documentation/state.md",
42
+ format: "md",
43
+ description: "Documentation module generation/runtime state contract."
44
+ },
45
+ instructions: {
46
+ directory: "src/modules/documentation/instructions",
47
+ entries: [
48
+ {
49
+ name: "document-project",
50
+ file: "document-project.md",
51
+ description: "Generate all project docs from templates to .ai and docs/maintainers surfaces."
52
+ },
53
+ {
54
+ name: "generate-document",
55
+ file: "generate-document.md",
56
+ description: "Generate a single document by type for .ai and docs/maintainers surfaces."
57
+ }
58
+ ]
59
+ }
60
+ },
61
+ async onCommand(command, ctx) {
62
+ const args = command.args ?? {};
63
+ const rawOptions: Record<string, unknown> =
64
+ typeof args.options === "object" && args.options !== null
65
+ ? (args.options as Record<string, unknown>)
66
+ : {};
67
+ const options = parseOptions(rawOptions);
68
+
69
+ if (command.name === "document-project") {
70
+ const batchResult = await generateAllDocuments({ options }, ctx);
71
+ return {
72
+ ok: batchResult.ok,
73
+ code: batchResult.ok ? "documented-project" : "documentation-batch-failed",
74
+ message: batchResult.ok
75
+ ? `Generated ${batchResult.summary.succeeded} documents (${batchResult.summary.skipped} skipped)`
76
+ : `Batch failed: ${batchResult.summary.failed} of ${batchResult.summary.total} documents failed`,
77
+ data: {
78
+ summary: batchResult.summary,
79
+ results: batchResult.results.map((r) => ({
80
+ documentType: r.evidence.documentType,
81
+ ok: r.ok,
82
+ aiOutputPath: r.aiOutputPath,
83
+ humanOutputPath: r.humanOutputPath,
84
+ filesWritten: r.evidence.filesWritten,
85
+ filesSkipped: r.evidence.filesSkipped,
86
+ }))
87
+ }
88
+ };
89
+ }
90
+
91
+ if (command.name === "generate-document") {
92
+ const result = await generateDocument(
93
+ {
94
+ documentType: typeof args.documentType === "string" ? args.documentType : undefined,
95
+ options
96
+ },
97
+ ctx
98
+ );
99
+
100
+ return {
101
+ ok: result.ok,
102
+ code: result.ok ? "generated-document" : "generation-failed",
103
+ message: result.ok
104
+ ? `Generated document '${args.documentType ?? "unknown"}'`
105
+ : `Failed to generate document '${args.documentType ?? "unknown"}'`,
106
+ data: {
107
+ aiOutputPath: result.aiOutputPath,
108
+ humanOutputPath: result.humanOutputPath,
109
+ evidence: result.evidence
110
+ }
111
+ };
112
+ }
113
+
114
+ return {
115
+ ok: false,
116
+ code: "unsupported-command",
117
+ message: `Documentation module does not support command '${command.name}'`
118
+ };
119
+ }
120
+ };
@@ -0,0 +1,44 @@
1
+ # document-project
2
+
3
+ Generate all project documentation by running `generate-document` for every template in the template library. Outputs AI-optimized docs to `.ai/` and human-readable docs to `docs/maintainers/`.
4
+
5
+ ## Inputs
6
+
7
+ - `options` (all optional):
8
+ - `dryRun?: boolean` — compute outputs/validations without writing files
9
+ - `overwriteAi?: boolean` — overwrite existing AI docs (default `false`)
10
+ - `overwriteHuman?: boolean` — overwrite existing human docs (default `true`)
11
+ - `strict?: boolean` — fail on unresolved warnings (default `false` in batch mode)
12
+ - `maxValidationAttempts?: number` — override retry limit per document
13
+
14
+ ## Shipped templates
15
+
16
+ All `.md` files under `sources.templatesRoot` (default `src/modules/documentation/templates`) are processed:
17
+
18
+ - `AGENTS.md`
19
+ - `ARCHITECTURE.md`
20
+ - `PRINCIPLES.md`
21
+ - `RELEASING.md`
22
+ - `ROADMAP.md`
23
+ - `SECURITY.md`
24
+ - `SUPPORT.md`
25
+ - `TERMS.md`
26
+ - `runbooks/parity-validation-flow.md`
27
+ - `runbooks/consumer-cadence.md`
28
+ - `runbooks/release-channels.md`
29
+ - `workbooks/transcript-automation-baseline.md`
30
+ - `workbooks/phase2-config-policy-workbook.md`
31
+ - `workbooks/task-engine-workbook.md`
32
+
33
+ ## Required behavior
34
+
35
+ 1. Discover all `.md` templates in `sources.templatesRoot`.
36
+ 2. For each template, invoke `generate-document` with the template basename as `documentType`.
37
+ 3. Default overwrite behavior: **preserve AI docs** (`overwriteAi: false`), **overwrite human docs** (`overwriteHuman: true`).
38
+ 4. Continue through all templates on individual failure; do not stop the batch.
39
+ 5. Collect per-document results and emit a batch summary with total/succeeded/failed/skipped counts.
40
+ 6. Return `ok: true` only when zero documents failed.
41
+
42
+ ## Skipped AI doc handling
43
+
44
+ When AI docs are skipped because they already exist (`filesSkipped` is non-empty in a document result), the calling agent **must** prompt the user before overwriting. Present the list of skipped AI doc paths and ask for explicit confirmation. If confirmed, re-run with `overwriteAi: true` for those documents.