@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.
- package/README.md +5 -4
- package/dist/cli/run-command.d.ts +11 -0
- package/dist/cli/run-command.js +138 -0
- package/dist/cli.js +18 -135
- package/dist/contracts/index.d.ts +1 -1
- package/dist/contracts/module-contract.d.ts +13 -0
- package/dist/core/config-cli.js +4 -4
- package/dist/core/config-metadata.js +199 -5
- package/dist/core/index.d.ts +6 -0
- package/dist/core/index.js +6 -0
- package/dist/core/instruction-template-mapper.d.ts +9 -0
- package/dist/core/instruction-template-mapper.js +35 -0
- package/dist/core/lineage-contract.d.ts +1 -1
- package/dist/core/lineage-contract.js +1 -1
- package/dist/core/policy.d.ts +13 -2
- package/dist/core/policy.js +42 -25
- package/dist/core/response-template-contract.d.ts +15 -0
- package/dist/core/response-template-contract.js +10 -0
- package/dist/core/response-template-registry.d.ts +4 -0
- package/dist/core/response-template-registry.js +44 -0
- package/dist/core/response-template-shaping.d.ts +6 -0
- package/dist/core/response-template-shaping.js +128 -0
- package/dist/core/session-policy.d.ts +18 -0
- package/dist/core/session-policy.js +57 -0
- package/dist/core/transcript-completion-hook.d.ts +7 -0
- package/dist/core/transcript-completion-hook.js +128 -0
- package/dist/core/workspace-kit-config.d.ts +2 -1
- package/dist/core/workspace-kit-config.js +19 -23
- package/dist/modules/approvals/index.js +2 -2
- package/dist/modules/documentation/runtime.js +413 -20
- package/dist/modules/improvement/generate-recommendations-runtime.d.ts +7 -0
- package/dist/modules/improvement/generate-recommendations-runtime.js +37 -4
- package/dist/modules/improvement/improvement-state.d.ts +10 -1
- package/dist/modules/improvement/improvement-state.js +36 -7
- package/dist/modules/improvement/index.js +70 -23
- package/dist/modules/improvement/ingest.js +2 -1
- package/dist/modules/improvement/transcript-redaction.d.ts +4 -0
- package/dist/modules/improvement/transcript-redaction.js +10 -0
- package/dist/modules/improvement/transcript-sync-runtime.d.ts +42 -1
- package/dist/modules/improvement/transcript-sync-runtime.js +215 -9
- package/dist/modules/index.d.ts +1 -1
- package/dist/modules/index.js +1 -1
- package/dist/modules/task-engine/index.d.ts +0 -2
- package/dist/modules/task-engine/index.js +4 -78
- package/package.json +6 -2
- package/src/modules/documentation/README.md +39 -0
- package/src/modules/documentation/RULES.md +70 -0
- package/src/modules/documentation/config.md +14 -0
- package/src/modules/documentation/index.ts +120 -0
- package/src/modules/documentation/instructions/document-project.md +44 -0
- package/src/modules/documentation/instructions/documentation-maintainer.md +81 -0
- package/src/modules/documentation/instructions/generate-document.md +44 -0
- package/src/modules/documentation/runtime.ts +870 -0
- package/src/modules/documentation/schemas/documentation-schema.md +54 -0
- package/src/modules/documentation/state.md +8 -0
- package/src/modules/documentation/templates/AGENTS.md +84 -0
- package/src/modules/documentation/templates/ARCHITECTURE.md +71 -0
- package/src/modules/documentation/templates/PRINCIPLES.md +122 -0
- package/src/modules/documentation/templates/RELEASING.md +96 -0
- package/src/modules/documentation/templates/ROADMAP.md +131 -0
- package/src/modules/documentation/templates/SECURITY.md +53 -0
- package/src/modules/documentation/templates/SUPPORT.md +40 -0
- package/src/modules/documentation/templates/TERMS.md +61 -0
- package/src/modules/documentation/templates/runbooks/consumer-cadence.md +55 -0
- package/src/modules/documentation/templates/runbooks/parity-validation-flow.md +68 -0
- package/src/modules/documentation/templates/runbooks/release-channels.md +30 -0
- package/src/modules/documentation/templates/workbooks/phase2-config-policy-workbook.md +42 -0
- package/src/modules/documentation/templates/workbooks/task-engine-workbook.md +42 -0
- package/src/modules/documentation/templates/workbooks/transcript-automation-baseline.md +68 -0
- package/src/modules/documentation/types.ts +51 -0
- package/dist/modules/task-engine/generator.d.ts +0 -3
- package/dist/modules/task-engine/generator.js +0 -118
- package/dist/modules/task-engine/importer.d.ts +0 -8
- 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 =
|
|
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
|
|
23
|
-
const
|
|
24
|
-
|
|
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
|
-
|
|
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 {
|
|
3
|
-
import {
|
|
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.
|
|
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
|
|
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: `
|
|
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
|
|
101
|
-
|
|
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,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
|
-
|
|
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
|
-
|
|
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 || "
|
|
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
|
-
|
|
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
|
|
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
|
}
|
package/dist/modules/index.d.ts
CHANGED
|
@@ -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,
|
|
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";
|
package/dist/modules/index.js
CHANGED
|
@@ -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,
|
|
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;
|