@workflow-cannon/workspace-kit 0.6.0 → 0.8.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 +3 -3
  2. package/dist/cli.js +31 -21
  3. package/dist/contracts/index.d.ts +1 -1
  4. package/dist/contracts/module-contract.d.ts +13 -0
  5. package/dist/core/config-metadata.js +303 -1
  6. package/dist/core/index.d.ts +6 -0
  7. package/dist/core/index.js +6 -0
  8. package/dist/core/instruction-template-mapper.d.ts +9 -0
  9. package/dist/core/instruction-template-mapper.js +35 -0
  10. package/dist/core/lineage-contract.d.ts +1 -1
  11. package/dist/core/lineage-contract.js +1 -1
  12. package/dist/core/policy.d.ts +4 -1
  13. package/dist/core/policy.js +5 -4
  14. package/dist/core/response-template-contract.d.ts +15 -0
  15. package/dist/core/response-template-contract.js +10 -0
  16. package/dist/core/response-template-registry.d.ts +4 -0
  17. package/dist/core/response-template-registry.js +44 -0
  18. package/dist/core/response-template-shaping.d.ts +6 -0
  19. package/dist/core/response-template-shaping.js +128 -0
  20. package/dist/core/session-policy.d.ts +18 -0
  21. package/dist/core/session-policy.js +57 -0
  22. package/dist/core/transcript-completion-hook.d.ts +7 -0
  23. package/dist/core/transcript-completion-hook.js +90 -0
  24. package/dist/core/workspace-kit-config.js +42 -2
  25. package/dist/modules/documentation/runtime.js +383 -14
  26. package/dist/modules/improvement/generate-recommendations-runtime.d.ts +7 -0
  27. package/dist/modules/improvement/generate-recommendations-runtime.js +51 -7
  28. package/dist/modules/improvement/improvement-state.d.ts +12 -1
  29. package/dist/modules/improvement/improvement-state.js +38 -7
  30. package/dist/modules/improvement/index.js +124 -2
  31. package/dist/modules/improvement/ingest.js +2 -1
  32. package/dist/modules/improvement/transcript-redaction.d.ts +4 -0
  33. package/dist/modules/improvement/transcript-redaction.js +10 -0
  34. package/dist/modules/improvement/transcript-sync-runtime.d.ts +60 -0
  35. package/dist/modules/improvement/transcript-sync-runtime.js +320 -0
  36. package/dist/modules/index.d.ts +1 -1
  37. package/dist/modules/index.js +1 -1
  38. package/dist/modules/task-engine/index.d.ts +0 -2
  39. package/dist/modules/task-engine/index.js +4 -70
  40. package/package.json +6 -2
  41. package/dist/modules/task-engine/generator.d.ts +0 -2
  42. package/dist/modules/task-engine/generator.js +0 -101
  43. package/dist/modules/task-engine/importer.d.ts +0 -8
  44. package/dist/modules/task-engine/importer.js +0 -157
@@ -1,9 +1,12 @@
1
1
  import { queryLineageChain } from "../../core/lineage-store.js";
2
- import { runGenerateRecommendations } from "./generate-recommendations-runtime.js";
2
+ import { resolveSessionId } from "../../core/session-policy.js";
3
+ import { getMaxRecommendationCandidatesPerRun, runGenerateRecommendations } from "./generate-recommendations-runtime.js";
4
+ import { resolveCadenceDecision, resolveImprovementTranscriptConfig, runSyncTranscripts } from "./transcript-sync-runtime.js";
5
+ import { loadImprovementState, saveImprovementState } from "./improvement-state.js";
3
6
  export const improvementModule = {
4
7
  registration: {
5
8
  id: "improvement",
6
- version: "0.5.0",
9
+ version: "0.8.0",
7
10
  contractVersion: "1",
8
11
  capabilities: ["improvement"],
9
12
  dependsOn: ["task-engine", "planning"],
@@ -30,6 +33,21 @@ export const improvementModule = {
30
33
  name: "query-lineage",
31
34
  file: "query-lineage.md",
32
35
  description: "Reconstruct lineage chain for a recommendation task id."
36
+ },
37
+ {
38
+ name: "sync-transcripts",
39
+ file: "sync-transcripts.md",
40
+ description: "Sync local transcript JSONL files into the archive."
41
+ },
42
+ {
43
+ name: "ingest-transcripts",
44
+ file: "ingest-transcripts.md",
45
+ description: "Run transcript sync and recommendation generation in one flow."
46
+ },
47
+ {
48
+ name: "transcript-automation-status",
49
+ file: "transcript-automation-status.md",
50
+ description: "Emit stable JSON status for transcript sync, ingest, and retry queue."
33
51
  }
34
52
  ]
35
53
  }
@@ -54,6 +72,110 @@ export const improvementModule = {
54
72
  return { ok: false, code: "generate-failed", message: msg };
55
73
  }
56
74
  }
75
+ if (command.name === "sync-transcripts") {
76
+ const syncArgs = {
77
+ sourcePath: typeof args.sourcePath === "string" ? args.sourcePath : undefined,
78
+ archivePath: typeof args.archivePath === "string" ? args.archivePath : undefined
79
+ };
80
+ try {
81
+ const state = await loadImprovementState(ctx.workspacePath);
82
+ const sync = await runSyncTranscripts(ctx, syncArgs, state);
83
+ state.lastSyncRunAt = new Date().toISOString();
84
+ await saveImprovementState(ctx.workspacePath, state);
85
+ return {
86
+ ok: true,
87
+ code: "transcripts-synced",
88
+ message: `Copied ${sync.copied} transcript file(s); skipped ${sync.skippedExisting} existing`,
89
+ data: sync
90
+ };
91
+ }
92
+ catch (e) {
93
+ const msg = e instanceof Error ? e.message : String(e);
94
+ return { ok: false, code: "sync-failed", message: msg };
95
+ }
96
+ }
97
+ if (command.name === "ingest-transcripts") {
98
+ const syncArgs = {
99
+ sourcePath: typeof args.sourcePath === "string" ? args.sourcePath : undefined,
100
+ archivePath: typeof args.archivePath === "string" ? args.archivePath : undefined
101
+ };
102
+ const now = new Date();
103
+ try {
104
+ const state = await loadImprovementState(ctx.workspacePath);
105
+ const sync = await runSyncTranscripts(ctx, syncArgs, state);
106
+ const cfg = resolveImprovementTranscriptConfig(ctx, syncArgs);
107
+ const cadenceDecision = resolveCadenceDecision(now, state.lastIngestRunAt, cfg.minIntervalMinutes, sync.copied, cfg.skipIfNoNewTranscripts);
108
+ state.lastSyncRunAt = now.toISOString();
109
+ const generate = cadenceDecision.shouldRunGenerate || args.forceGenerate === true || args.runGenerate === true;
110
+ let recommendations = null;
111
+ if (generate) {
112
+ recommendations = await runGenerateRecommendations(ctx, {
113
+ transcriptsRoot: sync.archivePath
114
+ });
115
+ state.lastIngestRunAt = now.toISOString();
116
+ }
117
+ await saveImprovementState(ctx.workspacePath, state);
118
+ const status = generate ? "generated" : "skipped";
119
+ return {
120
+ ok: true,
121
+ code: "transcripts-ingested",
122
+ message: `Ingest ${status}; sync copied ${sync.copied} file(s)`,
123
+ data: {
124
+ sync,
125
+ cadence: {
126
+ minIntervalMinutes: cfg.minIntervalMinutes,
127
+ skipIfNoNewTranscripts: cfg.skipIfNoNewTranscripts,
128
+ decision: cadenceDecision.reason
129
+ },
130
+ generatedRecommendations: recommendations
131
+ }
132
+ };
133
+ }
134
+ catch (e) {
135
+ const msg = e instanceof Error ? e.message : String(e);
136
+ return { ok: false, code: "ingest-failed", message: msg };
137
+ }
138
+ }
139
+ if (command.name === "transcript-automation-status") {
140
+ const syncArgs = {
141
+ sourcePath: typeof args.sourcePath === "string" ? args.sourcePath : undefined,
142
+ archivePath: typeof args.archivePath === "string" ? args.archivePath : undefined
143
+ };
144
+ const state = await loadImprovementState(ctx.workspacePath);
145
+ const cfg = resolveImprovementTranscriptConfig(ctx, syncArgs);
146
+ return {
147
+ ok: true,
148
+ code: "transcript-automation-status",
149
+ message: "Transcript automation status",
150
+ data: {
151
+ schemaVersion: 1,
152
+ lastSyncRunAt: state.lastSyncRunAt,
153
+ lastIngestRunAt: state.lastIngestRunAt,
154
+ cadence: {
155
+ minIntervalMinutes: cfg.minIntervalMinutes,
156
+ skipIfNoNewTranscripts: cfg.skipIfNoNewTranscripts,
157
+ maxRecommendationCandidatesPerRun: getMaxRecommendationCandidatesPerRun(ctx)
158
+ },
159
+ transcripts: {
160
+ sourcePath: cfg.sourcePath || null,
161
+ archivePath: cfg.archivePath,
162
+ discoveryPaths: cfg.discoveryPaths,
163
+ budgets: {
164
+ maxFilesPerSync: cfg.maxFilesPerSync,
165
+ maxBytesPerFile: cfg.maxBytesPerFile,
166
+ maxTotalScanBytes: cfg.maxTotalScanBytes
167
+ }
168
+ },
169
+ retryQueue: {
170
+ pending: state.transcriptRetryQueue?.length ?? 0,
171
+ entries: state.transcriptRetryQueue ?? []
172
+ },
173
+ policySession: {
174
+ sessionId: resolveSessionId(process.env)
175
+ }
176
+ }
177
+ };
178
+ }
57
179
  if (command.name === "query-lineage") {
58
180
  const taskId = typeof args.taskId === "string" ? args.taskId.trim() : "";
59
181
  if (!taskId) {
@@ -3,6 +3,7 @@ import path from "node:path";
3
3
  import { execFileSync } from "node:child_process";
4
4
  import crypto from "node:crypto";
5
5
  import { computeHeuristicConfidence, shouldAdmitRecommendation } from "./confidence.js";
6
+ import { redactTranscriptSnippet } from "./transcript-redaction.js";
6
7
  function sha256Hex(s) {
7
8
  return crypto.createHash("sha256").update(s, "utf8").digest("hex");
8
9
  }
@@ -80,7 +81,7 @@ export async function ingestAgentTranscripts(workspacePath, transcriptsRootRel,
80
81
  evidenceKind: "transcript",
81
82
  evidenceKey,
82
83
  title: `Reduce friction hinted in transcript (${path.basename(rel)})`,
83
- provenanceRefs: { transcriptPath: rel, sampleLine },
84
+ provenanceRefs: { transcriptPath: rel, sampleLine: redactTranscriptSnippet(sampleLine) },
84
85
  signals,
85
86
  confidence
86
87
  });
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Redact transcript-derived strings before they are persisted in task metadata or lineage.
3
+ */
4
+ export declare function redactTranscriptSnippet(text: string, maxLen?: number): string;
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Redact transcript-derived strings before they are persisted in task metadata or lineage.
3
+ */
4
+ export function redactTranscriptSnippet(text, maxLen = 160) {
5
+ let s = text.length > maxLen ? `${text.slice(0, maxLen)}…` : text;
6
+ s = s.replace(/\b(sk|pk|api|Bearer)[-_]?[a-zA-Z0-9]{12,}\b/gi, "[redacted-token]");
7
+ s = s.replace(/\b[A-Fa-f0-9]{32,}\b/g, "[redacted-hex]");
8
+ s = s.replace(/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/g, "[redacted-email]");
9
+ return s;
10
+ }
@@ -0,0 +1,60 @@
1
+ import type { ModuleLifecycleContext } from "../../contracts/module-contract.js";
2
+ import type { ImprovementStateDocument } from "./improvement-state.js";
3
+ export type TranscriptSyncArgs = {
4
+ sourcePath?: string;
5
+ archivePath?: string;
6
+ };
7
+ export type TranscriptSkipReason = "skipped-unchanged-hash" | "skipped-archive-conflict" | "skipped-file-too-large" | "skipped-budget-max-files" | "skipped-budget-total-bytes" | "skipped-read-error";
8
+ export type TranscriptSyncResult = {
9
+ runId: string;
10
+ sourcePath: string;
11
+ archivePath: string;
12
+ discoveredFrom: string;
13
+ discoveryCandidatesTried: string[];
14
+ scanned: number;
15
+ copied: number;
16
+ skippedExisting: number;
17
+ skippedConflict: number;
18
+ skippedBudget: number;
19
+ skippedLargeFile: number;
20
+ errors: Array<{
21
+ file: string;
22
+ code: string;
23
+ message: string;
24
+ }>;
25
+ copiedFiles: string[];
26
+ skipReasons: Record<string, TranscriptSkipReason>;
27
+ budget: {
28
+ maxFilesPerSync: number;
29
+ maxBytesPerFile: number;
30
+ maxTotalScanBytes: number;
31
+ scanBytesUsed: number;
32
+ };
33
+ retryQueue: {
34
+ pending: number;
35
+ processedRetries: number;
36
+ droppedPermanentFailures: number;
37
+ };
38
+ };
39
+ type ImprovementTranscriptConfig = {
40
+ sourcePath: string;
41
+ archivePath: string;
42
+ minIntervalMinutes: number;
43
+ skipIfNoNewTranscripts: boolean;
44
+ maxFilesPerSync: number;
45
+ maxBytesPerFile: number;
46
+ maxTotalScanBytes: number;
47
+ discoveryPaths: string[];
48
+ };
49
+ export declare function resolveImprovementTranscriptConfig(ctx: ModuleLifecycleContext, args: TranscriptSyncArgs): ImprovementTranscriptConfig;
50
+ export declare function resolveTranscriptSourceRoot(workspacePath: string, cfg: ImprovementTranscriptConfig, args: TranscriptSyncArgs): Promise<{
51
+ root: string;
52
+ discoveredFrom: string;
53
+ tried: string[];
54
+ }>;
55
+ export declare function runSyncTranscripts(ctx: ModuleLifecycleContext, args: TranscriptSyncArgs, state: ImprovementStateDocument, now?: Date): Promise<TranscriptSyncResult>;
56
+ export declare function resolveCadenceDecision(now: Date, previousRunAtIso: string | null, minIntervalMinutes: number, copiedCount: number, skipIfNoNewTranscripts: boolean): {
57
+ shouldRunGenerate: boolean;
58
+ reason: string;
59
+ };
60
+ export {};
@@ -0,0 +1,320 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { createHash, randomUUID } from "node:crypto";
4
+ const DEFAULT_DISCOVERY_PATHS = [".cursor/agent-transcripts", ".vscode/agent-transcripts"];
5
+ const MAX_RETRY_ATTEMPTS = 5;
6
+ const INITIAL_BACKOFF_MS = 60_000;
7
+ export function resolveImprovementTranscriptConfig(ctx, args) {
8
+ const improvement = ctx.effectiveConfig?.improvement && typeof ctx.effectiveConfig.improvement === "object"
9
+ ? ctx.effectiveConfig.improvement
10
+ : {};
11
+ const transcripts = improvement.transcripts && typeof improvement.transcripts === "object"
12
+ ? improvement.transcripts
13
+ : {};
14
+ const cadence = improvement.cadence && typeof improvement.cadence === "object"
15
+ ? improvement.cadence
16
+ : {};
17
+ const sourcePathArg = typeof args.sourcePath === "string" ? args.sourcePath.trim() : "";
18
+ const archivePathArg = typeof args.archivePath === "string" ? args.archivePath.trim() : "";
19
+ const sourcePathCfg = typeof transcripts.sourcePath === "string" ? transcripts.sourcePath.trim() : "";
20
+ const archivePathCfg = typeof transcripts.archivePath === "string" ? transcripts.archivePath.trim() : "";
21
+ const minIntervalCfg = typeof cadence.minIntervalMinutes === "number" && Number.isFinite(cadence.minIntervalMinutes)
22
+ ? cadence.minIntervalMinutes
23
+ : 15;
24
+ const skipIfNoNewCfg = typeof cadence.skipIfNoNewTranscripts === "boolean" ? cadence.skipIfNoNewTranscripts : true;
25
+ const maxFilesCfg = typeof transcripts.maxFilesPerSync === "number" && Number.isFinite(transcripts.maxFilesPerSync)
26
+ ? Math.max(1, Math.floor(transcripts.maxFilesPerSync))
27
+ : 5000;
28
+ const maxBytesFileCfg = typeof transcripts.maxBytesPerFile === "number" && Number.isFinite(transcripts.maxBytesPerFile)
29
+ ? Math.max(1024, Math.floor(transcripts.maxBytesPerFile))
30
+ : 50_000_000;
31
+ const maxTotalScanCfg = typeof transcripts.maxTotalScanBytes === "number" && Number.isFinite(transcripts.maxTotalScanBytes)
32
+ ? Math.max(1024, Math.floor(transcripts.maxTotalScanBytes))
33
+ : 500_000_000;
34
+ let discoveryPaths = [];
35
+ if (Array.isArray(transcripts.discoveryPaths)) {
36
+ discoveryPaths = transcripts.discoveryPaths.filter((x) => typeof x === "string" && x.trim().length > 0);
37
+ }
38
+ if (discoveryPaths.length === 0) {
39
+ discoveryPaths = [...DEFAULT_DISCOVERY_PATHS];
40
+ }
41
+ return {
42
+ sourcePath: sourcePathArg || sourcePathCfg || "",
43
+ archivePath: archivePathArg || archivePathCfg || "agent-transcripts",
44
+ minIntervalMinutes: Math.max(1, Math.floor(minIntervalCfg)),
45
+ skipIfNoNewTranscripts: skipIfNoNewCfg,
46
+ maxFilesPerSync: maxFilesCfg,
47
+ maxBytesPerFile: maxBytesFileCfg,
48
+ maxTotalScanBytes: maxTotalScanCfg,
49
+ discoveryPaths
50
+ };
51
+ }
52
+ async function pathExists(abs) {
53
+ try {
54
+ await fs.access(abs);
55
+ return true;
56
+ }
57
+ catch {
58
+ return false;
59
+ }
60
+ }
61
+ export async function resolveTranscriptSourceRoot(workspacePath, cfg, args) {
62
+ const sourcePathArg = typeof args.sourcePath === "string" ? args.sourcePath.trim() : "";
63
+ if (sourcePathArg) {
64
+ const root = path.resolve(workspacePath, sourcePathArg);
65
+ return { root, discoveredFrom: sourcePathArg, tried: [sourcePathArg] };
66
+ }
67
+ if (cfg.sourcePath) {
68
+ const root = path.resolve(workspacePath, cfg.sourcePath);
69
+ return { root, discoveredFrom: cfg.sourcePath, tried: [cfg.sourcePath] };
70
+ }
71
+ const tried = [];
72
+ for (const rel of cfg.discoveryPaths) {
73
+ tried.push(rel);
74
+ const abs = path.resolve(workspacePath, rel);
75
+ if (await pathExists(abs)) {
76
+ return { root: abs, discoveredFrom: rel, tried };
77
+ }
78
+ }
79
+ const fallback = path.resolve(workspacePath, ".cursor/agent-transcripts");
80
+ return { root: fallback, discoveredFrom: ".cursor/agent-transcripts (fallback)", tried };
81
+ }
82
+ async function listJsonlRelativePaths(root) {
83
+ const out = [];
84
+ async function walk(cur) {
85
+ const ents = await fs.readdir(cur, { withFileTypes: true });
86
+ for (const ent of ents) {
87
+ const abs = path.join(cur, ent.name);
88
+ if (ent.isDirectory()) {
89
+ await walk(abs);
90
+ }
91
+ else if (ent.isFile() && ent.name.endsWith(".jsonl")) {
92
+ out.push(path.relative(root, abs));
93
+ }
94
+ }
95
+ }
96
+ await walk(root);
97
+ return out.sort((a, b) => a.localeCompare(b));
98
+ }
99
+ async function statSize(filePath) {
100
+ try {
101
+ const st = await fs.stat(filePath);
102
+ return st.size;
103
+ }
104
+ catch {
105
+ return null;
106
+ }
107
+ }
108
+ async function fileSha256(filePath) {
109
+ const buf = await fs.readFile(filePath);
110
+ return createHash("sha256").update(buf).digest("hex");
111
+ }
112
+ function nextBackoffMs(attempts) {
113
+ return INITIAL_BACKOFF_MS * 2 ** Math.max(0, attempts - 1);
114
+ }
115
+ export async function runSyncTranscripts(ctx, args, state, now = new Date()) {
116
+ const cfg = resolveImprovementTranscriptConfig(ctx, args);
117
+ const runId = randomUUID();
118
+ const { root: sourceRoot, discoveredFrom, tried: discoveryCandidatesTried } = await resolveTranscriptSourceRoot(ctx.workspacePath, cfg, args);
119
+ const archiveRoot = path.resolve(ctx.workspacePath, cfg.archivePath);
120
+ const result = {
121
+ runId,
122
+ sourcePath: path.relative(ctx.workspacePath, sourceRoot) || ".",
123
+ archivePath: path.relative(ctx.workspacePath, archiveRoot) || ".",
124
+ discoveredFrom,
125
+ discoveryCandidatesTried,
126
+ scanned: 0,
127
+ copied: 0,
128
+ skippedExisting: 0,
129
+ skippedConflict: 0,
130
+ skippedBudget: 0,
131
+ skippedLargeFile: 0,
132
+ errors: [],
133
+ copiedFiles: [],
134
+ skipReasons: {},
135
+ budget: {
136
+ maxFilesPerSync: cfg.maxFilesPerSync,
137
+ maxBytesPerFile: cfg.maxBytesPerFile,
138
+ maxTotalScanBytes: cfg.maxTotalScanBytes,
139
+ scanBytesUsed: 0
140
+ },
141
+ retryQueue: { pending: 0, processedRetries: 0, droppedPermanentFailures: 0 }
142
+ };
143
+ let files = [];
144
+ try {
145
+ files = await listJsonlRelativePaths(sourceRoot);
146
+ }
147
+ catch (error) {
148
+ const msg = error instanceof Error ? error.message : String(error);
149
+ result.errors.push({
150
+ file: result.sourcePath,
151
+ code: "source-read-error",
152
+ message: `${msg} (discovery tried: ${discoveryCandidatesTried.join(", ")})`
153
+ });
154
+ return result;
155
+ }
156
+ await fs.mkdir(archiveRoot, { recursive: true });
157
+ const queue = state.transcriptRetryQueue ?? [];
158
+ const remainingRetries = [];
159
+ let scanBytes = 0;
160
+ for (const entry of queue) {
161
+ const due = new Date(entry.nextRetryAt).getTime() <= now.getTime();
162
+ if (!due) {
163
+ remainingRetries.push(entry);
164
+ continue;
165
+ }
166
+ if (entry.attempts >= MAX_RETRY_ATTEMPTS) {
167
+ result.retryQueue.droppedPermanentFailures += 1;
168
+ result.errors.push({
169
+ file: entry.relativePath,
170
+ code: "retry-exhausted",
171
+ message: entry.lastErrorMessage
172
+ });
173
+ continue;
174
+ }
175
+ const src = path.join(sourceRoot, entry.relativePath);
176
+ const dst = path.join(archiveRoot, entry.relativePath);
177
+ try {
178
+ await fs.mkdir(path.dirname(dst), { recursive: true });
179
+ const sz = await statSize(src);
180
+ if (sz !== null && sz > cfg.maxBytesPerFile) {
181
+ remainingRetries.push({
182
+ ...entry,
183
+ attempts: entry.attempts + 1,
184
+ lastErrorCode: "file-too-large",
185
+ lastErrorMessage: `size ${sz} exceeds maxBytesPerFile`,
186
+ nextRetryAt: new Date(now.getTime() + nextBackoffMs(entry.attempts + 1)).toISOString()
187
+ });
188
+ continue;
189
+ }
190
+ if (sz !== null) {
191
+ scanBytes += sz;
192
+ }
193
+ const srcHash = await fileSha256(src);
194
+ let dstHash = null;
195
+ try {
196
+ dstHash = await fileSha256(dst);
197
+ }
198
+ catch (error) {
199
+ if (error.code !== "ENOENT") {
200
+ throw error;
201
+ }
202
+ }
203
+ if (dstHash === srcHash) {
204
+ result.retryQueue.processedRetries += 1;
205
+ continue;
206
+ }
207
+ if (dstHash && dstHash !== srcHash) {
208
+ remainingRetries.push({
209
+ ...entry,
210
+ attempts: entry.attempts + 1,
211
+ lastErrorCode: "archive-conflict",
212
+ lastErrorMessage: "destination exists with different content",
213
+ nextRetryAt: new Date(now.getTime() + nextBackoffMs(entry.attempts + 1)).toISOString()
214
+ });
215
+ continue;
216
+ }
217
+ await fs.copyFile(src, dst);
218
+ result.copied += 1;
219
+ result.copiedFiles.push(entry.relativePath);
220
+ result.retryQueue.processedRetries += 1;
221
+ }
222
+ catch (error) {
223
+ const msg = error instanceof Error ? error.message : String(error);
224
+ remainingRetries.push({
225
+ ...entry,
226
+ attempts: entry.attempts + 1,
227
+ lastErrorCode: "copy-error",
228
+ lastErrorMessage: msg,
229
+ nextRetryAt: new Date(now.getTime() + nextBackoffMs(entry.attempts + 1)).toISOString()
230
+ });
231
+ }
232
+ }
233
+ let budgetFiles = 0;
234
+ for (const rel of files) {
235
+ if (budgetFiles >= cfg.maxFilesPerSync) {
236
+ result.skippedBudget += 1;
237
+ result.skipReasons[rel] = "skipped-budget-max-files";
238
+ continue;
239
+ }
240
+ const src = path.join(sourceRoot, rel);
241
+ const dst = path.join(archiveRoot, rel);
242
+ budgetFiles += 1;
243
+ result.scanned += 1;
244
+ try {
245
+ const sz = await statSize(src);
246
+ if (sz !== null && sz > cfg.maxBytesPerFile) {
247
+ result.skippedLargeFile += 1;
248
+ result.skipReasons[rel] = "skipped-file-too-large";
249
+ continue;
250
+ }
251
+ if (sz !== null && scanBytes + sz > cfg.maxTotalScanBytes) {
252
+ result.skippedBudget += 1;
253
+ result.skipReasons[rel] = "skipped-budget-total-bytes";
254
+ continue;
255
+ }
256
+ if (sz !== null) {
257
+ scanBytes += sz;
258
+ }
259
+ await fs.mkdir(path.dirname(dst), { recursive: true });
260
+ const srcHash = await fileSha256(src);
261
+ let dstHash = null;
262
+ try {
263
+ dstHash = await fileSha256(dst);
264
+ }
265
+ catch (error) {
266
+ if (error.code !== "ENOENT") {
267
+ throw error;
268
+ }
269
+ }
270
+ if (dstHash === srcHash) {
271
+ result.skippedExisting += 1;
272
+ result.skipReasons[rel] = "skipped-unchanged-hash";
273
+ continue;
274
+ }
275
+ if (dstHash && dstHash !== srcHash) {
276
+ result.skippedConflict += 1;
277
+ result.skipReasons[rel] = "skipped-archive-conflict";
278
+ continue;
279
+ }
280
+ await fs.copyFile(src, dst);
281
+ result.copied += 1;
282
+ result.copiedFiles.push(rel);
283
+ }
284
+ catch (error) {
285
+ const msg = error instanceof Error ? error.message : String(error);
286
+ result.errors.push({ file: rel, code: "copy-error", message: msg });
287
+ remainingRetries.push({
288
+ relativePath: rel,
289
+ attempts: 1,
290
+ lastErrorCode: "copy-error",
291
+ lastErrorMessage: msg,
292
+ nextRetryAt: new Date(now.getTime() + INITIAL_BACKOFF_MS).toISOString()
293
+ });
294
+ result.skipReasons[rel] = "skipped-read-error";
295
+ }
296
+ }
297
+ result.budget.scanBytesUsed = scanBytes;
298
+ state.transcriptRetryQueue = remainingRetries;
299
+ result.retryQueue.pending = remainingRetries.length;
300
+ result.copiedFiles.sort((a, b) => a.localeCompare(b));
301
+ return result;
302
+ }
303
+ export function resolveCadenceDecision(now, previousRunAtIso, minIntervalMinutes, copiedCount, skipIfNoNewTranscripts) {
304
+ if (copiedCount === 0 && skipIfNoNewTranscripts) {
305
+ return { shouldRunGenerate: false, reason: "skipped-no-new-transcripts" };
306
+ }
307
+ if (!previousRunAtIso) {
308
+ return { shouldRunGenerate: true, reason: "run-first-ingest" };
309
+ }
310
+ const prev = new Date(previousRunAtIso);
311
+ if (!Number.isFinite(prev.getTime())) {
312
+ return { shouldRunGenerate: true, reason: "run-invalid-last-ingest-at" };
313
+ }
314
+ const elapsedMs = now.getTime() - prev.getTime();
315
+ const requiredMs = minIntervalMinutes * 60 * 1000;
316
+ if (elapsedMs < requiredMs) {
317
+ return { shouldRunGenerate: false, reason: "skipped-min-interval" };
318
+ }
319
+ return { shouldRunGenerate: true, reason: "run-min-interval-satisfied" };
320
+ }
@@ -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, 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, getNextActions } from "./task-engine/index.js";
@@ -3,7 +3,5 @@ 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";
7
- export { importTasksFromMarkdown } from "./importer.js";
8
6
  export { getNextActions } from "./suggestions.js";
9
7
  export declare const taskEngineModule: WorkflowModule;