@workflow-cannon/workspace-kit 0.5.0 → 0.7.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.md CHANGED
@@ -63,7 +63,8 @@ This keeps automation adaptive without sacrificing safety, governance, or develo
63
63
 
64
64
  - **Phase 0** and **Phase 1** (task engine, `v0.3.0`) are complete.
65
65
  - **Phase 2** (layered config, policy gates, cutover docs, `v0.4.0`) is complete in-repo; see `docs/maintainers/TASKS.md` and `docs/maintainers/ROADMAP.md`.
66
- - **Phase 2b** (policy + config UX, `v0.4.1`) and **Phase 3** (enhancement loop MVP, `v0.5.0`) are complete in-repo: evidence-driven **improvement** tasks, **`approvals`** (`review-item`), heuristic confidence, and append-only lineage. **Phase 4** (`v0.6.0`, `T193`+) is next.
66
+ - **Phase 2b** (policy + config UX, `v0.4.1`) and **Phase 3** (enhancement loop MVP, `v0.5.0`) are complete in-repo: evidence-driven **improvement** tasks, **`approvals`** (`review-item`), heuristic confidence, and append-only lineage.
67
+ - **Phase 4** (`v0.6.0`) is complete in-repo: compatibility matrix/gates, diagnostics/SLO baseline evidence, release-channel mapping, and planning-doc consistency checks.
67
68
 
68
69
  ## Goals
69
70
 
@@ -28,6 +28,58 @@ const REGISTRY = {
28
28
  requiresApproval: true,
29
29
  exposure: "maintainer",
30
30
  writableLayers: ["project"]
31
+ },
32
+ "improvement.transcripts.sourcePath": {
33
+ key: "improvement.transcripts.sourcePath",
34
+ type: "string",
35
+ description: "Relative path to transcript JSONL source files for sync operations.",
36
+ default: ".cursor/agent-transcripts",
37
+ domainScope: "project",
38
+ owningModule: "improvement",
39
+ sensitive: false,
40
+ requiresRestart: false,
41
+ requiresApproval: false,
42
+ exposure: "public",
43
+ writableLayers: ["project", "user"]
44
+ },
45
+ "improvement.transcripts.archivePath": {
46
+ key: "improvement.transcripts.archivePath",
47
+ type: "string",
48
+ description: "Relative local archive path where synced transcript JSONL files are copied.",
49
+ default: "agent-transcripts",
50
+ domainScope: "project",
51
+ owningModule: "improvement",
52
+ sensitive: false,
53
+ requiresRestart: false,
54
+ requiresApproval: false,
55
+ exposure: "public",
56
+ writableLayers: ["project", "user"]
57
+ },
58
+ "improvement.cadence.minIntervalMinutes": {
59
+ key: "improvement.cadence.minIntervalMinutes",
60
+ type: "number",
61
+ description: "Minimum minutes between one-shot ingest recommendation generation runs.",
62
+ default: 15,
63
+ domainScope: "project",
64
+ owningModule: "improvement",
65
+ sensitive: false,
66
+ requiresRestart: false,
67
+ requiresApproval: false,
68
+ exposure: "maintainer",
69
+ writableLayers: ["project", "user"]
70
+ },
71
+ "improvement.cadence.skipIfNoNewTranscripts": {
72
+ key: "improvement.cadence.skipIfNoNewTranscripts",
73
+ type: "boolean",
74
+ description: "Skip recommendation generation when transcript sync copies no new files.",
75
+ default: true,
76
+ domainScope: "project",
77
+ owningModule: "improvement",
78
+ sensitive: false,
79
+ requiresRestart: false,
80
+ requiresApproval: false,
81
+ exposure: "maintainer",
82
+ writableLayers: ["project", "user"]
31
83
  }
32
84
  };
33
85
  export function getConfigKeyMetadata(key) {
@@ -102,7 +154,15 @@ function deepEqualLoose(a, b) {
102
154
  * Validate top-level shape of persisted kit config files (strict unknown-key rejection).
103
155
  */
104
156
  export function validatePersistedConfigDocument(data, label) {
105
- const allowed = new Set(["schemaVersion", "core", "tasks", "documentation", "policy", "modules"]);
157
+ const allowed = new Set([
158
+ "schemaVersion",
159
+ "core",
160
+ "tasks",
161
+ "documentation",
162
+ "policy",
163
+ "improvement",
164
+ "modules"
165
+ ]);
106
166
  for (const k of Object.keys(data)) {
107
167
  if (!allowed.has(k)) {
108
168
  throw new Error(`config-invalid(${label}): unknown top-level key '${k}'`);
@@ -156,6 +216,54 @@ export function validatePersistedConfigDocument(data, label) {
156
216
  throw new Error(`config-invalid(${label}): modules must be absent or an empty object`);
157
217
  }
158
218
  }
219
+ const improvement = data.improvement;
220
+ if (improvement !== undefined) {
221
+ if (typeof improvement !== "object" || improvement === null || Array.isArray(improvement)) {
222
+ throw new Error(`config-invalid(${label}): improvement must be an object`);
223
+ }
224
+ const imp = improvement;
225
+ for (const k of Object.keys(imp)) {
226
+ if (k !== "transcripts" && k !== "cadence") {
227
+ throw new Error(`config-invalid(${label}): unknown improvement.${k}`);
228
+ }
229
+ }
230
+ if (imp.transcripts !== undefined) {
231
+ if (typeof imp.transcripts !== "object" ||
232
+ imp.transcripts === null ||
233
+ Array.isArray(imp.transcripts)) {
234
+ throw new Error(`config-invalid(${label}): improvement.transcripts must be an object`);
235
+ }
236
+ const tr = imp.transcripts;
237
+ for (const k of Object.keys(tr)) {
238
+ if (k !== "sourcePath" && k !== "archivePath") {
239
+ throw new Error(`config-invalid(${label}): unknown improvement.transcripts.${k}`);
240
+ }
241
+ }
242
+ if (tr.sourcePath !== undefined) {
243
+ validateValueForMetadata(REGISTRY["improvement.transcripts.sourcePath"], tr.sourcePath);
244
+ }
245
+ if (tr.archivePath !== undefined) {
246
+ validateValueForMetadata(REGISTRY["improvement.transcripts.archivePath"], tr.archivePath);
247
+ }
248
+ }
249
+ if (imp.cadence !== undefined) {
250
+ if (typeof imp.cadence !== "object" || imp.cadence === null || Array.isArray(imp.cadence)) {
251
+ throw new Error(`config-invalid(${label}): improvement.cadence must be an object`);
252
+ }
253
+ const cd = imp.cadence;
254
+ for (const k of Object.keys(cd)) {
255
+ if (k !== "minIntervalMinutes" && k !== "skipIfNoNewTranscripts") {
256
+ throw new Error(`config-invalid(${label}): unknown improvement.cadence.${k}`);
257
+ }
258
+ }
259
+ if (cd.minIntervalMinutes !== undefined) {
260
+ validateValueForMetadata(REGISTRY["improvement.cadence.minIntervalMinutes"], cd.minIntervalMinutes);
261
+ }
262
+ if (cd.skipIfNoNewTranscripts !== undefined) {
263
+ validateValueForMetadata(REGISTRY["improvement.cadence.skipIfNoNewTranscripts"], cd.skipIfNoNewTranscripts);
264
+ }
265
+ }
266
+ }
159
267
  }
160
268
  export function getConfigRegistryExport() {
161
269
  return REGISTRY;
@@ -1,5 +1,5 @@
1
1
  export declare const POLICY_TRACE_SCHEMA_VERSION: 1;
2
- export type PolicyOperationId = "cli.upgrade" | "cli.init" | "cli.config-mutate" | "policy.dynamic-sensitive" | "doc.document-project" | "doc.generate-document" | "tasks.import-tasks" | "tasks.generate-tasks-md" | "tasks.run-transition" | "approvals.review-item" | "improvement.generate-recommendations";
2
+ export type PolicyOperationId = "cli.upgrade" | "cli.init" | "cli.config-mutate" | "policy.dynamic-sensitive" | "doc.document-project" | "doc.generate-document" | "tasks.import-tasks" | "tasks.generate-tasks-md" | "tasks.run-transition" | "approvals.review-item" | "improvement.generate-recommendations" | "improvement.ingest-transcripts";
3
3
  export declare function getOperationIdForCommand(commandName: string): PolicyOperationId | undefined;
4
4
  export declare function getExtraSensitiveModuleCommandsFromEffective(effective: Record<string, unknown>): string[];
5
5
  /** Resolve operation id for tracing, including config-declared sensitive module commands. */
@@ -9,7 +9,8 @@ const COMMAND_TO_OPERATION = {
9
9
  "generate-tasks-md": "tasks.generate-tasks-md",
10
10
  "run-transition": "tasks.run-transition",
11
11
  "review-item": "approvals.review-item",
12
- "generate-recommendations": "improvement.generate-recommendations"
12
+ "generate-recommendations": "improvement.generate-recommendations",
13
+ "ingest-transcripts": "improvement.ingest-transcripts"
13
14
  };
14
15
  export function getOperationIdForCommand(commandName) {
15
16
  return COMMAND_TO_OPERATION[commandName];
@@ -13,7 +13,17 @@ export const KIT_CONFIG_DEFAULTS = {
13
13
  tasks: {
14
14
  storeRelativePath: ".workspace-kit/tasks/state.json"
15
15
  },
16
- documentation: {}
16
+ documentation: {},
17
+ improvement: {
18
+ transcripts: {
19
+ sourcePath: ".cursor/agent-transcripts",
20
+ archivePath: "agent-transcripts"
21
+ },
22
+ cadence: {
23
+ minIntervalMinutes: 15,
24
+ skipIfNoNewTranscripts: true
25
+ }
26
+ }
17
27
  };
18
28
  /**
19
29
  * Static module-level defaults keyed by module id (merged in registry startup order).
@@ -30,7 +40,16 @@ export const MODULE_CONFIG_CONTRIBUTIONS = {
30
40
  },
31
41
  approvals: {},
32
42
  planning: {},
33
- improvement: {}
43
+ improvement: {
44
+ transcripts: {
45
+ sourcePath: ".cursor/agent-transcripts",
46
+ archivePath: "agent-transcripts"
47
+ },
48
+ cadence: {
49
+ minIntervalMinutes: 15,
50
+ skipIfNoNewTranscripts: true
51
+ }
52
+ }
34
53
  };
35
54
  export function deepMerge(target, source) {
36
55
  const out = { ...target };
@@ -19,13 +19,24 @@ function hasEvidenceKey(tasks, key) {
19
19
  return m.evidenceKey === key;
20
20
  });
21
21
  }
22
+ function resolveTranscriptArchivePath(ctx, args) {
23
+ if (typeof args.transcriptsRoot === "string" && args.transcriptsRoot.trim().length > 0) {
24
+ return args.transcriptsRoot.trim();
25
+ }
26
+ const improvement = ctx.effectiveConfig?.improvement && typeof ctx.effectiveConfig.improvement === "object"
27
+ ? ctx.effectiveConfig.improvement
28
+ : {};
29
+ const transcripts = improvement.transcripts && typeof improvement.transcripts === "object"
30
+ ? improvement.transcripts
31
+ : {};
32
+ const archivePath = typeof transcripts.archivePath === "string" ? transcripts.archivePath.trim() : "";
33
+ return archivePath || "agent-transcripts";
34
+ }
22
35
  export async function runGenerateRecommendations(ctx, args) {
23
36
  const store = new TaskStore(ctx.workspacePath, taskStoreRelativePath(ctx));
24
37
  await store.load();
25
38
  const state = await loadImprovementState(ctx.workspacePath);
26
- const transcriptsRoot = typeof args.transcriptsRoot === "string" && args.transcriptsRoot.trim()
27
- ? args.transcriptsRoot.trim()
28
- : "agent-transcripts";
39
+ const transcriptsRoot = resolveTranscriptArchivePath(ctx, args);
29
40
  const fromTag = typeof args.fromTag === "string" ? args.fromTag.trim() : undefined;
30
41
  const toTag = typeof args.toTag === "string" ? args.toTag.trim() : undefined;
31
42
  const candidates = [];
@@ -5,6 +5,8 @@ export type ImprovementStateDocument = {
5
5
  mutationLineCursor: number;
6
6
  transitionLogLengthCursor: number;
7
7
  transcriptLineCursors: Record<string, number>;
8
+ lastSyncRunAt: string | null;
9
+ lastIngestRunAt: string | null;
8
10
  };
9
11
  export declare function emptyImprovementState(): ImprovementStateDocument;
10
12
  export declare function loadImprovementState(workspacePath: string): Promise<ImprovementStateDocument>;
@@ -11,7 +11,9 @@ export function emptyImprovementState() {
11
11
  policyTraceLineCursor: 0,
12
12
  mutationLineCursor: 0,
13
13
  transitionLogLengthCursor: 0,
14
- transcriptLineCursors: {}
14
+ transcriptLineCursors: {},
15
+ lastSyncRunAt: null,
16
+ lastIngestRunAt: null
15
17
  };
16
18
  }
17
19
  export async function loadImprovementState(workspacePath) {
@@ -1,9 +1,11 @@
1
1
  import { queryLineageChain } from "../../core/lineage-store.js";
2
2
  import { runGenerateRecommendations } from "./generate-recommendations-runtime.js";
3
+ import { resolveCadenceDecision, runSyncTranscripts } from "./transcript-sync-runtime.js";
4
+ import { loadImprovementState, saveImprovementState } from "./improvement-state.js";
3
5
  export const improvementModule = {
4
6
  registration: {
5
7
  id: "improvement",
6
- version: "0.5.0",
8
+ version: "0.7.0",
7
9
  contractVersion: "1",
8
10
  capabilities: ["improvement"],
9
11
  dependsOn: ["task-engine", "planning"],
@@ -30,6 +32,16 @@ export const improvementModule = {
30
32
  name: "query-lineage",
31
33
  file: "query-lineage.md",
32
34
  description: "Reconstruct lineage chain for a recommendation task id."
35
+ },
36
+ {
37
+ name: "sync-transcripts",
38
+ file: "sync-transcripts.md",
39
+ description: "Sync local transcript JSONL files into the archive."
40
+ },
41
+ {
42
+ name: "ingest-transcripts",
43
+ file: "ingest-transcripts.md",
44
+ description: "Run transcript sync and recommendation generation in one flow."
33
45
  }
34
46
  ]
35
47
  }
@@ -54,6 +66,81 @@ export const improvementModule = {
54
66
  return { ok: false, code: "generate-failed", message: msg };
55
67
  }
56
68
  }
69
+ if (command.name === "sync-transcripts") {
70
+ const syncArgs = {
71
+ sourcePath: typeof args.sourcePath === "string" ? args.sourcePath : undefined,
72
+ archivePath: typeof args.archivePath === "string" ? args.archivePath : undefined
73
+ };
74
+ try {
75
+ const sync = await runSyncTranscripts(ctx, syncArgs);
76
+ const state = await loadImprovementState(ctx.workspacePath);
77
+ state.lastSyncRunAt = new Date().toISOString();
78
+ await saveImprovementState(ctx.workspacePath, state);
79
+ return {
80
+ ok: true,
81
+ code: "transcripts-synced",
82
+ message: `Copied ${sync.copied} transcript file(s); skipped ${sync.skippedExisting} existing`,
83
+ data: sync
84
+ };
85
+ }
86
+ catch (e) {
87
+ const msg = e instanceof Error ? e.message : String(e);
88
+ return { ok: false, code: "sync-failed", message: msg };
89
+ }
90
+ }
91
+ if (command.name === "ingest-transcripts") {
92
+ const syncArgs = {
93
+ sourcePath: typeof args.sourcePath === "string" ? args.sourcePath : undefined,
94
+ archivePath: typeof args.archivePath === "string" ? args.archivePath : undefined
95
+ };
96
+ const now = new Date();
97
+ try {
98
+ const sync = await runSyncTranscripts(ctx, syncArgs);
99
+ const state = await loadImprovementState(ctx.workspacePath);
100
+ const improvement = ctx.effectiveConfig?.improvement && typeof ctx.effectiveConfig.improvement === "object"
101
+ ? ctx.effectiveConfig.improvement
102
+ : {};
103
+ const cadence = improvement.cadence && typeof improvement.cadence === "object"
104
+ ? improvement.cadence
105
+ : {};
106
+ const minIntervalMinutes = typeof cadence.minIntervalMinutes === "number" && Number.isFinite(cadence.minIntervalMinutes)
107
+ ? Math.max(1, Math.floor(cadence.minIntervalMinutes))
108
+ : 15;
109
+ const skipIfNoNewTranscripts = typeof cadence.skipIfNoNewTranscripts === "boolean"
110
+ ? cadence.skipIfNoNewTranscripts
111
+ : true;
112
+ const cadenceDecision = resolveCadenceDecision(now, state.lastIngestRunAt, minIntervalMinutes, sync.copied, skipIfNoNewTranscripts);
113
+ state.lastSyncRunAt = now.toISOString();
114
+ const generate = cadenceDecision.shouldRunGenerate || args.forceGenerate === true || args.runGenerate === true;
115
+ let recommendations = null;
116
+ if (generate) {
117
+ recommendations = await runGenerateRecommendations(ctx, {
118
+ transcriptsRoot: sync.archivePath
119
+ });
120
+ state.lastIngestRunAt = now.toISOString();
121
+ }
122
+ await saveImprovementState(ctx.workspacePath, state);
123
+ const status = generate ? "generated" : "skipped";
124
+ return {
125
+ ok: true,
126
+ code: "transcripts-ingested",
127
+ message: `Ingest ${status}; sync copied ${sync.copied} file(s)`,
128
+ data: {
129
+ sync,
130
+ cadence: {
131
+ minIntervalMinutes,
132
+ skipIfNoNewTranscripts,
133
+ decision: cadenceDecision.reason
134
+ },
135
+ generatedRecommendations: recommendations
136
+ }
137
+ };
138
+ }
139
+ catch (e) {
140
+ const msg = e instanceof Error ? e.message : String(e);
141
+ return { ok: false, code: "ingest-failed", message: msg };
142
+ }
143
+ }
57
144
  if (command.name === "query-lineage") {
58
145
  const taskId = typeof args.taskId === "string" ? args.taskId.trim() : "";
59
146
  if (!taskId) {
@@ -0,0 +1,24 @@
1
+ import type { ModuleLifecycleContext } from "../../contracts/module-contract.js";
2
+ export type TranscriptSyncArgs = {
3
+ sourcePath?: string;
4
+ archivePath?: string;
5
+ };
6
+ export type TranscriptSyncResult = {
7
+ sourcePath: string;
8
+ archivePath: string;
9
+ scanned: number;
10
+ copied: number;
11
+ skippedExisting: number;
12
+ skippedConflict: number;
13
+ errors: Array<{
14
+ file: string;
15
+ code: string;
16
+ message: string;
17
+ }>;
18
+ copiedFiles: string[];
19
+ };
20
+ export declare function runSyncTranscripts(ctx: ModuleLifecycleContext, args: TranscriptSyncArgs): Promise<TranscriptSyncResult>;
21
+ export declare function resolveCadenceDecision(now: Date, previousRunAtIso: string | null, minIntervalMinutes: number, copiedCount: number, skipIfNoNewTranscripts: boolean): {
22
+ shouldRunGenerate: boolean;
23
+ reason: string;
24
+ };
@@ -0,0 +1,131 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { createHash } from "node:crypto";
4
+ function resolveImprovementTranscriptConfig(ctx, args) {
5
+ const improvement = ctx.effectiveConfig?.improvement && typeof ctx.effectiveConfig.improvement === "object"
6
+ ? ctx.effectiveConfig.improvement
7
+ : {};
8
+ const transcripts = improvement.transcripts && typeof improvement.transcripts === "object"
9
+ ? improvement.transcripts
10
+ : {};
11
+ const cadence = improvement.cadence && typeof improvement.cadence === "object"
12
+ ? improvement.cadence
13
+ : {};
14
+ const sourcePathArg = typeof args.sourcePath === "string" ? args.sourcePath.trim() : "";
15
+ const archivePathArg = typeof args.archivePath === "string" ? args.archivePath.trim() : "";
16
+ const sourcePathCfg = typeof transcripts.sourcePath === "string" ? transcripts.sourcePath.trim() : "";
17
+ const archivePathCfg = typeof transcripts.archivePath === "string" ? transcripts.archivePath.trim() : "";
18
+ const minIntervalCfg = typeof cadence.minIntervalMinutes === "number" && Number.isFinite(cadence.minIntervalMinutes)
19
+ ? cadence.minIntervalMinutes
20
+ : 15;
21
+ const skipIfNoNewCfg = typeof cadence.skipIfNoNewTranscripts === "boolean" ? cadence.skipIfNoNewTranscripts : true;
22
+ return {
23
+ sourcePath: sourcePathArg || sourcePathCfg || ".cursor/agent-transcripts",
24
+ archivePath: archivePathArg || archivePathCfg || "agent-transcripts",
25
+ minIntervalMinutes: Math.max(1, Math.floor(minIntervalCfg)),
26
+ skipIfNoNewTranscripts: skipIfNoNewCfg
27
+ };
28
+ }
29
+ async function listJsonlRelativePaths(root) {
30
+ const out = [];
31
+ async function walk(cur) {
32
+ const ents = await fs.readdir(cur, { withFileTypes: true });
33
+ for (const ent of ents) {
34
+ const abs = path.join(cur, ent.name);
35
+ if (ent.isDirectory()) {
36
+ await walk(abs);
37
+ }
38
+ else if (ent.isFile() && ent.name.endsWith(".jsonl")) {
39
+ out.push(path.relative(root, abs));
40
+ }
41
+ }
42
+ }
43
+ await walk(root);
44
+ return out.sort((a, b) => a.localeCompare(b));
45
+ }
46
+ async function fileSha256(filePath) {
47
+ const buf = await fs.readFile(filePath);
48
+ return createHash("sha256").update(buf).digest("hex");
49
+ }
50
+ export async function runSyncTranscripts(ctx, args) {
51
+ const cfg = resolveImprovementTranscriptConfig(ctx, args);
52
+ const sourceRoot = path.resolve(ctx.workspacePath, cfg.sourcePath);
53
+ const archiveRoot = path.resolve(ctx.workspacePath, cfg.archivePath);
54
+ const result = {
55
+ sourcePath: path.relative(ctx.workspacePath, sourceRoot) || ".",
56
+ archivePath: path.relative(ctx.workspacePath, archiveRoot) || ".",
57
+ scanned: 0,
58
+ copied: 0,
59
+ skippedExisting: 0,
60
+ skippedConflict: 0,
61
+ errors: [],
62
+ copiedFiles: []
63
+ };
64
+ let files = [];
65
+ try {
66
+ files = await listJsonlRelativePaths(sourceRoot);
67
+ }
68
+ catch (error) {
69
+ const msg = error instanceof Error ? error.message : String(error);
70
+ result.errors.push({
71
+ file: result.sourcePath,
72
+ code: "source-read-error",
73
+ message: msg
74
+ });
75
+ return result;
76
+ }
77
+ result.scanned = files.length;
78
+ await fs.mkdir(archiveRoot, { recursive: true });
79
+ for (const rel of files) {
80
+ const src = path.join(sourceRoot, rel);
81
+ const dst = path.join(archiveRoot, rel);
82
+ try {
83
+ await fs.mkdir(path.dirname(dst), { recursive: true });
84
+ const srcHash = await fileSha256(src);
85
+ let dstHash = null;
86
+ try {
87
+ dstHash = await fileSha256(dst);
88
+ }
89
+ catch (error) {
90
+ if (error.code !== "ENOENT") {
91
+ throw error;
92
+ }
93
+ }
94
+ if (dstHash === srcHash) {
95
+ result.skippedExisting += 1;
96
+ continue;
97
+ }
98
+ if (dstHash && dstHash !== srcHash) {
99
+ result.skippedConflict += 1;
100
+ continue;
101
+ }
102
+ await fs.copyFile(src, dst);
103
+ result.copied += 1;
104
+ result.copiedFiles.push(rel);
105
+ }
106
+ catch (error) {
107
+ const msg = error instanceof Error ? error.message : String(error);
108
+ result.errors.push({ file: rel, code: "copy-error", message: msg });
109
+ }
110
+ }
111
+ result.copiedFiles.sort((a, b) => a.localeCompare(b));
112
+ return result;
113
+ }
114
+ export function resolveCadenceDecision(now, previousRunAtIso, minIntervalMinutes, copiedCount, skipIfNoNewTranscripts) {
115
+ if (copiedCount === 0 && skipIfNoNewTranscripts) {
116
+ return { shouldRunGenerate: false, reason: "skipped-no-new-transcripts" };
117
+ }
118
+ if (!previousRunAtIso) {
119
+ return { shouldRunGenerate: true, reason: "run-first-ingest" };
120
+ }
121
+ const prev = new Date(previousRunAtIso);
122
+ if (!Number.isFinite(prev.getTime())) {
123
+ return { shouldRunGenerate: true, reason: "run-invalid-last-ingest-at" };
124
+ }
125
+ const elapsedMs = now.getTime() - prev.getTime();
126
+ const requiredMs = minIntervalMinutes * 60 * 1000;
127
+ if (elapsedMs < requiredMs) {
128
+ return { shouldRunGenerate: false, reason: "skipped-min-interval" };
129
+ }
130
+ return { shouldRunGenerate: true, reason: "run-min-interval-satisfied" };
131
+ }
@@ -5,5 +5,5 @@ export { improvementModule } from "./improvement/index.js";
5
5
  export { computeHeuristicConfidence, HEURISTIC_1_ADMISSION_THRESHOLD, shouldAdmitRecommendation, type ConfidenceResult, type ConfidenceSignals, type EvidenceKind } from "./improvement/confidence.js";
6
6
  export { workspaceConfigModule } from "./workspace-config/index.js";
7
7
  export { planningModule } from "./planning/index.js";
8
- export { taskEngineModule, TaskStore, TransitionService, TaskEngineError, TransitionValidator, isTransitionAllowed, getTransitionAction, resolveTargetState, getAllowedTransitionsFrom, stateValidityGuard, dependencyCheckGuard, generateTasksMd, importTasksFromMarkdown, getNextActions } from "./task-engine/index.js";
8
+ export { taskEngineModule, TaskStore, TransitionService, TaskEngineError, TransitionValidator, isTransitionAllowed, getTransitionAction, resolveTargetState, getAllowedTransitionsFrom, stateValidityGuard, dependencyCheckGuard, generateTasksMd, syncTaskHeadingsInMarkdown, importTasksFromMarkdown, getNextActions } from "./task-engine/index.js";
9
9
  export type { TaskEntity, TaskStatus, TaskPriority, TaskStoreDocument, TransitionEvidence, TransitionGuard, TransitionContext, GuardResult, TaskEngineErrorCode, TaskAdapter, TaskAdapterCapability, NextActionSuggestion, BlockingAnalysisEntry } from "./task-engine/index.js";
@@ -4,4 +4,4 @@ export { improvementModule } from "./improvement/index.js";
4
4
  export { computeHeuristicConfidence, HEURISTIC_1_ADMISSION_THRESHOLD, shouldAdmitRecommendation } from "./improvement/confidence.js";
5
5
  export { workspaceConfigModule } from "./workspace-config/index.js";
6
6
  export { planningModule } from "./planning/index.js";
7
- export { taskEngineModule, TaskStore, TransitionService, TaskEngineError, TransitionValidator, isTransitionAllowed, getTransitionAction, resolveTargetState, getAllowedTransitionsFrom, stateValidityGuard, dependencyCheckGuard, generateTasksMd, importTasksFromMarkdown, getNextActions } from "./task-engine/index.js";
7
+ export { taskEngineModule, TaskStore, TransitionService, TaskEngineError, TransitionValidator, isTransitionAllowed, getTransitionAction, resolveTargetState, getAllowedTransitionsFrom, stateValidityGuard, dependencyCheckGuard, generateTasksMd, syncTaskHeadingsInMarkdown, importTasksFromMarkdown, getNextActions } from "./task-engine/index.js";
@@ -1,2 +1,3 @@
1
1
  import type { TaskEntity } from "./types.js";
2
2
  export declare function generateTasksMd(tasks: TaskEntity[]): string;
3
+ export declare function syncTaskHeadingsInMarkdown(markdown: string, tasks: TaskEntity[]): string;
@@ -99,3 +99,20 @@ export function generateTasksMd(tasks) {
99
99
  }
100
100
  return lines.join("\n");
101
101
  }
102
+ export function syncTaskHeadingsInMarkdown(markdown, tasks) {
103
+ const byId = new Map(tasks.map((task) => [task.id, task]));
104
+ const lines = markdown.split("\n");
105
+ for (let i = 0; i < lines.length; i++) {
106
+ const line = lines[i];
107
+ const match = line.match(/^###\s+\[[^\]]*\]\s+(T\d+)\s+(.+)$/);
108
+ if (!match)
109
+ continue;
110
+ const id = match[1];
111
+ const task = byId.get(id);
112
+ if (!task)
113
+ continue;
114
+ const marker = STATUS_MARKERS[task.status] ?? "[ ]";
115
+ lines[i] = `### ${marker} ${task.id} ${task.title}`;
116
+ }
117
+ return lines.join("\n");
118
+ }
@@ -139,8 +139,14 @@ export async function importTasksFromMarkdown(sourcePath) {
139
139
  }
140
140
  if (line.startsWith("### ")) {
141
141
  flushTask();
142
- taskStartIdx = i;
143
- taskLines = [line];
142
+ if (parseTaskId(line)) {
143
+ taskStartIdx = i;
144
+ taskLines = [line];
145
+ }
146
+ else {
147
+ taskStartIdx = -1;
148
+ taskLines = [];
149
+ }
144
150
  continue;
145
151
  }
146
152
  if (taskStartIdx !== -1) {
@@ -3,7 +3,7 @@ export type { TaskEntity, TaskStatus, TaskPriority, TaskStoreDocument, Transitio
3
3
  export { TaskStore } from "./store.js";
4
4
  export { TransitionService } from "./service.js";
5
5
  export { TaskEngineError, TransitionValidator, isTransitionAllowed, getTransitionAction, resolveTargetState, getAllowedTransitionsFrom, stateValidityGuard, dependencyCheckGuard } from "./transitions.js";
6
- export { generateTasksMd } from "./generator.js";
6
+ export { generateTasksMd, syncTaskHeadingsInMarkdown } from "./generator.js";
7
7
  export { importTasksFromMarkdown } from "./importer.js";
8
8
  export { getNextActions } from "./suggestions.js";
9
9
  export declare const taskEngineModule: WorkflowModule;
@@ -3,13 +3,13 @@ import fs from "node:fs/promises";
3
3
  import { TaskStore } from "./store.js";
4
4
  import { TransitionService } from "./service.js";
5
5
  import { TaskEngineError } from "./transitions.js";
6
- import { generateTasksMd } from "./generator.js";
6
+ import { generateTasksMd, syncTaskHeadingsInMarkdown } from "./generator.js";
7
7
  import { importTasksFromMarkdown } from "./importer.js";
8
8
  import { getNextActions } from "./suggestions.js";
9
9
  export { TaskStore } from "./store.js";
10
10
  export { TransitionService } from "./service.js";
11
11
  export { TaskEngineError, TransitionValidator, isTransitionAllowed, getTransitionAction, resolveTargetState, getAllowedTransitionsFrom, stateValidityGuard, dependencyCheckGuard } from "./transitions.js";
12
- export { generateTasksMd } from "./generator.js";
12
+ export { generateTasksMd, syncTaskHeadingsInMarkdown } from "./generator.js";
13
13
  export { importTasksFromMarkdown } from "./importer.js";
14
14
  export { getNextActions } from "./suggestions.js";
15
15
  function taskStorePath(ctx) {
@@ -225,8 +225,16 @@ export const taskEngineModule = {
225
225
  ? path.resolve(ctx.workspacePath, args.outputPath)
226
226
  : path.resolve(ctx.workspacePath, "docs/maintainers/TASKS.md");
227
227
  const tasks = store.getAllTasks();
228
- const markdown = generateTasksMd(tasks);
228
+ const fallbackMarkdown = generateTasksMd(tasks);
229
+ let markdown = fallbackMarkdown;
230
+ const preserveStructure = args.preserveStructure !== false;
229
231
  try {
232
+ if (preserveStructure) {
233
+ const existing = await fs.readFile(outputPath, "utf8").catch(() => undefined);
234
+ if (typeof existing === "string" && existing.length > 0) {
235
+ markdown = syncTaskHeadingsInMarkdown(existing, tasks);
236
+ }
237
+ }
230
238
  await fs.mkdir(path.dirname(outputPath), { recursive: true });
231
239
  await fs.writeFile(outputPath, markdown, "utf8");
232
240
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@workflow-cannon/workspace-kit",
3
- "version": "0.5.0",
3
+ "version": "0.7.0",
4
4
  "private": false,
5
5
  "packageManager": "pnpm@10.0.0",
6
6
  "license": "MIT",
@@ -24,7 +24,14 @@
24
24
  "test": "pnpm run build && node --test test/**/*.test.mjs",
25
25
  "pack:dry-run": "pnpm run build && pnpm pack --pack-destination ./artifacts/workspace-kit-pack",
26
26
  "check-release-metadata": "node scripts/check-release-metadata.mjs",
27
- "parity": "node scripts/run-parity.mjs"
27
+ "parity": "node scripts/run-parity.mjs",
28
+ "check-compatibility": "pnpm run build && node scripts/check-compatibility.mjs",
29
+ "check-planning-consistency": "node scripts/check-planning-doc-consistency.mjs",
30
+ "check-release-channel": "node scripts/check-release-channel.mjs",
31
+ "generate-runtime-diagnostics": "node scripts/generate-runtime-diagnostics.mjs",
32
+ "prune-evidence": "node scripts/prune-evidence.mjs",
33
+ "phase4-gates": "pnpm run check-compatibility && pnpm run check-planning-consistency && pnpm run check-release-channel",
34
+ "phase5-gates": "pnpm run phase4-gates && pnpm run test"
28
35
  },
29
36
  "devDependencies": {
30
37
  "@types/node": "^25.5.0",