@workflow-cannon/workspace-kit 0.7.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 +197 -3
  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 +3 -3
  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 +25 -4
  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 +37 -4
  28. package/dist/modules/improvement/improvement-state.d.ts +10 -1
  29. package/dist/modules/improvement/improvement-state.js +36 -7
  30. package/dist/modules/improvement/index.js +55 -20
  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 +37 -1
  35. package/dist/modules/improvement/transcript-sync-runtime.js +198 -9
  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 -78
  40. package/package.json +5 -2
  41. package/dist/modules/task-engine/generator.d.ts +0 -3
  42. package/dist/modules/task-engine/generator.js +0 -118
  43. package/dist/modules/task-engine/importer.d.ts +0 -8
  44. package/dist/modules/task-engine/importer.js +0 -163
@@ -1,11 +1,12 @@
1
1
  import { queryLineageChain } from "../../core/lineage-store.js";
2
- import { runGenerateRecommendations } from "./generate-recommendations-runtime.js";
3
- import { resolveCadenceDecision, runSyncTranscripts } from "./transcript-sync-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";
4
5
  import { loadImprovementState, saveImprovementState } from "./improvement-state.js";
5
6
  export const improvementModule = {
6
7
  registration: {
7
8
  id: "improvement",
8
- version: "0.7.0",
9
+ version: "0.8.0",
9
10
  contractVersion: "1",
10
11
  capabilities: ["improvement"],
11
12
  dependsOn: ["task-engine", "planning"],
@@ -42,6 +43,11 @@ export const improvementModule = {
42
43
  name: "ingest-transcripts",
43
44
  file: "ingest-transcripts.md",
44
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."
45
51
  }
46
52
  ]
47
53
  }
@@ -72,8 +78,8 @@ export const improvementModule = {
72
78
  archivePath: typeof args.archivePath === "string" ? args.archivePath : undefined
73
79
  };
74
80
  try {
75
- const sync = await runSyncTranscripts(ctx, syncArgs);
76
81
  const state = await loadImprovementState(ctx.workspacePath);
82
+ const sync = await runSyncTranscripts(ctx, syncArgs, state);
77
83
  state.lastSyncRunAt = new Date().toISOString();
78
84
  await saveImprovementState(ctx.workspacePath, state);
79
85
  return {
@@ -95,21 +101,10 @@ export const improvementModule = {
95
101
  };
96
102
  const now = new Date();
97
103
  try {
98
- const sync = await runSyncTranscripts(ctx, syncArgs);
99
104
  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);
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);
113
108
  state.lastSyncRunAt = now.toISOString();
114
109
  const generate = cadenceDecision.shouldRunGenerate || args.forceGenerate === true || args.runGenerate === true;
115
110
  let recommendations = null;
@@ -128,8 +123,8 @@ export const improvementModule = {
128
123
  data: {
129
124
  sync,
130
125
  cadence: {
131
- minIntervalMinutes,
132
- skipIfNoNewTranscripts,
126
+ minIntervalMinutes: cfg.minIntervalMinutes,
127
+ skipIfNoNewTranscripts: cfg.skipIfNoNewTranscripts,
133
128
  decision: cadenceDecision.reason
134
129
  },
135
130
  generatedRecommendations: recommendations
@@ -141,6 +136,46 @@ export const improvementModule = {
141
136
  return { ok: false, code: "ingest-failed", message: msg };
142
137
  }
143
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
+ }
144
179
  if (command.name === "query-lineage") {
145
180
  const taskId = typeof args.taskId === "string" ? args.taskId.trim() : "";
146
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
+ }
@@ -1,24 +1,60 @@
1
1
  import type { ModuleLifecycleContext } from "../../contracts/module-contract.js";
2
+ import type { ImprovementStateDocument } from "./improvement-state.js";
2
3
  export type TranscriptSyncArgs = {
3
4
  sourcePath?: string;
4
5
  archivePath?: string;
5
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";
6
8
  export type TranscriptSyncResult = {
9
+ runId: string;
7
10
  sourcePath: string;
8
11
  archivePath: string;
12
+ discoveredFrom: string;
13
+ discoveryCandidatesTried: string[];
9
14
  scanned: number;
10
15
  copied: number;
11
16
  skippedExisting: number;
12
17
  skippedConflict: number;
18
+ skippedBudget: number;
19
+ skippedLargeFile: number;
13
20
  errors: Array<{
14
21
  file: string;
15
22
  code: string;
16
23
  message: string;
17
24
  }>;
18
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
+ };
19
38
  };
20
- export declare function runSyncTranscripts(ctx: ModuleLifecycleContext, args: TranscriptSyncArgs): Promise<TranscriptSyncResult>;
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>;
21
56
  export declare function resolveCadenceDecision(now: Date, previousRunAtIso: string | null, minIntervalMinutes: number, copiedCount: number, skipIfNoNewTranscripts: boolean): {
22
57
  shouldRunGenerate: boolean;
23
58
  reason: string;
24
59
  };
60
+ export {};
@@ -1,7 +1,10 @@
1
1
  import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
- import { createHash } from "node:crypto";
4
- function resolveImprovementTranscriptConfig(ctx, args) {
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) {
5
8
  const improvement = ctx.effectiveConfig?.improvement && typeof ctx.effectiveConfig.improvement === "object"
6
9
  ? ctx.effectiveConfig.improvement
7
10
  : {};
@@ -19,13 +22,63 @@ function resolveImprovementTranscriptConfig(ctx, args) {
19
22
  ? cadence.minIntervalMinutes
20
23
  : 15;
21
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
+ }
22
41
  return {
23
- sourcePath: sourcePathArg || sourcePathCfg || ".cursor/agent-transcripts",
42
+ sourcePath: sourcePathArg || sourcePathCfg || "",
24
43
  archivePath: archivePathArg || archivePathCfg || "agent-transcripts",
25
44
  minIntervalMinutes: Math.max(1, Math.floor(minIntervalCfg)),
26
- skipIfNoNewTranscripts: skipIfNoNewCfg
45
+ skipIfNoNewTranscripts: skipIfNoNewCfg,
46
+ maxFilesPerSync: maxFilesCfg,
47
+ maxBytesPerFile: maxBytesFileCfg,
48
+ maxTotalScanBytes: maxTotalScanCfg,
49
+ discoveryPaths
27
50
  };
28
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
+ }
29
82
  async function listJsonlRelativePaths(root) {
30
83
  const out = [];
31
84
  async function walk(cur) {
@@ -43,23 +96,49 @@ async function listJsonlRelativePaths(root) {
43
96
  await walk(root);
44
97
  return out.sort((a, b) => a.localeCompare(b));
45
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
+ }
46
108
  async function fileSha256(filePath) {
47
109
  const buf = await fs.readFile(filePath);
48
110
  return createHash("sha256").update(buf).digest("hex");
49
111
  }
50
- export async function runSyncTranscripts(ctx, args) {
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()) {
51
116
  const cfg = resolveImprovementTranscriptConfig(ctx, args);
52
- const sourceRoot = path.resolve(ctx.workspacePath, cfg.sourcePath);
117
+ const runId = randomUUID();
118
+ const { root: sourceRoot, discoveredFrom, tried: discoveryCandidatesTried } = await resolveTranscriptSourceRoot(ctx.workspacePath, cfg, args);
53
119
  const archiveRoot = path.resolve(ctx.workspacePath, cfg.archivePath);
54
120
  const result = {
121
+ runId,
55
122
  sourcePath: path.relative(ctx.workspacePath, sourceRoot) || ".",
56
123
  archivePath: path.relative(ctx.workspacePath, archiveRoot) || ".",
124
+ discoveredFrom,
125
+ discoveryCandidatesTried,
57
126
  scanned: 0,
58
127
  copied: 0,
59
128
  skippedExisting: 0,
60
129
  skippedConflict: 0,
130
+ skippedBudget: 0,
131
+ skippedLargeFile: 0,
61
132
  errors: [],
62
- copiedFiles: []
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 }
63
142
  };
64
143
  let files = [];
65
144
  try {
@@ -70,16 +149,113 @@ export async function runSyncTranscripts(ctx, args) {
70
149
  result.errors.push({
71
150
  file: result.sourcePath,
72
151
  code: "source-read-error",
73
- message: msg
152
+ message: `${msg} (discovery tried: ${discoveryCandidatesTried.join(", ")})`
74
153
  });
75
154
  return result;
76
155
  }
77
- result.scanned = files.length;
78
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;
79
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
+ }
80
240
  const src = path.join(sourceRoot, rel);
81
241
  const dst = path.join(archiveRoot, rel);
242
+ budgetFiles += 1;
243
+ result.scanned += 1;
82
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
+ }
83
259
  await fs.mkdir(path.dirname(dst), { recursive: true });
84
260
  const srcHash = await fileSha256(src);
85
261
  let dstHash = null;
@@ -93,10 +269,12 @@ export async function runSyncTranscripts(ctx, args) {
93
269
  }
94
270
  if (dstHash === srcHash) {
95
271
  result.skippedExisting += 1;
272
+ result.skipReasons[rel] = "skipped-unchanged-hash";
96
273
  continue;
97
274
  }
98
275
  if (dstHash && dstHash !== srcHash) {
99
276
  result.skippedConflict += 1;
277
+ result.skipReasons[rel] = "skipped-archive-conflict";
100
278
  continue;
101
279
  }
102
280
  await fs.copyFile(src, dst);
@@ -106,8 +284,19 @@ export async function runSyncTranscripts(ctx, args) {
106
284
  catch (error) {
107
285
  const msg = error instanceof Error ? error.message : String(error);
108
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";
109
295
  }
110
296
  }
297
+ result.budget.scanBytesUsed = scanBytes;
298
+ state.transcriptRetryQueue = remainingRetries;
299
+ result.retryQueue.pending = remainingRetries.length;
111
300
  result.copiedFiles.sort((a, b) => a.localeCompare(b));
112
301
  return result;
113
302
  }
@@ -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, syncTaskHeadingsInMarkdown, 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, syncTaskHeadingsInMarkdown, 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, syncTaskHeadingsInMarkdown } from "./generator.js";
7
- export { importTasksFromMarkdown } from "./importer.js";
8
6
  export { getNextActions } from "./suggestions.js";
9
7
  export declare const taskEngineModule: WorkflowModule;
@@ -1,16 +1,11 @@
1
- import path from "node:path";
2
- import fs from "node:fs/promises";
1
+ import { maybeSpawnTranscriptHookAfterCompletion } from "../../core/transcript-completion-hook.js";
3
2
  import { TaskStore } from "./store.js";
4
3
  import { TransitionService } from "./service.js";
5
4
  import { TaskEngineError } from "./transitions.js";
6
- import { generateTasksMd, syncTaskHeadingsInMarkdown } from "./generator.js";
7
- import { importTasksFromMarkdown } from "./importer.js";
8
5
  import { getNextActions } from "./suggestions.js";
9
6
  export { TaskStore } from "./store.js";
10
7
  export { TransitionService } from "./service.js";
11
8
  export { TaskEngineError, TransitionValidator, isTransitionAllowed, getTransitionAction, resolveTargetState, getAllowedTransitionsFrom, stateValidityGuard, dependencyCheckGuard } from "./transitions.js";
12
- export { generateTasksMd, syncTaskHeadingsInMarkdown } from "./generator.js";
13
- export { importTasksFromMarkdown } from "./importer.js";
14
9
  export { getNextActions } from "./suggestions.js";
15
10
  function taskStorePath(ctx) {
16
11
  const tasks = ctx.effectiveConfig?.tasks;
@@ -61,16 +56,6 @@ export const taskEngineModule = {
61
56
  file: "get-ready-queue.md",
62
57
  description: "Get ready tasks sorted by priority."
63
58
  },
64
- {
65
- name: "import-tasks",
66
- file: "import-tasks.md",
67
- description: "One-time import from TASKS.md into engine state."
68
- },
69
- {
70
- name: "generate-tasks-md",
71
- file: "generate-tasks-md.md",
72
- description: "Generate read-only TASKS.md from engine state."
73
- },
74
59
  {
75
60
  name: "get-next-actions",
76
61
  file: "get-next-actions.md",
@@ -113,6 +98,9 @@ export const taskEngineModule = {
113
98
  try {
114
99
  const service = new TransitionService(store);
115
100
  const result = await service.runTransition({ taskId, action, actor });
101
+ if (result.evidence.toState === "completed") {
102
+ maybeSpawnTranscriptHookAfterCompletion(ctx.workspacePath, (ctx.effectiveConfig ?? {}));
103
+ }
116
104
  return {
117
105
  ok: true,
118
106
  code: "transition-applied",
@@ -190,68 +178,6 @@ export const taskEngineModule = {
190
178
  data: { tasks: ready, count: ready.length }
191
179
  };
192
180
  }
193
- if (command.name === "import-tasks") {
194
- const sourcePath = typeof args.sourcePath === "string"
195
- ? path.resolve(ctx.workspacePath, args.sourcePath)
196
- : path.resolve(ctx.workspacePath, "docs/maintainers/TASKS.md");
197
- try {
198
- const result = await importTasksFromMarkdown(sourcePath);
199
- store.replaceAllTasks(result.tasks);
200
- await store.save();
201
- return {
202
- ok: true,
203
- code: "tasks-imported",
204
- message: `Imported ${result.imported} tasks (${result.skipped} skipped)`,
205
- data: {
206
- imported: result.imported,
207
- skipped: result.skipped,
208
- errors: result.errors
209
- }
210
- };
211
- }
212
- catch (err) {
213
- if (err instanceof TaskEngineError) {
214
- return { ok: false, code: err.code, message: err.message };
215
- }
216
- return {
217
- ok: false,
218
- code: "import-parse-error",
219
- message: err.message
220
- };
221
- }
222
- }
223
- if (command.name === "generate-tasks-md") {
224
- const outputPath = typeof args.outputPath === "string"
225
- ? path.resolve(ctx.workspacePath, args.outputPath)
226
- : path.resolve(ctx.workspacePath, "docs/maintainers/TASKS.md");
227
- const tasks = store.getAllTasks();
228
- const fallbackMarkdown = generateTasksMd(tasks);
229
- let markdown = fallbackMarkdown;
230
- const preserveStructure = args.preserveStructure !== false;
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
- }
238
- await fs.mkdir(path.dirname(outputPath), { recursive: true });
239
- await fs.writeFile(outputPath, markdown, "utf8");
240
- }
241
- catch (err) {
242
- return {
243
- ok: false,
244
- code: "storage-write-error",
245
- message: `Failed to write TASKS.md: ${err.message}`
246
- };
247
- }
248
- return {
249
- ok: true,
250
- code: "tasks-md-generated",
251
- message: `Generated TASKS.md with ${tasks.length} tasks`,
252
- data: { outputPath, taskCount: tasks.length }
253
- };
254
- }
255
181
  if (command.name === "get-next-actions") {
256
182
  const tasks = store.getAllTasks();
257
183
  const suggestion = getNextActions(tasks);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@workflow-cannon/workspace-kit",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "private": false,
5
5
  "packageManager": "pnpm@10.0.0",
6
6
  "license": "MIT",
@@ -31,7 +31,10 @@
31
31
  "generate-runtime-diagnostics": "node scripts/generate-runtime-diagnostics.mjs",
32
32
  "prune-evidence": "node scripts/prune-evidence.mjs",
33
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"
34
+ "phase5-gates": "pnpm run phase4-gates && pnpm run test",
35
+ "pre-release-transcript-hook": "pnpm run build && node scripts/pre-release-transcript-hook.mjs",
36
+ "transcript:sync": "node scripts/run-transcript-cli.mjs sync-transcripts",
37
+ "transcript:ingest": "node scripts/run-transcript-cli.mjs ingest-transcripts"
35
38
  },
36
39
  "devDependencies": {
37
40
  "@types/node": "^25.5.0",
@@ -1,3 +0,0 @@
1
- import type { TaskEntity } from "./types.js";
2
- export declare function generateTasksMd(tasks: TaskEntity[]): string;
3
- export declare function syncTaskHeadingsInMarkdown(markdown: string, tasks: TaskEntity[]): string;