@workflow-cannon/workspace-kit 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +197 -3
- 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 +3 -3
- 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 +25 -4
- 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 +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 +55 -20
- 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 +37 -1
- package/dist/modules/improvement/transcript-sync-runtime.js +198 -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 +5 -2
- 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,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
|
}
|
|
@@ -72,8 +78,8 @@ export const improvementModule = {
|
|
|
72
78
|
archivePath: typeof args.archivePath === "string" ? args.archivePath : undefined
|
|
73
79
|
};
|
|
74
80
|
try {
|
|
75
|
-
const sync = await runSyncTranscripts(ctx, syncArgs);
|
|
76
81
|
const state = await loadImprovementState(ctx.workspacePath);
|
|
82
|
+
const sync = await runSyncTranscripts(ctx, syncArgs, state);
|
|
77
83
|
state.lastSyncRunAt = new Date().toISOString();
|
|
78
84
|
await saveImprovementState(ctx.workspacePath, state);
|
|
79
85
|
return {
|
|
@@ -95,21 +101,10 @@ export const improvementModule = {
|
|
|
95
101
|
};
|
|
96
102
|
const now = new Date();
|
|
97
103
|
try {
|
|
98
|
-
const sync = await runSyncTranscripts(ctx, syncArgs);
|
|
99
104
|
const state = await loadImprovementState(ctx.workspacePath);
|
|
100
|
-
const
|
|
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);
|
|
105
|
+
const sync = await runSyncTranscripts(ctx, syncArgs, state);
|
|
106
|
+
const cfg = resolveImprovementTranscriptConfig(ctx, syncArgs);
|
|
107
|
+
const cadenceDecision = resolveCadenceDecision(now, state.lastIngestRunAt, cfg.minIntervalMinutes, sync.copied, cfg.skipIfNoNewTranscripts);
|
|
113
108
|
state.lastSyncRunAt = now.toISOString();
|
|
114
109
|
const generate = cadenceDecision.shouldRunGenerate || args.forceGenerate === true || args.runGenerate === true;
|
|
115
110
|
let recommendations = null;
|
|
@@ -128,8 +123,8 @@ export const improvementModule = {
|
|
|
128
123
|
data: {
|
|
129
124
|
sync,
|
|
130
125
|
cadence: {
|
|
131
|
-
minIntervalMinutes,
|
|
132
|
-
skipIfNoNewTranscripts,
|
|
126
|
+
minIntervalMinutes: cfg.minIntervalMinutes,
|
|
127
|
+
skipIfNoNewTranscripts: cfg.skipIfNoNewTranscripts,
|
|
133
128
|
decision: cadenceDecision.reason
|
|
134
129
|
},
|
|
135
130
|
generatedRecommendations: recommendations
|
|
@@ -141,6 +136,46 @@ export const improvementModule = {
|
|
|
141
136
|
return { ok: false, code: "ingest-failed", message: msg };
|
|
142
137
|
}
|
|
143
138
|
}
|
|
139
|
+
if (command.name === "transcript-automation-status") {
|
|
140
|
+
const syncArgs = {
|
|
141
|
+
sourcePath: typeof args.sourcePath === "string" ? args.sourcePath : undefined,
|
|
142
|
+
archivePath: typeof args.archivePath === "string" ? args.archivePath : undefined
|
|
143
|
+
};
|
|
144
|
+
const state = await loadImprovementState(ctx.workspacePath);
|
|
145
|
+
const cfg = resolveImprovementTranscriptConfig(ctx, syncArgs);
|
|
146
|
+
return {
|
|
147
|
+
ok: true,
|
|
148
|
+
code: "transcript-automation-status",
|
|
149
|
+
message: "Transcript automation status",
|
|
150
|
+
data: {
|
|
151
|
+
schemaVersion: 1,
|
|
152
|
+
lastSyncRunAt: state.lastSyncRunAt,
|
|
153
|
+
lastIngestRunAt: state.lastIngestRunAt,
|
|
154
|
+
cadence: {
|
|
155
|
+
minIntervalMinutes: cfg.minIntervalMinutes,
|
|
156
|
+
skipIfNoNewTranscripts: cfg.skipIfNoNewTranscripts,
|
|
157
|
+
maxRecommendationCandidatesPerRun: getMaxRecommendationCandidatesPerRun(ctx)
|
|
158
|
+
},
|
|
159
|
+
transcripts: {
|
|
160
|
+
sourcePath: cfg.sourcePath || null,
|
|
161
|
+
archivePath: cfg.archivePath,
|
|
162
|
+
discoveryPaths: cfg.discoveryPaths,
|
|
163
|
+
budgets: {
|
|
164
|
+
maxFilesPerSync: cfg.maxFilesPerSync,
|
|
165
|
+
maxBytesPerFile: cfg.maxBytesPerFile,
|
|
166
|
+
maxTotalScanBytes: cfg.maxTotalScanBytes
|
|
167
|
+
}
|
|
168
|
+
},
|
|
169
|
+
retryQueue: {
|
|
170
|
+
pending: state.transcriptRetryQueue?.length ?? 0,
|
|
171
|
+
entries: state.transcriptRetryQueue ?? []
|
|
172
|
+
},
|
|
173
|
+
policySession: {
|
|
174
|
+
sessionId: resolveSessionId(process.env)
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
}
|
|
144
179
|
if (command.name === "query-lineage") {
|
|
145
180
|
const taskId = typeof args.taskId === "string" ? args.taskId.trim() : "";
|
|
146
181
|
if (!taskId) {
|
|
@@ -3,6 +3,7 @@ import path from "node:path";
|
|
|
3
3
|
import { execFileSync } from "node:child_process";
|
|
4
4
|
import crypto from "node:crypto";
|
|
5
5
|
import { computeHeuristicConfidence, shouldAdmitRecommendation } from "./confidence.js";
|
|
6
|
+
import { redactTranscriptSnippet } from "./transcript-redaction.js";
|
|
6
7
|
function sha256Hex(s) {
|
|
7
8
|
return crypto.createHash("sha256").update(s, "utf8").digest("hex");
|
|
8
9
|
}
|
|
@@ -80,7 +81,7 @@ export async function ingestAgentTranscripts(workspacePath, transcriptsRootRel,
|
|
|
80
81
|
evidenceKind: "transcript",
|
|
81
82
|
evidenceKey,
|
|
82
83
|
title: `Reduce friction hinted in transcript (${path.basename(rel)})`,
|
|
83
|
-
provenanceRefs: { transcriptPath: rel, sampleLine },
|
|
84
|
+
provenanceRefs: { transcriptPath: rel, sampleLine: redactTranscriptSnippet(sampleLine) },
|
|
84
85
|
signals,
|
|
85
86
|
confidence
|
|
86
87
|
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Redact transcript-derived strings before they are persisted in task metadata or lineage.
|
|
3
|
+
*/
|
|
4
|
+
export function redactTranscriptSnippet(text, maxLen = 160) {
|
|
5
|
+
let s = text.length > maxLen ? `${text.slice(0, maxLen)}…` : text;
|
|
6
|
+
s = s.replace(/\b(sk|pk|api|Bearer)[-_]?[a-zA-Z0-9]{12,}\b/gi, "[redacted-token]");
|
|
7
|
+
s = s.replace(/\b[A-Fa-f0-9]{32,}\b/g, "[redacted-hex]");
|
|
8
|
+
s = s.replace(/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/g, "[redacted-email]");
|
|
9
|
+
return s;
|
|
10
|
+
}
|
|
@@ -1,24 +1,60 @@
|
|
|
1
1
|
import type { ModuleLifecycleContext } from "../../contracts/module-contract.js";
|
|
2
|
+
import type { ImprovementStateDocument } from "./improvement-state.js";
|
|
2
3
|
export type TranscriptSyncArgs = {
|
|
3
4
|
sourcePath?: string;
|
|
4
5
|
archivePath?: string;
|
|
5
6
|
};
|
|
7
|
+
export type TranscriptSkipReason = "skipped-unchanged-hash" | "skipped-archive-conflict" | "skipped-file-too-large" | "skipped-budget-max-files" | "skipped-budget-total-bytes" | "skipped-read-error";
|
|
6
8
|
export type TranscriptSyncResult = {
|
|
9
|
+
runId: string;
|
|
7
10
|
sourcePath: string;
|
|
8
11
|
archivePath: string;
|
|
12
|
+
discoveredFrom: string;
|
|
13
|
+
discoveryCandidatesTried: string[];
|
|
9
14
|
scanned: number;
|
|
10
15
|
copied: number;
|
|
11
16
|
skippedExisting: number;
|
|
12
17
|
skippedConflict: number;
|
|
18
|
+
skippedBudget: number;
|
|
19
|
+
skippedLargeFile: number;
|
|
13
20
|
errors: Array<{
|
|
14
21
|
file: string;
|
|
15
22
|
code: string;
|
|
16
23
|
message: string;
|
|
17
24
|
}>;
|
|
18
25
|
copiedFiles: string[];
|
|
26
|
+
skipReasons: Record<string, TranscriptSkipReason>;
|
|
27
|
+
budget: {
|
|
28
|
+
maxFilesPerSync: number;
|
|
29
|
+
maxBytesPerFile: number;
|
|
30
|
+
maxTotalScanBytes: number;
|
|
31
|
+
scanBytesUsed: number;
|
|
32
|
+
};
|
|
33
|
+
retryQueue: {
|
|
34
|
+
pending: number;
|
|
35
|
+
processedRetries: number;
|
|
36
|
+
droppedPermanentFailures: number;
|
|
37
|
+
};
|
|
19
38
|
};
|
|
20
|
-
|
|
39
|
+
type ImprovementTranscriptConfig = {
|
|
40
|
+
sourcePath: string;
|
|
41
|
+
archivePath: string;
|
|
42
|
+
minIntervalMinutes: number;
|
|
43
|
+
skipIfNoNewTranscripts: boolean;
|
|
44
|
+
maxFilesPerSync: number;
|
|
45
|
+
maxBytesPerFile: number;
|
|
46
|
+
maxTotalScanBytes: number;
|
|
47
|
+
discoveryPaths: string[];
|
|
48
|
+
};
|
|
49
|
+
export declare function resolveImprovementTranscriptConfig(ctx: ModuleLifecycleContext, args: TranscriptSyncArgs): ImprovementTranscriptConfig;
|
|
50
|
+
export declare function resolveTranscriptSourceRoot(workspacePath: string, cfg: ImprovementTranscriptConfig, args: TranscriptSyncArgs): Promise<{
|
|
51
|
+
root: string;
|
|
52
|
+
discoveredFrom: string;
|
|
53
|
+
tried: string[];
|
|
54
|
+
}>;
|
|
55
|
+
export declare function runSyncTranscripts(ctx: ModuleLifecycleContext, args: TranscriptSyncArgs, state: ImprovementStateDocument, now?: Date): Promise<TranscriptSyncResult>;
|
|
21
56
|
export declare function resolveCadenceDecision(now: Date, previousRunAtIso: string | null, minIntervalMinutes: number, copiedCount: number, skipIfNoNewTranscripts: boolean): {
|
|
22
57
|
shouldRunGenerate: boolean;
|
|
23
58
|
reason: string;
|
|
24
59
|
};
|
|
60
|
+
export {};
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { createHash } from "node:crypto";
|
|
4
|
-
|
|
3
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
4
|
+
const DEFAULT_DISCOVERY_PATHS = [".cursor/agent-transcripts", ".vscode/agent-transcripts"];
|
|
5
|
+
const MAX_RETRY_ATTEMPTS = 5;
|
|
6
|
+
const INITIAL_BACKOFF_MS = 60_000;
|
|
7
|
+
export function resolveImprovementTranscriptConfig(ctx, args) {
|
|
5
8
|
const improvement = ctx.effectiveConfig?.improvement && typeof ctx.effectiveConfig.improvement === "object"
|
|
6
9
|
? ctx.effectiveConfig.improvement
|
|
7
10
|
: {};
|
|
@@ -19,13 +22,63 @@ function resolveImprovementTranscriptConfig(ctx, args) {
|
|
|
19
22
|
? cadence.minIntervalMinutes
|
|
20
23
|
: 15;
|
|
21
24
|
const skipIfNoNewCfg = typeof cadence.skipIfNoNewTranscripts === "boolean" ? cadence.skipIfNoNewTranscripts : true;
|
|
25
|
+
const maxFilesCfg = typeof transcripts.maxFilesPerSync === "number" && Number.isFinite(transcripts.maxFilesPerSync)
|
|
26
|
+
? Math.max(1, Math.floor(transcripts.maxFilesPerSync))
|
|
27
|
+
: 5000;
|
|
28
|
+
const maxBytesFileCfg = typeof transcripts.maxBytesPerFile === "number" && Number.isFinite(transcripts.maxBytesPerFile)
|
|
29
|
+
? Math.max(1024, Math.floor(transcripts.maxBytesPerFile))
|
|
30
|
+
: 50_000_000;
|
|
31
|
+
const maxTotalScanCfg = typeof transcripts.maxTotalScanBytes === "number" && Number.isFinite(transcripts.maxTotalScanBytes)
|
|
32
|
+
? Math.max(1024, Math.floor(transcripts.maxTotalScanBytes))
|
|
33
|
+
: 500_000_000;
|
|
34
|
+
let discoveryPaths = [];
|
|
35
|
+
if (Array.isArray(transcripts.discoveryPaths)) {
|
|
36
|
+
discoveryPaths = transcripts.discoveryPaths.filter((x) => typeof x === "string" && x.trim().length > 0);
|
|
37
|
+
}
|
|
38
|
+
if (discoveryPaths.length === 0) {
|
|
39
|
+
discoveryPaths = [...DEFAULT_DISCOVERY_PATHS];
|
|
40
|
+
}
|
|
22
41
|
return {
|
|
23
|
-
sourcePath: sourcePathArg || sourcePathCfg || "
|
|
42
|
+
sourcePath: sourcePathArg || sourcePathCfg || "",
|
|
24
43
|
archivePath: archivePathArg || archivePathCfg || "agent-transcripts",
|
|
25
44
|
minIntervalMinutes: Math.max(1, Math.floor(minIntervalCfg)),
|
|
26
|
-
skipIfNoNewTranscripts: skipIfNoNewCfg
|
|
45
|
+
skipIfNoNewTranscripts: skipIfNoNewCfg,
|
|
46
|
+
maxFilesPerSync: maxFilesCfg,
|
|
47
|
+
maxBytesPerFile: maxBytesFileCfg,
|
|
48
|
+
maxTotalScanBytes: maxTotalScanCfg,
|
|
49
|
+
discoveryPaths
|
|
27
50
|
};
|
|
28
51
|
}
|
|
52
|
+
async function pathExists(abs) {
|
|
53
|
+
try {
|
|
54
|
+
await fs.access(abs);
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
export async function resolveTranscriptSourceRoot(workspacePath, cfg, args) {
|
|
62
|
+
const sourcePathArg = typeof args.sourcePath === "string" ? args.sourcePath.trim() : "";
|
|
63
|
+
if (sourcePathArg) {
|
|
64
|
+
const root = path.resolve(workspacePath, sourcePathArg);
|
|
65
|
+
return { root, discoveredFrom: sourcePathArg, tried: [sourcePathArg] };
|
|
66
|
+
}
|
|
67
|
+
if (cfg.sourcePath) {
|
|
68
|
+
const root = path.resolve(workspacePath, cfg.sourcePath);
|
|
69
|
+
return { root, discoveredFrom: cfg.sourcePath, tried: [cfg.sourcePath] };
|
|
70
|
+
}
|
|
71
|
+
const tried = [];
|
|
72
|
+
for (const rel of cfg.discoveryPaths) {
|
|
73
|
+
tried.push(rel);
|
|
74
|
+
const abs = path.resolve(workspacePath, rel);
|
|
75
|
+
if (await pathExists(abs)) {
|
|
76
|
+
return { root: abs, discoveredFrom: rel, tried };
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
const fallback = path.resolve(workspacePath, ".cursor/agent-transcripts");
|
|
80
|
+
return { root: fallback, discoveredFrom: ".cursor/agent-transcripts (fallback)", tried };
|
|
81
|
+
}
|
|
29
82
|
async function listJsonlRelativePaths(root) {
|
|
30
83
|
const out = [];
|
|
31
84
|
async function walk(cur) {
|
|
@@ -43,23 +96,49 @@ async function listJsonlRelativePaths(root) {
|
|
|
43
96
|
await walk(root);
|
|
44
97
|
return out.sort((a, b) => a.localeCompare(b));
|
|
45
98
|
}
|
|
99
|
+
async function statSize(filePath) {
|
|
100
|
+
try {
|
|
101
|
+
const st = await fs.stat(filePath);
|
|
102
|
+
return st.size;
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
46
108
|
async function fileSha256(filePath) {
|
|
47
109
|
const buf = await fs.readFile(filePath);
|
|
48
110
|
return createHash("sha256").update(buf).digest("hex");
|
|
49
111
|
}
|
|
50
|
-
|
|
112
|
+
function nextBackoffMs(attempts) {
|
|
113
|
+
return INITIAL_BACKOFF_MS * 2 ** Math.max(0, attempts - 1);
|
|
114
|
+
}
|
|
115
|
+
export async function runSyncTranscripts(ctx, args, state, now = new Date()) {
|
|
51
116
|
const cfg = resolveImprovementTranscriptConfig(ctx, args);
|
|
52
|
-
const
|
|
117
|
+
const runId = randomUUID();
|
|
118
|
+
const { root: sourceRoot, discoveredFrom, tried: discoveryCandidatesTried } = await resolveTranscriptSourceRoot(ctx.workspacePath, cfg, args);
|
|
53
119
|
const archiveRoot = path.resolve(ctx.workspacePath, cfg.archivePath);
|
|
54
120
|
const result = {
|
|
121
|
+
runId,
|
|
55
122
|
sourcePath: path.relative(ctx.workspacePath, sourceRoot) || ".",
|
|
56
123
|
archivePath: path.relative(ctx.workspacePath, archiveRoot) || ".",
|
|
124
|
+
discoveredFrom,
|
|
125
|
+
discoveryCandidatesTried,
|
|
57
126
|
scanned: 0,
|
|
58
127
|
copied: 0,
|
|
59
128
|
skippedExisting: 0,
|
|
60
129
|
skippedConflict: 0,
|
|
130
|
+
skippedBudget: 0,
|
|
131
|
+
skippedLargeFile: 0,
|
|
61
132
|
errors: [],
|
|
62
|
-
copiedFiles: []
|
|
133
|
+
copiedFiles: [],
|
|
134
|
+
skipReasons: {},
|
|
135
|
+
budget: {
|
|
136
|
+
maxFilesPerSync: cfg.maxFilesPerSync,
|
|
137
|
+
maxBytesPerFile: cfg.maxBytesPerFile,
|
|
138
|
+
maxTotalScanBytes: cfg.maxTotalScanBytes,
|
|
139
|
+
scanBytesUsed: 0
|
|
140
|
+
},
|
|
141
|
+
retryQueue: { pending: 0, processedRetries: 0, droppedPermanentFailures: 0 }
|
|
63
142
|
};
|
|
64
143
|
let files = [];
|
|
65
144
|
try {
|
|
@@ -70,16 +149,113 @@ export async function runSyncTranscripts(ctx, args) {
|
|
|
70
149
|
result.errors.push({
|
|
71
150
|
file: result.sourcePath,
|
|
72
151
|
code: "source-read-error",
|
|
73
|
-
message: msg
|
|
152
|
+
message: `${msg} (discovery tried: ${discoveryCandidatesTried.join(", ")})`
|
|
74
153
|
});
|
|
75
154
|
return result;
|
|
76
155
|
}
|
|
77
|
-
result.scanned = files.length;
|
|
78
156
|
await fs.mkdir(archiveRoot, { recursive: true });
|
|
157
|
+
const queue = state.transcriptRetryQueue ?? [];
|
|
158
|
+
const remainingRetries = [];
|
|
159
|
+
let scanBytes = 0;
|
|
160
|
+
for (const entry of queue) {
|
|
161
|
+
const due = new Date(entry.nextRetryAt).getTime() <= now.getTime();
|
|
162
|
+
if (!due) {
|
|
163
|
+
remainingRetries.push(entry);
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
if (entry.attempts >= MAX_RETRY_ATTEMPTS) {
|
|
167
|
+
result.retryQueue.droppedPermanentFailures += 1;
|
|
168
|
+
result.errors.push({
|
|
169
|
+
file: entry.relativePath,
|
|
170
|
+
code: "retry-exhausted",
|
|
171
|
+
message: entry.lastErrorMessage
|
|
172
|
+
});
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
const src = path.join(sourceRoot, entry.relativePath);
|
|
176
|
+
const dst = path.join(archiveRoot, entry.relativePath);
|
|
177
|
+
try {
|
|
178
|
+
await fs.mkdir(path.dirname(dst), { recursive: true });
|
|
179
|
+
const sz = await statSize(src);
|
|
180
|
+
if (sz !== null && sz > cfg.maxBytesPerFile) {
|
|
181
|
+
remainingRetries.push({
|
|
182
|
+
...entry,
|
|
183
|
+
attempts: entry.attempts + 1,
|
|
184
|
+
lastErrorCode: "file-too-large",
|
|
185
|
+
lastErrorMessage: `size ${sz} exceeds maxBytesPerFile`,
|
|
186
|
+
nextRetryAt: new Date(now.getTime() + nextBackoffMs(entry.attempts + 1)).toISOString()
|
|
187
|
+
});
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
if (sz !== null) {
|
|
191
|
+
scanBytes += sz;
|
|
192
|
+
}
|
|
193
|
+
const srcHash = await fileSha256(src);
|
|
194
|
+
let dstHash = null;
|
|
195
|
+
try {
|
|
196
|
+
dstHash = await fileSha256(dst);
|
|
197
|
+
}
|
|
198
|
+
catch (error) {
|
|
199
|
+
if (error.code !== "ENOENT") {
|
|
200
|
+
throw error;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
if (dstHash === srcHash) {
|
|
204
|
+
result.retryQueue.processedRetries += 1;
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
if (dstHash && dstHash !== srcHash) {
|
|
208
|
+
remainingRetries.push({
|
|
209
|
+
...entry,
|
|
210
|
+
attempts: entry.attempts + 1,
|
|
211
|
+
lastErrorCode: "archive-conflict",
|
|
212
|
+
lastErrorMessage: "destination exists with different content",
|
|
213
|
+
nextRetryAt: new Date(now.getTime() + nextBackoffMs(entry.attempts + 1)).toISOString()
|
|
214
|
+
});
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
await fs.copyFile(src, dst);
|
|
218
|
+
result.copied += 1;
|
|
219
|
+
result.copiedFiles.push(entry.relativePath);
|
|
220
|
+
result.retryQueue.processedRetries += 1;
|
|
221
|
+
}
|
|
222
|
+
catch (error) {
|
|
223
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
224
|
+
remainingRetries.push({
|
|
225
|
+
...entry,
|
|
226
|
+
attempts: entry.attempts + 1,
|
|
227
|
+
lastErrorCode: "copy-error",
|
|
228
|
+
lastErrorMessage: msg,
|
|
229
|
+
nextRetryAt: new Date(now.getTime() + nextBackoffMs(entry.attempts + 1)).toISOString()
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
let budgetFiles = 0;
|
|
79
234
|
for (const rel of files) {
|
|
235
|
+
if (budgetFiles >= cfg.maxFilesPerSync) {
|
|
236
|
+
result.skippedBudget += 1;
|
|
237
|
+
result.skipReasons[rel] = "skipped-budget-max-files";
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
80
240
|
const src = path.join(sourceRoot, rel);
|
|
81
241
|
const dst = path.join(archiveRoot, rel);
|
|
242
|
+
budgetFiles += 1;
|
|
243
|
+
result.scanned += 1;
|
|
82
244
|
try {
|
|
245
|
+
const sz = await statSize(src);
|
|
246
|
+
if (sz !== null && sz > cfg.maxBytesPerFile) {
|
|
247
|
+
result.skippedLargeFile += 1;
|
|
248
|
+
result.skipReasons[rel] = "skipped-file-too-large";
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
if (sz !== null && scanBytes + sz > cfg.maxTotalScanBytes) {
|
|
252
|
+
result.skippedBudget += 1;
|
|
253
|
+
result.skipReasons[rel] = "skipped-budget-total-bytes";
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
if (sz !== null) {
|
|
257
|
+
scanBytes += sz;
|
|
258
|
+
}
|
|
83
259
|
await fs.mkdir(path.dirname(dst), { recursive: true });
|
|
84
260
|
const srcHash = await fileSha256(src);
|
|
85
261
|
let dstHash = null;
|
|
@@ -93,10 +269,12 @@ export async function runSyncTranscripts(ctx, args) {
|
|
|
93
269
|
}
|
|
94
270
|
if (dstHash === srcHash) {
|
|
95
271
|
result.skippedExisting += 1;
|
|
272
|
+
result.skipReasons[rel] = "skipped-unchanged-hash";
|
|
96
273
|
continue;
|
|
97
274
|
}
|
|
98
275
|
if (dstHash && dstHash !== srcHash) {
|
|
99
276
|
result.skippedConflict += 1;
|
|
277
|
+
result.skipReasons[rel] = "skipped-archive-conflict";
|
|
100
278
|
continue;
|
|
101
279
|
}
|
|
102
280
|
await fs.copyFile(src, dst);
|
|
@@ -106,8 +284,19 @@ export async function runSyncTranscripts(ctx, args) {
|
|
|
106
284
|
catch (error) {
|
|
107
285
|
const msg = error instanceof Error ? error.message : String(error);
|
|
108
286
|
result.errors.push({ file: rel, code: "copy-error", message: msg });
|
|
287
|
+
remainingRetries.push({
|
|
288
|
+
relativePath: rel,
|
|
289
|
+
attempts: 1,
|
|
290
|
+
lastErrorCode: "copy-error",
|
|
291
|
+
lastErrorMessage: msg,
|
|
292
|
+
nextRetryAt: new Date(now.getTime() + INITIAL_BACKOFF_MS).toISOString()
|
|
293
|
+
});
|
|
294
|
+
result.skipReasons[rel] = "skipped-read-error";
|
|
109
295
|
}
|
|
110
296
|
}
|
|
297
|
+
result.budget.scanBytesUsed = scanBytes;
|
|
298
|
+
state.transcriptRetryQueue = remainingRetries;
|
|
299
|
+
result.retryQueue.pending = remainingRetries.length;
|
|
111
300
|
result.copiedFiles.sort((a, b) => a.localeCompare(b));
|
|
112
301
|
return result;
|
|
113
302
|
}
|
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;
|
|
@@ -1,16 +1,11 @@
|
|
|
1
|
-
import
|
|
2
|
-
import fs from "node:fs/promises";
|
|
1
|
+
import { maybeSpawnTranscriptHookAfterCompletion } from "../../core/transcript-completion-hook.js";
|
|
3
2
|
import { TaskStore } from "./store.js";
|
|
4
3
|
import { TransitionService } from "./service.js";
|
|
5
4
|
import { TaskEngineError } from "./transitions.js";
|
|
6
|
-
import { generateTasksMd, syncTaskHeadingsInMarkdown } from "./generator.js";
|
|
7
|
-
import { importTasksFromMarkdown } from "./importer.js";
|
|
8
5
|
import { getNextActions } from "./suggestions.js";
|
|
9
6
|
export { TaskStore } from "./store.js";
|
|
10
7
|
export { TransitionService } from "./service.js";
|
|
11
8
|
export { TaskEngineError, TransitionValidator, isTransitionAllowed, getTransitionAction, resolveTargetState, getAllowedTransitionsFrom, stateValidityGuard, dependencyCheckGuard } from "./transitions.js";
|
|
12
|
-
export { generateTasksMd, syncTaskHeadingsInMarkdown } from "./generator.js";
|
|
13
|
-
export { importTasksFromMarkdown } from "./importer.js";
|
|
14
9
|
export { getNextActions } from "./suggestions.js";
|
|
15
10
|
function taskStorePath(ctx) {
|
|
16
11
|
const tasks = ctx.effectiveConfig?.tasks;
|
|
@@ -61,16 +56,6 @@ export const taskEngineModule = {
|
|
|
61
56
|
file: "get-ready-queue.md",
|
|
62
57
|
description: "Get ready tasks sorted by priority."
|
|
63
58
|
},
|
|
64
|
-
{
|
|
65
|
-
name: "import-tasks",
|
|
66
|
-
file: "import-tasks.md",
|
|
67
|
-
description: "One-time import from TASKS.md into engine state."
|
|
68
|
-
},
|
|
69
|
-
{
|
|
70
|
-
name: "generate-tasks-md",
|
|
71
|
-
file: "generate-tasks-md.md",
|
|
72
|
-
description: "Generate read-only TASKS.md from engine state."
|
|
73
|
-
},
|
|
74
59
|
{
|
|
75
60
|
name: "get-next-actions",
|
|
76
61
|
file: "get-next-actions.md",
|
|
@@ -113,6 +98,9 @@ export const taskEngineModule = {
|
|
|
113
98
|
try {
|
|
114
99
|
const service = new TransitionService(store);
|
|
115
100
|
const result = await service.runTransition({ taskId, action, actor });
|
|
101
|
+
if (result.evidence.toState === "completed") {
|
|
102
|
+
maybeSpawnTranscriptHookAfterCompletion(ctx.workspacePath, (ctx.effectiveConfig ?? {}));
|
|
103
|
+
}
|
|
116
104
|
return {
|
|
117
105
|
ok: true,
|
|
118
106
|
code: "transition-applied",
|
|
@@ -190,68 +178,6 @@ export const taskEngineModule = {
|
|
|
190
178
|
data: { tasks: ready, count: ready.length }
|
|
191
179
|
};
|
|
192
180
|
}
|
|
193
|
-
if (command.name === "import-tasks") {
|
|
194
|
-
const sourcePath = typeof args.sourcePath === "string"
|
|
195
|
-
? path.resolve(ctx.workspacePath, args.sourcePath)
|
|
196
|
-
: path.resolve(ctx.workspacePath, "docs/maintainers/TASKS.md");
|
|
197
|
-
try {
|
|
198
|
-
const result = await importTasksFromMarkdown(sourcePath);
|
|
199
|
-
store.replaceAllTasks(result.tasks);
|
|
200
|
-
await store.save();
|
|
201
|
-
return {
|
|
202
|
-
ok: true,
|
|
203
|
-
code: "tasks-imported",
|
|
204
|
-
message: `Imported ${result.imported} tasks (${result.skipped} skipped)`,
|
|
205
|
-
data: {
|
|
206
|
-
imported: result.imported,
|
|
207
|
-
skipped: result.skipped,
|
|
208
|
-
errors: result.errors
|
|
209
|
-
}
|
|
210
|
-
};
|
|
211
|
-
}
|
|
212
|
-
catch (err) {
|
|
213
|
-
if (err instanceof TaskEngineError) {
|
|
214
|
-
return { ok: false, code: err.code, message: err.message };
|
|
215
|
-
}
|
|
216
|
-
return {
|
|
217
|
-
ok: false,
|
|
218
|
-
code: "import-parse-error",
|
|
219
|
-
message: err.message
|
|
220
|
-
};
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
if (command.name === "generate-tasks-md") {
|
|
224
|
-
const outputPath = typeof args.outputPath === "string"
|
|
225
|
-
? path.resolve(ctx.workspacePath, args.outputPath)
|
|
226
|
-
: path.resolve(ctx.workspacePath, "docs/maintainers/TASKS.md");
|
|
227
|
-
const tasks = store.getAllTasks();
|
|
228
|
-
const fallbackMarkdown = generateTasksMd(tasks);
|
|
229
|
-
let markdown = fallbackMarkdown;
|
|
230
|
-
const preserveStructure = args.preserveStructure !== false;
|
|
231
|
-
try {
|
|
232
|
-
if (preserveStructure) {
|
|
233
|
-
const existing = await fs.readFile(outputPath, "utf8").catch(() => undefined);
|
|
234
|
-
if (typeof existing === "string" && existing.length > 0) {
|
|
235
|
-
markdown = syncTaskHeadingsInMarkdown(existing, tasks);
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
await fs.mkdir(path.dirname(outputPath), { recursive: true });
|
|
239
|
-
await fs.writeFile(outputPath, markdown, "utf8");
|
|
240
|
-
}
|
|
241
|
-
catch (err) {
|
|
242
|
-
return {
|
|
243
|
-
ok: false,
|
|
244
|
-
code: "storage-write-error",
|
|
245
|
-
message: `Failed to write TASKS.md: ${err.message}`
|
|
246
|
-
};
|
|
247
|
-
}
|
|
248
|
-
return {
|
|
249
|
-
ok: true,
|
|
250
|
-
code: "tasks-md-generated",
|
|
251
|
-
message: `Generated TASKS.md with ${tasks.length} tasks`,
|
|
252
|
-
data: { outputPath, taskCount: tasks.length }
|
|
253
|
-
};
|
|
254
|
-
}
|
|
255
181
|
if (command.name === "get-next-actions") {
|
|
256
182
|
const tasks = store.getAllTasks();
|
|
257
183
|
const suggestion = getNextActions(tasks);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@workflow-cannon/workspace-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"packageManager": "pnpm@10.0.0",
|
|
6
6
|
"license": "MIT",
|
|
@@ -31,7 +31,10 @@
|
|
|
31
31
|
"generate-runtime-diagnostics": "node scripts/generate-runtime-diagnostics.mjs",
|
|
32
32
|
"prune-evidence": "node scripts/prune-evidence.mjs",
|
|
33
33
|
"phase4-gates": "pnpm run check-compatibility && pnpm run check-planning-consistency && pnpm run check-release-channel",
|
|
34
|
-
"phase5-gates": "pnpm run phase4-gates && pnpm run test"
|
|
34
|
+
"phase5-gates": "pnpm run phase4-gates && pnpm run test",
|
|
35
|
+
"pre-release-transcript-hook": "pnpm run build && node scripts/pre-release-transcript-hook.mjs",
|
|
36
|
+
"transcript:sync": "node scripts/run-transcript-cli.mjs sync-transcripts",
|
|
37
|
+
"transcript:ingest": "node scripts/run-transcript-cli.mjs ingest-transcripts"
|
|
35
38
|
},
|
|
36
39
|
"devDependencies": {
|
|
37
40
|
"@types/node": "^25.5.0",
|