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