@workflow-cannon/workspace-kit 0.7.0 → 0.9.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 (74) hide show
  1. package/README.md +5 -4
  2. package/dist/cli/run-command.d.ts +11 -0
  3. package/dist/cli/run-command.js +138 -0
  4. package/dist/cli.js +18 -135
  5. package/dist/contracts/index.d.ts +1 -1
  6. package/dist/contracts/module-contract.d.ts +13 -0
  7. package/dist/core/config-cli.js +4 -4
  8. package/dist/core/config-metadata.js +199 -5
  9. package/dist/core/index.d.ts +6 -0
  10. package/dist/core/index.js +6 -0
  11. package/dist/core/instruction-template-mapper.d.ts +9 -0
  12. package/dist/core/instruction-template-mapper.js +35 -0
  13. package/dist/core/lineage-contract.d.ts +1 -1
  14. package/dist/core/lineage-contract.js +1 -1
  15. package/dist/core/policy.d.ts +13 -2
  16. package/dist/core/policy.js +42 -25
  17. package/dist/core/response-template-contract.d.ts +15 -0
  18. package/dist/core/response-template-contract.js +10 -0
  19. package/dist/core/response-template-registry.d.ts +4 -0
  20. package/dist/core/response-template-registry.js +44 -0
  21. package/dist/core/response-template-shaping.d.ts +6 -0
  22. package/dist/core/response-template-shaping.js +128 -0
  23. package/dist/core/session-policy.d.ts +18 -0
  24. package/dist/core/session-policy.js +57 -0
  25. package/dist/core/transcript-completion-hook.d.ts +7 -0
  26. package/dist/core/transcript-completion-hook.js +128 -0
  27. package/dist/core/workspace-kit-config.d.ts +2 -1
  28. package/dist/core/workspace-kit-config.js +19 -23
  29. package/dist/modules/approvals/index.js +2 -2
  30. package/dist/modules/documentation/runtime.js +413 -20
  31. package/dist/modules/improvement/generate-recommendations-runtime.d.ts +7 -0
  32. package/dist/modules/improvement/generate-recommendations-runtime.js +37 -4
  33. package/dist/modules/improvement/improvement-state.d.ts +10 -1
  34. package/dist/modules/improvement/improvement-state.js +36 -7
  35. package/dist/modules/improvement/index.js +70 -23
  36. package/dist/modules/improvement/ingest.js +2 -1
  37. package/dist/modules/improvement/transcript-redaction.d.ts +4 -0
  38. package/dist/modules/improvement/transcript-redaction.js +10 -0
  39. package/dist/modules/improvement/transcript-sync-runtime.d.ts +42 -1
  40. package/dist/modules/improvement/transcript-sync-runtime.js +215 -9
  41. package/dist/modules/index.d.ts +1 -1
  42. package/dist/modules/index.js +1 -1
  43. package/dist/modules/task-engine/index.d.ts +0 -2
  44. package/dist/modules/task-engine/index.js +4 -78
  45. package/package.json +6 -2
  46. package/src/modules/documentation/README.md +39 -0
  47. package/src/modules/documentation/RULES.md +70 -0
  48. package/src/modules/documentation/config.md +14 -0
  49. package/src/modules/documentation/index.ts +120 -0
  50. package/src/modules/documentation/instructions/document-project.md +44 -0
  51. package/src/modules/documentation/instructions/documentation-maintainer.md +81 -0
  52. package/src/modules/documentation/instructions/generate-document.md +44 -0
  53. package/src/modules/documentation/runtime.ts +870 -0
  54. package/src/modules/documentation/schemas/documentation-schema.md +54 -0
  55. package/src/modules/documentation/state.md +8 -0
  56. package/src/modules/documentation/templates/AGENTS.md +84 -0
  57. package/src/modules/documentation/templates/ARCHITECTURE.md +71 -0
  58. package/src/modules/documentation/templates/PRINCIPLES.md +122 -0
  59. package/src/modules/documentation/templates/RELEASING.md +96 -0
  60. package/src/modules/documentation/templates/ROADMAP.md +131 -0
  61. package/src/modules/documentation/templates/SECURITY.md +53 -0
  62. package/src/modules/documentation/templates/SUPPORT.md +40 -0
  63. package/src/modules/documentation/templates/TERMS.md +61 -0
  64. package/src/modules/documentation/templates/runbooks/consumer-cadence.md +55 -0
  65. package/src/modules/documentation/templates/runbooks/parity-validation-flow.md +68 -0
  66. package/src/modules/documentation/templates/runbooks/release-channels.md +30 -0
  67. package/src/modules/documentation/templates/workbooks/phase2-config-policy-workbook.md +42 -0
  68. package/src/modules/documentation/templates/workbooks/task-engine-workbook.md +42 -0
  69. package/src/modules/documentation/templates/workbooks/transcript-automation-baseline.md +68 -0
  70. package/src/modules/documentation/types.ts +51 -0
  71. package/dist/modules/task-engine/generator.d.ts +0 -3
  72. package/dist/modules/task-engine/generator.js +0 -118
  73. package/dist/modules/task-engine/importer.d.ts +0 -8
  74. package/dist/modules/task-engine/importer.js +0 -163
@@ -1,6 +1,6 @@
1
1
  import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
- export const IMPROVEMENT_STATE_SCHEMA_VERSION = 1;
3
+ export const IMPROVEMENT_STATE_SCHEMA_VERSION = 2;
4
4
  const DEFAULT_REL = ".workspace-kit/improvement/state.json";
5
5
  function statePath(workspacePath) {
6
6
  return path.join(workspacePath, DEFAULT_REL);
@@ -13,21 +13,46 @@ export function emptyImprovementState() {
13
13
  transitionLogLengthCursor: 0,
14
14
  transcriptLineCursors: {},
15
15
  lastSyncRunAt: null,
16
- lastIngestRunAt: null
16
+ lastIngestRunAt: null,
17
+ transcriptRetryQueue: []
18
+ };
19
+ }
20
+ function migrateFromV1(raw) {
21
+ const base = emptyImprovementState();
22
+ return {
23
+ ...base,
24
+ policyTraceLineCursor: typeof raw.policyTraceLineCursor === "number" ? raw.policyTraceLineCursor : 0,
25
+ mutationLineCursor: typeof raw.mutationLineCursor === "number" ? raw.mutationLineCursor : 0,
26
+ transitionLogLengthCursor: typeof raw.transitionLogLengthCursor === "number" ? raw.transitionLogLengthCursor : 0,
27
+ transcriptLineCursors: raw.transcriptLineCursors && typeof raw.transcriptLineCursors === "object" && raw.transcriptLineCursors !== null
28
+ ? raw.transcriptLineCursors
29
+ : {},
30
+ lastSyncRunAt: typeof raw.lastSyncRunAt === "string" ? raw.lastSyncRunAt : null,
31
+ lastIngestRunAt: typeof raw.lastIngestRunAt === "string" ? raw.lastIngestRunAt : null
17
32
  };
18
33
  }
19
34
  export async function loadImprovementState(workspacePath) {
20
35
  const fp = statePath(workspacePath);
21
36
  try {
22
- const raw = await fs.readFile(fp, "utf8");
23
- const doc = JSON.parse(raw);
24
- if (doc.schemaVersion !== IMPROVEMENT_STATE_SCHEMA_VERSION) {
37
+ const rawText = await fs.readFile(fp, "utf8");
38
+ const raw = JSON.parse(rawText);
39
+ const ver = raw.schemaVersion;
40
+ if (ver === 1) {
41
+ return migrateFromV1(raw);
42
+ }
43
+ if (ver !== IMPROVEMENT_STATE_SCHEMA_VERSION) {
25
44
  return emptyImprovementState();
26
45
  }
46
+ const doc = raw;
27
47
  return {
28
48
  ...emptyImprovementState(),
29
49
  ...doc,
30
- transcriptLineCursors: doc.transcriptLineCursors ?? {}
50
+ transcriptLineCursors: doc.transcriptLineCursors ?? {},
51
+ transcriptRetryQueue: Array.isArray(doc.transcriptRetryQueue)
52
+ ? doc.transcriptRetryQueue.filter((e) => e !== null &&
53
+ typeof e === "object" &&
54
+ typeof e.relativePath === "string")
55
+ : []
31
56
  };
32
57
  }
33
58
  catch (e) {
@@ -40,5 +65,9 @@ export async function loadImprovementState(workspacePath) {
40
65
  export async function saveImprovementState(workspacePath, doc) {
41
66
  const fp = statePath(workspacePath);
42
67
  await fs.mkdir(path.dirname(fp), { recursive: true });
43
- await fs.writeFile(fp, `${JSON.stringify(doc, null, 2)}\n`, "utf8");
68
+ const out = {
69
+ ...doc,
70
+ schemaVersion: IMPROVEMENT_STATE_SCHEMA_VERSION
71
+ };
72
+ await fs.writeFile(fp, `${JSON.stringify(out, null, 2)}\n`, "utf8");
44
73
  }
@@ -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
  }
@@ -52,13 +58,25 @@ export const improvementModule = {
52
58
  const transcriptsRoot = typeof args.transcriptsRoot === "string" ? args.transcriptsRoot : undefined;
53
59
  const fromTag = typeof args.fromTag === "string" ? args.fromTag : undefined;
54
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
+ };
55
65
  try {
56
- 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
+ });
57
75
  return {
58
76
  ok: true,
59
77
  code: "recommendations-generated",
60
- message: `Created ${result.created.length} improvement task(s); skipped ${result.skipped} duplicate(s)`,
61
- 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 }
62
80
  };
63
81
  }
64
82
  catch (e) {
@@ -72,8 +90,8 @@ export const improvementModule = {
72
90
  archivePath: typeof args.archivePath === "string" ? args.archivePath : undefined
73
91
  };
74
92
  try {
75
- const sync = await runSyncTranscripts(ctx, syncArgs);
76
93
  const state = await loadImprovementState(ctx.workspacePath);
94
+ const sync = await runSyncTranscripts(ctx, syncArgs, state);
77
95
  state.lastSyncRunAt = new Date().toISOString();
78
96
  await saveImprovementState(ctx.workspacePath, state);
79
97
  return {
@@ -95,21 +113,10 @@ export const improvementModule = {
95
113
  };
96
114
  const now = new Date();
97
115
  try {
98
- const sync = await runSyncTranscripts(ctx, syncArgs);
99
116
  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);
117
+ const sync = await runSyncTranscripts(ctx, syncArgs, state);
118
+ const cfg = resolveImprovementTranscriptConfig(ctx, syncArgs);
119
+ const cadenceDecision = resolveCadenceDecision(now, state.lastIngestRunAt, cfg.minIntervalMinutes, sync.copied, cfg.skipIfNoNewTranscripts);
113
120
  state.lastSyncRunAt = now.toISOString();
114
121
  const generate = cadenceDecision.shouldRunGenerate || args.forceGenerate === true || args.runGenerate === true;
115
122
  let recommendations = null;
@@ -128,8 +135,8 @@ export const improvementModule = {
128
135
  data: {
129
136
  sync,
130
137
  cadence: {
131
- minIntervalMinutes,
132
- skipIfNoNewTranscripts,
138
+ minIntervalMinutes: cfg.minIntervalMinutes,
139
+ skipIfNoNewTranscripts: cfg.skipIfNoNewTranscripts,
133
140
  decision: cadenceDecision.reason
134
141
  },
135
142
  generatedRecommendations: recommendations
@@ -141,6 +148,46 @@ export const improvementModule = {
141
148
  return { ok: false, code: "ingest-failed", message: msg };
142
149
  }
143
150
  }
151
+ if (command.name === "transcript-automation-status") {
152
+ const syncArgs = {
153
+ sourcePath: typeof args.sourcePath === "string" ? args.sourcePath : undefined,
154
+ archivePath: typeof args.archivePath === "string" ? args.archivePath : undefined
155
+ };
156
+ const state = await loadImprovementState(ctx.workspacePath);
157
+ const cfg = resolveImprovementTranscriptConfig(ctx, syncArgs);
158
+ return {
159
+ ok: true,
160
+ code: "transcript-automation-status",
161
+ message: "Transcript automation status",
162
+ data: {
163
+ schemaVersion: 1,
164
+ lastSyncRunAt: state.lastSyncRunAt,
165
+ lastIngestRunAt: state.lastIngestRunAt,
166
+ cadence: {
167
+ minIntervalMinutes: cfg.minIntervalMinutes,
168
+ skipIfNoNewTranscripts: cfg.skipIfNoNewTranscripts,
169
+ maxRecommendationCandidatesPerRun: getMaxRecommendationCandidatesPerRun(ctx)
170
+ },
171
+ transcripts: {
172
+ sourcePath: cfg.sourcePath || null,
173
+ archivePath: cfg.archivePath,
174
+ discoveryPaths: cfg.discoveryPaths,
175
+ budgets: {
176
+ maxFilesPerSync: cfg.maxFilesPerSync,
177
+ maxBytesPerFile: cfg.maxBytesPerFile,
178
+ maxTotalScanBytes: cfg.maxTotalScanBytes
179
+ }
180
+ },
181
+ retryQueue: {
182
+ pending: state.transcriptRetryQueue?.length ?? 0,
183
+ entries: state.transcriptRetryQueue ?? []
184
+ },
185
+ policySession: {
186
+ sessionId: resolveSessionId(process.env)
187
+ }
188
+ }
189
+ };
190
+ }
144
191
  if (command.name === "query-lineage") {
145
192
  const taskId = typeof args.taskId === "string" ? args.taskId.trim() : "";
146
193
  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,65 @@
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
+ /**
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;
55
+ export declare function resolveTranscriptSourceRoot(workspacePath: string, cfg: ImprovementTranscriptConfig, args: TranscriptSyncArgs): Promise<{
56
+ root: string;
57
+ discoveredFrom: string;
58
+ tried: string[];
59
+ }>;
60
+ export declare function runSyncTranscripts(ctx: ModuleLifecycleContext, args: TranscriptSyncArgs, state: ImprovementStateDocument, now?: Date): Promise<TranscriptSyncResult>;
21
61
  export declare function resolveCadenceDecision(now: Date, previousRunAtIso: string | null, minIntervalMinutes: number, copiedCount: number, skipIfNoNewTranscripts: boolean): {
22
62
  shouldRunGenerate: boolean;
23
63
  reason: string;
24
64
  };
65
+ export {};
@@ -1,7 +1,11 @@
1
1
  import fs from "node:fs/promises";
2
+ import os from "node:os";
2
3
  import path from "node:path";
3
- import { createHash } from "node:crypto";
4
- function resolveImprovementTranscriptConfig(ctx, args) {
4
+ import { createHash, randomUUID } from "node:crypto";
5
+ const DEFAULT_DISCOVERY_PATHS = [".cursor/agent-transcripts", ".vscode/agent-transcripts"];
6
+ const MAX_RETRY_ATTEMPTS = 5;
7
+ const INITIAL_BACKOFF_MS = 60_000;
8
+ export function resolveImprovementTranscriptConfig(ctx, args) {
5
9
  const improvement = ctx.effectiveConfig?.improvement && typeof ctx.effectiveConfig.improvement === "object"
6
10
  ? ctx.effectiveConfig.improvement
7
11
  : {};
@@ -19,13 +23,79 @@ function resolveImprovementTranscriptConfig(ctx, args) {
19
23
  ? cadence.minIntervalMinutes
20
24
  : 15;
21
25
  const skipIfNoNewCfg = typeof cadence.skipIfNoNewTranscripts === "boolean" ? cadence.skipIfNoNewTranscripts : true;
26
+ const maxFilesCfg = typeof transcripts.maxFilesPerSync === "number" && Number.isFinite(transcripts.maxFilesPerSync)
27
+ ? Math.max(1, Math.floor(transcripts.maxFilesPerSync))
28
+ : 5000;
29
+ const maxBytesFileCfg = typeof transcripts.maxBytesPerFile === "number" && Number.isFinite(transcripts.maxBytesPerFile)
30
+ ? Math.max(1024, Math.floor(transcripts.maxBytesPerFile))
31
+ : 50_000_000;
32
+ const maxTotalScanCfg = typeof transcripts.maxTotalScanBytes === "number" && Number.isFinite(transcripts.maxTotalScanBytes)
33
+ ? Math.max(1024, Math.floor(transcripts.maxTotalScanBytes))
34
+ : 500_000_000;
35
+ let discoveryPaths = [];
36
+ if (Array.isArray(transcripts.discoveryPaths)) {
37
+ discoveryPaths = transcripts.discoveryPaths.filter((x) => typeof x === "string" && x.trim().length > 0);
38
+ }
39
+ if (discoveryPaths.length === 0) {
40
+ discoveryPaths = [...DEFAULT_DISCOVERY_PATHS];
41
+ }
22
42
  return {
23
- sourcePath: sourcePathArg || sourcePathCfg || ".cursor/agent-transcripts",
43
+ sourcePath: sourcePathArg || sourcePathCfg || "",
24
44
  archivePath: archivePathArg || archivePathCfg || "agent-transcripts",
25
45
  minIntervalMinutes: Math.max(1, Math.floor(minIntervalCfg)),
26
- skipIfNoNewTranscripts: skipIfNoNewCfg
46
+ skipIfNoNewTranscripts: skipIfNoNewCfg,
47
+ maxFilesPerSync: maxFilesCfg,
48
+ maxBytesPerFile: maxBytesFileCfg,
49
+ maxTotalScanBytes: maxTotalScanCfg,
50
+ discoveryPaths
27
51
  };
28
52
  }
53
+ async function pathExists(abs) {
54
+ try {
55
+ await fs.access(abs);
56
+ return true;
57
+ }
58
+ catch {
59
+ return false;
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
+ }
72
+ export async function resolveTranscriptSourceRoot(workspacePath, cfg, args) {
73
+ const sourcePathArg = typeof args.sourcePath === "string" ? args.sourcePath.trim() : "";
74
+ if (sourcePathArg) {
75
+ const root = path.resolve(workspacePath, sourcePathArg);
76
+ return { root, discoveredFrom: sourcePathArg, tried: [sourcePathArg] };
77
+ }
78
+ if (cfg.sourcePath) {
79
+ const root = path.resolve(workspacePath, cfg.sourcePath);
80
+ return { root, discoveredFrom: cfg.sourcePath, tried: [cfg.sourcePath] };
81
+ }
82
+ const tried = [];
83
+ for (const rel of cfg.discoveryPaths) {
84
+ tried.push(rel);
85
+ const abs = path.resolve(workspacePath, rel);
86
+ if (await pathExists(abs)) {
87
+ return { root: abs, discoveredFrom: rel, tried };
88
+ }
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
+ }
96
+ const fallback = path.resolve(workspacePath, ".cursor/agent-transcripts");
97
+ return { root: fallback, discoveredFrom: ".cursor/agent-transcripts (fallback)", tried };
98
+ }
29
99
  async function listJsonlRelativePaths(root) {
30
100
  const out = [];
31
101
  async function walk(cur) {
@@ -43,23 +113,49 @@ async function listJsonlRelativePaths(root) {
43
113
  await walk(root);
44
114
  return out.sort((a, b) => a.localeCompare(b));
45
115
  }
116
+ async function statSize(filePath) {
117
+ try {
118
+ const st = await fs.stat(filePath);
119
+ return st.size;
120
+ }
121
+ catch {
122
+ return null;
123
+ }
124
+ }
46
125
  async function fileSha256(filePath) {
47
126
  const buf = await fs.readFile(filePath);
48
127
  return createHash("sha256").update(buf).digest("hex");
49
128
  }
50
- export async function runSyncTranscripts(ctx, args) {
129
+ function nextBackoffMs(attempts) {
130
+ return INITIAL_BACKOFF_MS * 2 ** Math.max(0, attempts - 1);
131
+ }
132
+ export async function runSyncTranscripts(ctx, args, state, now = new Date()) {
51
133
  const cfg = resolveImprovementTranscriptConfig(ctx, args);
52
- const sourceRoot = path.resolve(ctx.workspacePath, cfg.sourcePath);
134
+ const runId = randomUUID();
135
+ const { root: sourceRoot, discoveredFrom, tried: discoveryCandidatesTried } = await resolveTranscriptSourceRoot(ctx.workspacePath, cfg, args);
53
136
  const archiveRoot = path.resolve(ctx.workspacePath, cfg.archivePath);
54
137
  const result = {
138
+ runId,
55
139
  sourcePath: path.relative(ctx.workspacePath, sourceRoot) || ".",
56
140
  archivePath: path.relative(ctx.workspacePath, archiveRoot) || ".",
141
+ discoveredFrom,
142
+ discoveryCandidatesTried,
57
143
  scanned: 0,
58
144
  copied: 0,
59
145
  skippedExisting: 0,
60
146
  skippedConflict: 0,
147
+ skippedBudget: 0,
148
+ skippedLargeFile: 0,
61
149
  errors: [],
62
- copiedFiles: []
150
+ copiedFiles: [],
151
+ skipReasons: {},
152
+ budget: {
153
+ maxFilesPerSync: cfg.maxFilesPerSync,
154
+ maxBytesPerFile: cfg.maxBytesPerFile,
155
+ maxTotalScanBytes: cfg.maxTotalScanBytes,
156
+ scanBytesUsed: 0
157
+ },
158
+ retryQueue: { pending: 0, processedRetries: 0, droppedPermanentFailures: 0 }
63
159
  };
64
160
  let files = [];
65
161
  try {
@@ -70,16 +166,113 @@ export async function runSyncTranscripts(ctx, args) {
70
166
  result.errors.push({
71
167
  file: result.sourcePath,
72
168
  code: "source-read-error",
73
- message: msg
169
+ message: `${msg} (discovery tried: ${discoveryCandidatesTried.join(", ")})`
74
170
  });
75
171
  return result;
76
172
  }
77
- result.scanned = files.length;
78
173
  await fs.mkdir(archiveRoot, { recursive: true });
174
+ const queue = state.transcriptRetryQueue ?? [];
175
+ const remainingRetries = [];
176
+ let scanBytes = 0;
177
+ for (const entry of queue) {
178
+ const due = new Date(entry.nextRetryAt).getTime() <= now.getTime();
179
+ if (!due) {
180
+ remainingRetries.push(entry);
181
+ continue;
182
+ }
183
+ if (entry.attempts >= MAX_RETRY_ATTEMPTS) {
184
+ result.retryQueue.droppedPermanentFailures += 1;
185
+ result.errors.push({
186
+ file: entry.relativePath,
187
+ code: "retry-exhausted",
188
+ message: entry.lastErrorMessage
189
+ });
190
+ continue;
191
+ }
192
+ const src = path.join(sourceRoot, entry.relativePath);
193
+ const dst = path.join(archiveRoot, entry.relativePath);
194
+ try {
195
+ await fs.mkdir(path.dirname(dst), { recursive: true });
196
+ const sz = await statSize(src);
197
+ if (sz !== null && sz > cfg.maxBytesPerFile) {
198
+ remainingRetries.push({
199
+ ...entry,
200
+ attempts: entry.attempts + 1,
201
+ lastErrorCode: "file-too-large",
202
+ lastErrorMessage: `size ${sz} exceeds maxBytesPerFile`,
203
+ nextRetryAt: new Date(now.getTime() + nextBackoffMs(entry.attempts + 1)).toISOString()
204
+ });
205
+ continue;
206
+ }
207
+ if (sz !== null) {
208
+ scanBytes += sz;
209
+ }
210
+ const srcHash = await fileSha256(src);
211
+ let dstHash = null;
212
+ try {
213
+ dstHash = await fileSha256(dst);
214
+ }
215
+ catch (error) {
216
+ if (error.code !== "ENOENT") {
217
+ throw error;
218
+ }
219
+ }
220
+ if (dstHash === srcHash) {
221
+ result.retryQueue.processedRetries += 1;
222
+ continue;
223
+ }
224
+ if (dstHash && dstHash !== srcHash) {
225
+ remainingRetries.push({
226
+ ...entry,
227
+ attempts: entry.attempts + 1,
228
+ lastErrorCode: "archive-conflict",
229
+ lastErrorMessage: "destination exists with different content",
230
+ nextRetryAt: new Date(now.getTime() + nextBackoffMs(entry.attempts + 1)).toISOString()
231
+ });
232
+ continue;
233
+ }
234
+ await fs.copyFile(src, dst);
235
+ result.copied += 1;
236
+ result.copiedFiles.push(entry.relativePath);
237
+ result.retryQueue.processedRetries += 1;
238
+ }
239
+ catch (error) {
240
+ const msg = error instanceof Error ? error.message : String(error);
241
+ remainingRetries.push({
242
+ ...entry,
243
+ attempts: entry.attempts + 1,
244
+ lastErrorCode: "copy-error",
245
+ lastErrorMessage: msg,
246
+ nextRetryAt: new Date(now.getTime() + nextBackoffMs(entry.attempts + 1)).toISOString()
247
+ });
248
+ }
249
+ }
250
+ let budgetFiles = 0;
79
251
  for (const rel of files) {
252
+ if (budgetFiles >= cfg.maxFilesPerSync) {
253
+ result.skippedBudget += 1;
254
+ result.skipReasons[rel] = "skipped-budget-max-files";
255
+ continue;
256
+ }
80
257
  const src = path.join(sourceRoot, rel);
81
258
  const dst = path.join(archiveRoot, rel);
259
+ budgetFiles += 1;
260
+ result.scanned += 1;
82
261
  try {
262
+ const sz = await statSize(src);
263
+ if (sz !== null && sz > cfg.maxBytesPerFile) {
264
+ result.skippedLargeFile += 1;
265
+ result.skipReasons[rel] = "skipped-file-too-large";
266
+ continue;
267
+ }
268
+ if (sz !== null && scanBytes + sz > cfg.maxTotalScanBytes) {
269
+ result.skippedBudget += 1;
270
+ result.skipReasons[rel] = "skipped-budget-total-bytes";
271
+ continue;
272
+ }
273
+ if (sz !== null) {
274
+ scanBytes += sz;
275
+ }
83
276
  await fs.mkdir(path.dirname(dst), { recursive: true });
84
277
  const srcHash = await fileSha256(src);
85
278
  let dstHash = null;
@@ -93,10 +286,12 @@ export async function runSyncTranscripts(ctx, args) {
93
286
  }
94
287
  if (dstHash === srcHash) {
95
288
  result.skippedExisting += 1;
289
+ result.skipReasons[rel] = "skipped-unchanged-hash";
96
290
  continue;
97
291
  }
98
292
  if (dstHash && dstHash !== srcHash) {
99
293
  result.skippedConflict += 1;
294
+ result.skipReasons[rel] = "skipped-archive-conflict";
100
295
  continue;
101
296
  }
102
297
  await fs.copyFile(src, dst);
@@ -106,8 +301,19 @@ export async function runSyncTranscripts(ctx, args) {
106
301
  catch (error) {
107
302
  const msg = error instanceof Error ? error.message : String(error);
108
303
  result.errors.push({ file: rel, code: "copy-error", message: msg });
304
+ remainingRetries.push({
305
+ relativePath: rel,
306
+ attempts: 1,
307
+ lastErrorCode: "copy-error",
308
+ lastErrorMessage: msg,
309
+ nextRetryAt: new Date(now.getTime() + INITIAL_BACKOFF_MS).toISOString()
310
+ });
311
+ result.skipReasons[rel] = "skipped-read-error";
109
312
  }
110
313
  }
314
+ result.budget.scanBytesUsed = scanBytes;
315
+ state.transcriptRetryQueue = remainingRetries;
316
+ result.retryQueue.pending = remainingRetries.length;
111
317
  result.copiedFiles.sort((a, b) => a.localeCompare(b));
112
318
  return result;
113
319
  }
@@ -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;