@workflow-cannon/workspace-kit 0.5.0 → 0.7.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 +2 -1
- package/dist/core/config-metadata.js +109 -1
- package/dist/core/policy.d.ts +1 -1
- package/dist/core/policy.js +2 -1
- package/dist/core/workspace-kit-config.js +21 -2
- package/dist/modules/improvement/generate-recommendations-runtime.js +14 -3
- package/dist/modules/improvement/improvement-state.d.ts +2 -0
- package/dist/modules/improvement/improvement-state.js +3 -1
- package/dist/modules/improvement/index.js +88 -1
- package/dist/modules/improvement/transcript-sync-runtime.d.ts +24 -0
- package/dist/modules/improvement/transcript-sync-runtime.js +131 -0
- package/dist/modules/index.d.ts +1 -1
- package/dist/modules/index.js +1 -1
- package/dist/modules/task-engine/generator.d.ts +1 -0
- package/dist/modules/task-engine/generator.js +17 -0
- package/dist/modules/task-engine/importer.js +8 -2
- package/dist/modules/task-engine/index.d.ts +1 -1
- package/dist/modules/task-engine/index.js +11 -3
- package/package.json +9 -2
package/README.md
CHANGED
|
@@ -63,7 +63,8 @@ This keeps automation adaptive without sacrificing safety, governance, or develo
|
|
|
63
63
|
|
|
64
64
|
- **Phase 0** and **Phase 1** (task engine, `v0.3.0`) are complete.
|
|
65
65
|
- **Phase 2** (layered config, policy gates, cutover docs, `v0.4.0`) is complete in-repo; see `docs/maintainers/TASKS.md` and `docs/maintainers/ROADMAP.md`.
|
|
66
|
-
- **Phase 2b** (policy + config UX, `v0.4.1`) and **Phase 3** (enhancement loop MVP, `v0.5.0`) are complete in-repo: evidence-driven **improvement** tasks, **`approvals`** (`review-item`), heuristic confidence, and append-only lineage.
|
|
66
|
+
- **Phase 2b** (policy + config UX, `v0.4.1`) and **Phase 3** (enhancement loop MVP, `v0.5.0`) are complete in-repo: evidence-driven **improvement** tasks, **`approvals`** (`review-item`), heuristic confidence, and append-only lineage.
|
|
67
|
+
- **Phase 4** (`v0.6.0`) is complete in-repo: compatibility matrix/gates, diagnostics/SLO baseline evidence, release-channel mapping, and planning-doc consistency checks.
|
|
67
68
|
|
|
68
69
|
## Goals
|
|
69
70
|
|
|
@@ -28,6 +28,58 @@ const REGISTRY = {
|
|
|
28
28
|
requiresApproval: true,
|
|
29
29
|
exposure: "maintainer",
|
|
30
30
|
writableLayers: ["project"]
|
|
31
|
+
},
|
|
32
|
+
"improvement.transcripts.sourcePath": {
|
|
33
|
+
key: "improvement.transcripts.sourcePath",
|
|
34
|
+
type: "string",
|
|
35
|
+
description: "Relative path to transcript JSONL source files for sync operations.",
|
|
36
|
+
default: ".cursor/agent-transcripts",
|
|
37
|
+
domainScope: "project",
|
|
38
|
+
owningModule: "improvement",
|
|
39
|
+
sensitive: false,
|
|
40
|
+
requiresRestart: false,
|
|
41
|
+
requiresApproval: false,
|
|
42
|
+
exposure: "public",
|
|
43
|
+
writableLayers: ["project", "user"]
|
|
44
|
+
},
|
|
45
|
+
"improvement.transcripts.archivePath": {
|
|
46
|
+
key: "improvement.transcripts.archivePath",
|
|
47
|
+
type: "string",
|
|
48
|
+
description: "Relative local archive path where synced transcript JSONL files are copied.",
|
|
49
|
+
default: "agent-transcripts",
|
|
50
|
+
domainScope: "project",
|
|
51
|
+
owningModule: "improvement",
|
|
52
|
+
sensitive: false,
|
|
53
|
+
requiresRestart: false,
|
|
54
|
+
requiresApproval: false,
|
|
55
|
+
exposure: "public",
|
|
56
|
+
writableLayers: ["project", "user"]
|
|
57
|
+
},
|
|
58
|
+
"improvement.cadence.minIntervalMinutes": {
|
|
59
|
+
key: "improvement.cadence.minIntervalMinutes",
|
|
60
|
+
type: "number",
|
|
61
|
+
description: "Minimum minutes between one-shot ingest recommendation generation runs.",
|
|
62
|
+
default: 15,
|
|
63
|
+
domainScope: "project",
|
|
64
|
+
owningModule: "improvement",
|
|
65
|
+
sensitive: false,
|
|
66
|
+
requiresRestart: false,
|
|
67
|
+
requiresApproval: false,
|
|
68
|
+
exposure: "maintainer",
|
|
69
|
+
writableLayers: ["project", "user"]
|
|
70
|
+
},
|
|
71
|
+
"improvement.cadence.skipIfNoNewTranscripts": {
|
|
72
|
+
key: "improvement.cadence.skipIfNoNewTranscripts",
|
|
73
|
+
type: "boolean",
|
|
74
|
+
description: "Skip recommendation generation when transcript sync copies no new files.",
|
|
75
|
+
default: true,
|
|
76
|
+
domainScope: "project",
|
|
77
|
+
owningModule: "improvement",
|
|
78
|
+
sensitive: false,
|
|
79
|
+
requiresRestart: false,
|
|
80
|
+
requiresApproval: false,
|
|
81
|
+
exposure: "maintainer",
|
|
82
|
+
writableLayers: ["project", "user"]
|
|
31
83
|
}
|
|
32
84
|
};
|
|
33
85
|
export function getConfigKeyMetadata(key) {
|
|
@@ -102,7 +154,15 @@ function deepEqualLoose(a, b) {
|
|
|
102
154
|
* Validate top-level shape of persisted kit config files (strict unknown-key rejection).
|
|
103
155
|
*/
|
|
104
156
|
export function validatePersistedConfigDocument(data, label) {
|
|
105
|
-
const allowed = new Set([
|
|
157
|
+
const allowed = new Set([
|
|
158
|
+
"schemaVersion",
|
|
159
|
+
"core",
|
|
160
|
+
"tasks",
|
|
161
|
+
"documentation",
|
|
162
|
+
"policy",
|
|
163
|
+
"improvement",
|
|
164
|
+
"modules"
|
|
165
|
+
]);
|
|
106
166
|
for (const k of Object.keys(data)) {
|
|
107
167
|
if (!allowed.has(k)) {
|
|
108
168
|
throw new Error(`config-invalid(${label}): unknown top-level key '${k}'`);
|
|
@@ -156,6 +216,54 @@ export function validatePersistedConfigDocument(data, label) {
|
|
|
156
216
|
throw new Error(`config-invalid(${label}): modules must be absent or an empty object`);
|
|
157
217
|
}
|
|
158
218
|
}
|
|
219
|
+
const improvement = data.improvement;
|
|
220
|
+
if (improvement !== undefined) {
|
|
221
|
+
if (typeof improvement !== "object" || improvement === null || Array.isArray(improvement)) {
|
|
222
|
+
throw new Error(`config-invalid(${label}): improvement must be an object`);
|
|
223
|
+
}
|
|
224
|
+
const imp = improvement;
|
|
225
|
+
for (const k of Object.keys(imp)) {
|
|
226
|
+
if (k !== "transcripts" && k !== "cadence") {
|
|
227
|
+
throw new Error(`config-invalid(${label}): unknown improvement.${k}`);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
if (imp.transcripts !== undefined) {
|
|
231
|
+
if (typeof imp.transcripts !== "object" ||
|
|
232
|
+
imp.transcripts === null ||
|
|
233
|
+
Array.isArray(imp.transcripts)) {
|
|
234
|
+
throw new Error(`config-invalid(${label}): improvement.transcripts must be an object`);
|
|
235
|
+
}
|
|
236
|
+
const tr = imp.transcripts;
|
|
237
|
+
for (const k of Object.keys(tr)) {
|
|
238
|
+
if (k !== "sourcePath" && k !== "archivePath") {
|
|
239
|
+
throw new Error(`config-invalid(${label}): unknown improvement.transcripts.${k}`);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
if (tr.sourcePath !== undefined) {
|
|
243
|
+
validateValueForMetadata(REGISTRY["improvement.transcripts.sourcePath"], tr.sourcePath);
|
|
244
|
+
}
|
|
245
|
+
if (tr.archivePath !== undefined) {
|
|
246
|
+
validateValueForMetadata(REGISTRY["improvement.transcripts.archivePath"], tr.archivePath);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
if (imp.cadence !== undefined) {
|
|
250
|
+
if (typeof imp.cadence !== "object" || imp.cadence === null || Array.isArray(imp.cadence)) {
|
|
251
|
+
throw new Error(`config-invalid(${label}): improvement.cadence must be an object`);
|
|
252
|
+
}
|
|
253
|
+
const cd = imp.cadence;
|
|
254
|
+
for (const k of Object.keys(cd)) {
|
|
255
|
+
if (k !== "minIntervalMinutes" && k !== "skipIfNoNewTranscripts") {
|
|
256
|
+
throw new Error(`config-invalid(${label}): unknown improvement.cadence.${k}`);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
if (cd.minIntervalMinutes !== undefined) {
|
|
260
|
+
validateValueForMetadata(REGISTRY["improvement.cadence.minIntervalMinutes"], cd.minIntervalMinutes);
|
|
261
|
+
}
|
|
262
|
+
if (cd.skipIfNoNewTranscripts !== undefined) {
|
|
263
|
+
validateValueForMetadata(REGISTRY["improvement.cadence.skipIfNoNewTranscripts"], cd.skipIfNoNewTranscripts);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
159
267
|
}
|
|
160
268
|
export function getConfigRegistryExport() {
|
|
161
269
|
return REGISTRY;
|
package/dist/core/policy.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export declare const POLICY_TRACE_SCHEMA_VERSION: 1;
|
|
2
|
-
export type PolicyOperationId = "cli.upgrade" | "cli.init" | "cli.config-mutate" | "policy.dynamic-sensitive" | "doc.document-project" | "doc.generate-document" | "tasks.import-tasks" | "tasks.generate-tasks-md" | "tasks.run-transition" | "approvals.review-item" | "improvement.generate-recommendations";
|
|
2
|
+
export type PolicyOperationId = "cli.upgrade" | "cli.init" | "cli.config-mutate" | "policy.dynamic-sensitive" | "doc.document-project" | "doc.generate-document" | "tasks.import-tasks" | "tasks.generate-tasks-md" | "tasks.run-transition" | "approvals.review-item" | "improvement.generate-recommendations" | "improvement.ingest-transcripts";
|
|
3
3
|
export declare function getOperationIdForCommand(commandName: string): PolicyOperationId | undefined;
|
|
4
4
|
export declare function getExtraSensitiveModuleCommandsFromEffective(effective: Record<string, unknown>): string[];
|
|
5
5
|
/** Resolve operation id for tracing, including config-declared sensitive module commands. */
|
package/dist/core/policy.js
CHANGED
|
@@ -9,7 +9,8 @@ const COMMAND_TO_OPERATION = {
|
|
|
9
9
|
"generate-tasks-md": "tasks.generate-tasks-md",
|
|
10
10
|
"run-transition": "tasks.run-transition",
|
|
11
11
|
"review-item": "approvals.review-item",
|
|
12
|
-
"generate-recommendations": "improvement.generate-recommendations"
|
|
12
|
+
"generate-recommendations": "improvement.generate-recommendations",
|
|
13
|
+
"ingest-transcripts": "improvement.ingest-transcripts"
|
|
13
14
|
};
|
|
14
15
|
export function getOperationIdForCommand(commandName) {
|
|
15
16
|
return COMMAND_TO_OPERATION[commandName];
|
|
@@ -13,7 +13,17 @@ export const KIT_CONFIG_DEFAULTS = {
|
|
|
13
13
|
tasks: {
|
|
14
14
|
storeRelativePath: ".workspace-kit/tasks/state.json"
|
|
15
15
|
},
|
|
16
|
-
documentation: {}
|
|
16
|
+
documentation: {},
|
|
17
|
+
improvement: {
|
|
18
|
+
transcripts: {
|
|
19
|
+
sourcePath: ".cursor/agent-transcripts",
|
|
20
|
+
archivePath: "agent-transcripts"
|
|
21
|
+
},
|
|
22
|
+
cadence: {
|
|
23
|
+
minIntervalMinutes: 15,
|
|
24
|
+
skipIfNoNewTranscripts: true
|
|
25
|
+
}
|
|
26
|
+
}
|
|
17
27
|
};
|
|
18
28
|
/**
|
|
19
29
|
* Static module-level defaults keyed by module id (merged in registry startup order).
|
|
@@ -30,7 +40,16 @@ export const MODULE_CONFIG_CONTRIBUTIONS = {
|
|
|
30
40
|
},
|
|
31
41
|
approvals: {},
|
|
32
42
|
planning: {},
|
|
33
|
-
improvement: {
|
|
43
|
+
improvement: {
|
|
44
|
+
transcripts: {
|
|
45
|
+
sourcePath: ".cursor/agent-transcripts",
|
|
46
|
+
archivePath: "agent-transcripts"
|
|
47
|
+
},
|
|
48
|
+
cadence: {
|
|
49
|
+
minIntervalMinutes: 15,
|
|
50
|
+
skipIfNoNewTranscripts: true
|
|
51
|
+
}
|
|
52
|
+
}
|
|
34
53
|
};
|
|
35
54
|
export function deepMerge(target, source) {
|
|
36
55
|
const out = { ...target };
|
|
@@ -19,13 +19,24 @@ function hasEvidenceKey(tasks, key) {
|
|
|
19
19
|
return m.evidenceKey === key;
|
|
20
20
|
});
|
|
21
21
|
}
|
|
22
|
+
function resolveTranscriptArchivePath(ctx, args) {
|
|
23
|
+
if (typeof args.transcriptsRoot === "string" && args.transcriptsRoot.trim().length > 0) {
|
|
24
|
+
return args.transcriptsRoot.trim();
|
|
25
|
+
}
|
|
26
|
+
const improvement = ctx.effectiveConfig?.improvement && typeof ctx.effectiveConfig.improvement === "object"
|
|
27
|
+
? ctx.effectiveConfig.improvement
|
|
28
|
+
: {};
|
|
29
|
+
const transcripts = improvement.transcripts && typeof improvement.transcripts === "object"
|
|
30
|
+
? improvement.transcripts
|
|
31
|
+
: {};
|
|
32
|
+
const archivePath = typeof transcripts.archivePath === "string" ? transcripts.archivePath.trim() : "";
|
|
33
|
+
return archivePath || "agent-transcripts";
|
|
34
|
+
}
|
|
22
35
|
export async function runGenerateRecommendations(ctx, args) {
|
|
23
36
|
const store = new TaskStore(ctx.workspacePath, taskStoreRelativePath(ctx));
|
|
24
37
|
await store.load();
|
|
25
38
|
const state = await loadImprovementState(ctx.workspacePath);
|
|
26
|
-
const transcriptsRoot =
|
|
27
|
-
? args.transcriptsRoot.trim()
|
|
28
|
-
: "agent-transcripts";
|
|
39
|
+
const transcriptsRoot = resolveTranscriptArchivePath(ctx, args);
|
|
29
40
|
const fromTag = typeof args.fromTag === "string" ? args.fromTag.trim() : undefined;
|
|
30
41
|
const toTag = typeof args.toTag === "string" ? args.toTag.trim() : undefined;
|
|
31
42
|
const candidates = [];
|
|
@@ -5,6 +5,8 @@ export type ImprovementStateDocument = {
|
|
|
5
5
|
mutationLineCursor: number;
|
|
6
6
|
transitionLogLengthCursor: number;
|
|
7
7
|
transcriptLineCursors: Record<string, number>;
|
|
8
|
+
lastSyncRunAt: string | null;
|
|
9
|
+
lastIngestRunAt: string | null;
|
|
8
10
|
};
|
|
9
11
|
export declare function emptyImprovementState(): ImprovementStateDocument;
|
|
10
12
|
export declare function loadImprovementState(workspacePath: string): Promise<ImprovementStateDocument>;
|
|
@@ -11,7 +11,9 @@ export function emptyImprovementState() {
|
|
|
11
11
|
policyTraceLineCursor: 0,
|
|
12
12
|
mutationLineCursor: 0,
|
|
13
13
|
transitionLogLengthCursor: 0,
|
|
14
|
-
transcriptLineCursors: {}
|
|
14
|
+
transcriptLineCursors: {},
|
|
15
|
+
lastSyncRunAt: null,
|
|
16
|
+
lastIngestRunAt: null
|
|
15
17
|
};
|
|
16
18
|
}
|
|
17
19
|
export async function loadImprovementState(workspacePath) {
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { queryLineageChain } from "../../core/lineage-store.js";
|
|
2
2
|
import { runGenerateRecommendations } from "./generate-recommendations-runtime.js";
|
|
3
|
+
import { resolveCadenceDecision, runSyncTranscripts } from "./transcript-sync-runtime.js";
|
|
4
|
+
import { loadImprovementState, saveImprovementState } from "./improvement-state.js";
|
|
3
5
|
export const improvementModule = {
|
|
4
6
|
registration: {
|
|
5
7
|
id: "improvement",
|
|
6
|
-
version: "0.
|
|
8
|
+
version: "0.7.0",
|
|
7
9
|
contractVersion: "1",
|
|
8
10
|
capabilities: ["improvement"],
|
|
9
11
|
dependsOn: ["task-engine", "planning"],
|
|
@@ -30,6 +32,16 @@ export const improvementModule = {
|
|
|
30
32
|
name: "query-lineage",
|
|
31
33
|
file: "query-lineage.md",
|
|
32
34
|
description: "Reconstruct lineage chain for a recommendation task id."
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
name: "sync-transcripts",
|
|
38
|
+
file: "sync-transcripts.md",
|
|
39
|
+
description: "Sync local transcript JSONL files into the archive."
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
name: "ingest-transcripts",
|
|
43
|
+
file: "ingest-transcripts.md",
|
|
44
|
+
description: "Run transcript sync and recommendation generation in one flow."
|
|
33
45
|
}
|
|
34
46
|
]
|
|
35
47
|
}
|
|
@@ -54,6 +66,81 @@ export const improvementModule = {
|
|
|
54
66
|
return { ok: false, code: "generate-failed", message: msg };
|
|
55
67
|
}
|
|
56
68
|
}
|
|
69
|
+
if (command.name === "sync-transcripts") {
|
|
70
|
+
const syncArgs = {
|
|
71
|
+
sourcePath: typeof args.sourcePath === "string" ? args.sourcePath : undefined,
|
|
72
|
+
archivePath: typeof args.archivePath === "string" ? args.archivePath : undefined
|
|
73
|
+
};
|
|
74
|
+
try {
|
|
75
|
+
const sync = await runSyncTranscripts(ctx, syncArgs);
|
|
76
|
+
const state = await loadImprovementState(ctx.workspacePath);
|
|
77
|
+
state.lastSyncRunAt = new Date().toISOString();
|
|
78
|
+
await saveImprovementState(ctx.workspacePath, state);
|
|
79
|
+
return {
|
|
80
|
+
ok: true,
|
|
81
|
+
code: "transcripts-synced",
|
|
82
|
+
message: `Copied ${sync.copied} transcript file(s); skipped ${sync.skippedExisting} existing`,
|
|
83
|
+
data: sync
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
catch (e) {
|
|
87
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
88
|
+
return { ok: false, code: "sync-failed", message: msg };
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (command.name === "ingest-transcripts") {
|
|
92
|
+
const syncArgs = {
|
|
93
|
+
sourcePath: typeof args.sourcePath === "string" ? args.sourcePath : undefined,
|
|
94
|
+
archivePath: typeof args.archivePath === "string" ? args.archivePath : undefined
|
|
95
|
+
};
|
|
96
|
+
const now = new Date();
|
|
97
|
+
try {
|
|
98
|
+
const sync = await runSyncTranscripts(ctx, syncArgs);
|
|
99
|
+
const state = await loadImprovementState(ctx.workspacePath);
|
|
100
|
+
const improvement = ctx.effectiveConfig?.improvement && typeof ctx.effectiveConfig.improvement === "object"
|
|
101
|
+
? ctx.effectiveConfig.improvement
|
|
102
|
+
: {};
|
|
103
|
+
const cadence = improvement.cadence && typeof improvement.cadence === "object"
|
|
104
|
+
? improvement.cadence
|
|
105
|
+
: {};
|
|
106
|
+
const minIntervalMinutes = typeof cadence.minIntervalMinutes === "number" && Number.isFinite(cadence.minIntervalMinutes)
|
|
107
|
+
? Math.max(1, Math.floor(cadence.minIntervalMinutes))
|
|
108
|
+
: 15;
|
|
109
|
+
const skipIfNoNewTranscripts = typeof cadence.skipIfNoNewTranscripts === "boolean"
|
|
110
|
+
? cadence.skipIfNoNewTranscripts
|
|
111
|
+
: true;
|
|
112
|
+
const cadenceDecision = resolveCadenceDecision(now, state.lastIngestRunAt, minIntervalMinutes, sync.copied, skipIfNoNewTranscripts);
|
|
113
|
+
state.lastSyncRunAt = now.toISOString();
|
|
114
|
+
const generate = cadenceDecision.shouldRunGenerate || args.forceGenerate === true || args.runGenerate === true;
|
|
115
|
+
let recommendations = null;
|
|
116
|
+
if (generate) {
|
|
117
|
+
recommendations = await runGenerateRecommendations(ctx, {
|
|
118
|
+
transcriptsRoot: sync.archivePath
|
|
119
|
+
});
|
|
120
|
+
state.lastIngestRunAt = now.toISOString();
|
|
121
|
+
}
|
|
122
|
+
await saveImprovementState(ctx.workspacePath, state);
|
|
123
|
+
const status = generate ? "generated" : "skipped";
|
|
124
|
+
return {
|
|
125
|
+
ok: true,
|
|
126
|
+
code: "transcripts-ingested",
|
|
127
|
+
message: `Ingest ${status}; sync copied ${sync.copied} file(s)`,
|
|
128
|
+
data: {
|
|
129
|
+
sync,
|
|
130
|
+
cadence: {
|
|
131
|
+
minIntervalMinutes,
|
|
132
|
+
skipIfNoNewTranscripts,
|
|
133
|
+
decision: cadenceDecision.reason
|
|
134
|
+
},
|
|
135
|
+
generatedRecommendations: recommendations
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
catch (e) {
|
|
140
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
141
|
+
return { ok: false, code: "ingest-failed", message: msg };
|
|
142
|
+
}
|
|
143
|
+
}
|
|
57
144
|
if (command.name === "query-lineage") {
|
|
58
145
|
const taskId = typeof args.taskId === "string" ? args.taskId.trim() : "";
|
|
59
146
|
if (!taskId) {
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { ModuleLifecycleContext } from "../../contracts/module-contract.js";
|
|
2
|
+
export type TranscriptSyncArgs = {
|
|
3
|
+
sourcePath?: string;
|
|
4
|
+
archivePath?: string;
|
|
5
|
+
};
|
|
6
|
+
export type TranscriptSyncResult = {
|
|
7
|
+
sourcePath: string;
|
|
8
|
+
archivePath: string;
|
|
9
|
+
scanned: number;
|
|
10
|
+
copied: number;
|
|
11
|
+
skippedExisting: number;
|
|
12
|
+
skippedConflict: number;
|
|
13
|
+
errors: Array<{
|
|
14
|
+
file: string;
|
|
15
|
+
code: string;
|
|
16
|
+
message: string;
|
|
17
|
+
}>;
|
|
18
|
+
copiedFiles: string[];
|
|
19
|
+
};
|
|
20
|
+
export declare function runSyncTranscripts(ctx: ModuleLifecycleContext, args: TranscriptSyncArgs): Promise<TranscriptSyncResult>;
|
|
21
|
+
export declare function resolveCadenceDecision(now: Date, previousRunAtIso: string | null, minIntervalMinutes: number, copiedCount: number, skipIfNoNewTranscripts: boolean): {
|
|
22
|
+
shouldRunGenerate: boolean;
|
|
23
|
+
reason: string;
|
|
24
|
+
};
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
function resolveImprovementTranscriptConfig(ctx, args) {
|
|
5
|
+
const improvement = ctx.effectiveConfig?.improvement && typeof ctx.effectiveConfig.improvement === "object"
|
|
6
|
+
? ctx.effectiveConfig.improvement
|
|
7
|
+
: {};
|
|
8
|
+
const transcripts = improvement.transcripts && typeof improvement.transcripts === "object"
|
|
9
|
+
? improvement.transcripts
|
|
10
|
+
: {};
|
|
11
|
+
const cadence = improvement.cadence && typeof improvement.cadence === "object"
|
|
12
|
+
? improvement.cadence
|
|
13
|
+
: {};
|
|
14
|
+
const sourcePathArg = typeof args.sourcePath === "string" ? args.sourcePath.trim() : "";
|
|
15
|
+
const archivePathArg = typeof args.archivePath === "string" ? args.archivePath.trim() : "";
|
|
16
|
+
const sourcePathCfg = typeof transcripts.sourcePath === "string" ? transcripts.sourcePath.trim() : "";
|
|
17
|
+
const archivePathCfg = typeof transcripts.archivePath === "string" ? transcripts.archivePath.trim() : "";
|
|
18
|
+
const minIntervalCfg = typeof cadence.minIntervalMinutes === "number" && Number.isFinite(cadence.minIntervalMinutes)
|
|
19
|
+
? cadence.minIntervalMinutes
|
|
20
|
+
: 15;
|
|
21
|
+
const skipIfNoNewCfg = typeof cadence.skipIfNoNewTranscripts === "boolean" ? cadence.skipIfNoNewTranscripts : true;
|
|
22
|
+
return {
|
|
23
|
+
sourcePath: sourcePathArg || sourcePathCfg || ".cursor/agent-transcripts",
|
|
24
|
+
archivePath: archivePathArg || archivePathCfg || "agent-transcripts",
|
|
25
|
+
minIntervalMinutes: Math.max(1, Math.floor(minIntervalCfg)),
|
|
26
|
+
skipIfNoNewTranscripts: skipIfNoNewCfg
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
async function listJsonlRelativePaths(root) {
|
|
30
|
+
const out = [];
|
|
31
|
+
async function walk(cur) {
|
|
32
|
+
const ents = await fs.readdir(cur, { withFileTypes: true });
|
|
33
|
+
for (const ent of ents) {
|
|
34
|
+
const abs = path.join(cur, ent.name);
|
|
35
|
+
if (ent.isDirectory()) {
|
|
36
|
+
await walk(abs);
|
|
37
|
+
}
|
|
38
|
+
else if (ent.isFile() && ent.name.endsWith(".jsonl")) {
|
|
39
|
+
out.push(path.relative(root, abs));
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
await walk(root);
|
|
44
|
+
return out.sort((a, b) => a.localeCompare(b));
|
|
45
|
+
}
|
|
46
|
+
async function fileSha256(filePath) {
|
|
47
|
+
const buf = await fs.readFile(filePath);
|
|
48
|
+
return createHash("sha256").update(buf).digest("hex");
|
|
49
|
+
}
|
|
50
|
+
export async function runSyncTranscripts(ctx, args) {
|
|
51
|
+
const cfg = resolveImprovementTranscriptConfig(ctx, args);
|
|
52
|
+
const sourceRoot = path.resolve(ctx.workspacePath, cfg.sourcePath);
|
|
53
|
+
const archiveRoot = path.resolve(ctx.workspacePath, cfg.archivePath);
|
|
54
|
+
const result = {
|
|
55
|
+
sourcePath: path.relative(ctx.workspacePath, sourceRoot) || ".",
|
|
56
|
+
archivePath: path.relative(ctx.workspacePath, archiveRoot) || ".",
|
|
57
|
+
scanned: 0,
|
|
58
|
+
copied: 0,
|
|
59
|
+
skippedExisting: 0,
|
|
60
|
+
skippedConflict: 0,
|
|
61
|
+
errors: [],
|
|
62
|
+
copiedFiles: []
|
|
63
|
+
};
|
|
64
|
+
let files = [];
|
|
65
|
+
try {
|
|
66
|
+
files = await listJsonlRelativePaths(sourceRoot);
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
70
|
+
result.errors.push({
|
|
71
|
+
file: result.sourcePath,
|
|
72
|
+
code: "source-read-error",
|
|
73
|
+
message: msg
|
|
74
|
+
});
|
|
75
|
+
return result;
|
|
76
|
+
}
|
|
77
|
+
result.scanned = files.length;
|
|
78
|
+
await fs.mkdir(archiveRoot, { recursive: true });
|
|
79
|
+
for (const rel of files) {
|
|
80
|
+
const src = path.join(sourceRoot, rel);
|
|
81
|
+
const dst = path.join(archiveRoot, rel);
|
|
82
|
+
try {
|
|
83
|
+
await fs.mkdir(path.dirname(dst), { recursive: true });
|
|
84
|
+
const srcHash = await fileSha256(src);
|
|
85
|
+
let dstHash = null;
|
|
86
|
+
try {
|
|
87
|
+
dstHash = await fileSha256(dst);
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
if (error.code !== "ENOENT") {
|
|
91
|
+
throw error;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if (dstHash === srcHash) {
|
|
95
|
+
result.skippedExisting += 1;
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (dstHash && dstHash !== srcHash) {
|
|
99
|
+
result.skippedConflict += 1;
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
await fs.copyFile(src, dst);
|
|
103
|
+
result.copied += 1;
|
|
104
|
+
result.copiedFiles.push(rel);
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
108
|
+
result.errors.push({ file: rel, code: "copy-error", message: msg });
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
result.copiedFiles.sort((a, b) => a.localeCompare(b));
|
|
112
|
+
return result;
|
|
113
|
+
}
|
|
114
|
+
export function resolveCadenceDecision(now, previousRunAtIso, minIntervalMinutes, copiedCount, skipIfNoNewTranscripts) {
|
|
115
|
+
if (copiedCount === 0 && skipIfNoNewTranscripts) {
|
|
116
|
+
return { shouldRunGenerate: false, reason: "skipped-no-new-transcripts" };
|
|
117
|
+
}
|
|
118
|
+
if (!previousRunAtIso) {
|
|
119
|
+
return { shouldRunGenerate: true, reason: "run-first-ingest" };
|
|
120
|
+
}
|
|
121
|
+
const prev = new Date(previousRunAtIso);
|
|
122
|
+
if (!Number.isFinite(prev.getTime())) {
|
|
123
|
+
return { shouldRunGenerate: true, reason: "run-invalid-last-ingest-at" };
|
|
124
|
+
}
|
|
125
|
+
const elapsedMs = now.getTime() - prev.getTime();
|
|
126
|
+
const requiredMs = minIntervalMinutes * 60 * 1000;
|
|
127
|
+
if (elapsedMs < requiredMs) {
|
|
128
|
+
return { shouldRunGenerate: false, reason: "skipped-min-interval" };
|
|
129
|
+
}
|
|
130
|
+
return { shouldRunGenerate: true, reason: "run-min-interval-satisfied" };
|
|
131
|
+
}
|
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, generateTasksMd, importTasksFromMarkdown, getNextActions } from "./task-engine/index.js";
|
|
8
|
+
export { taskEngineModule, TaskStore, TransitionService, TaskEngineError, TransitionValidator, isTransitionAllowed, getTransitionAction, resolveTargetState, getAllowedTransitionsFrom, stateValidityGuard, dependencyCheckGuard, generateTasksMd, syncTaskHeadingsInMarkdown, importTasksFromMarkdown, 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, generateTasksMd, importTasksFromMarkdown, getNextActions } from "./task-engine/index.js";
|
|
7
|
+
export { taskEngineModule, TaskStore, TransitionService, TaskEngineError, TransitionValidator, isTransitionAllowed, getTransitionAction, resolveTargetState, getAllowedTransitionsFrom, stateValidityGuard, dependencyCheckGuard, generateTasksMd, syncTaskHeadingsInMarkdown, importTasksFromMarkdown, getNextActions } from "./task-engine/index.js";
|
|
@@ -99,3 +99,20 @@ export function generateTasksMd(tasks) {
|
|
|
99
99
|
}
|
|
100
100
|
return lines.join("\n");
|
|
101
101
|
}
|
|
102
|
+
export function syncTaskHeadingsInMarkdown(markdown, tasks) {
|
|
103
|
+
const byId = new Map(tasks.map((task) => [task.id, task]));
|
|
104
|
+
const lines = markdown.split("\n");
|
|
105
|
+
for (let i = 0; i < lines.length; i++) {
|
|
106
|
+
const line = lines[i];
|
|
107
|
+
const match = line.match(/^###\s+\[[^\]]*\]\s+(T\d+)\s+(.+)$/);
|
|
108
|
+
if (!match)
|
|
109
|
+
continue;
|
|
110
|
+
const id = match[1];
|
|
111
|
+
const task = byId.get(id);
|
|
112
|
+
if (!task)
|
|
113
|
+
continue;
|
|
114
|
+
const marker = STATUS_MARKERS[task.status] ?? "[ ]";
|
|
115
|
+
lines[i] = `### ${marker} ${task.id} ${task.title}`;
|
|
116
|
+
}
|
|
117
|
+
return lines.join("\n");
|
|
118
|
+
}
|
|
@@ -139,8 +139,14 @@ export async function importTasksFromMarkdown(sourcePath) {
|
|
|
139
139
|
}
|
|
140
140
|
if (line.startsWith("### ")) {
|
|
141
141
|
flushTask();
|
|
142
|
-
|
|
143
|
-
|
|
142
|
+
if (parseTaskId(line)) {
|
|
143
|
+
taskStartIdx = i;
|
|
144
|
+
taskLines = [line];
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
taskStartIdx = -1;
|
|
148
|
+
taskLines = [];
|
|
149
|
+
}
|
|
144
150
|
continue;
|
|
145
151
|
}
|
|
146
152
|
if (taskStartIdx !== -1) {
|
|
@@ -3,7 +3,7 @@ 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";
|
|
6
|
+
export { generateTasksMd, syncTaskHeadingsInMarkdown } from "./generator.js";
|
|
7
7
|
export { importTasksFromMarkdown } from "./importer.js";
|
|
8
8
|
export { getNextActions } from "./suggestions.js";
|
|
9
9
|
export declare const taskEngineModule: WorkflowModule;
|
|
@@ -3,13 +3,13 @@ import fs from "node:fs/promises";
|
|
|
3
3
|
import { TaskStore } from "./store.js";
|
|
4
4
|
import { TransitionService } from "./service.js";
|
|
5
5
|
import { TaskEngineError } from "./transitions.js";
|
|
6
|
-
import { generateTasksMd } from "./generator.js";
|
|
6
|
+
import { generateTasksMd, syncTaskHeadingsInMarkdown } from "./generator.js";
|
|
7
7
|
import { importTasksFromMarkdown } from "./importer.js";
|
|
8
8
|
import { getNextActions } from "./suggestions.js";
|
|
9
9
|
export { TaskStore } from "./store.js";
|
|
10
10
|
export { TransitionService } from "./service.js";
|
|
11
11
|
export { TaskEngineError, TransitionValidator, isTransitionAllowed, getTransitionAction, resolveTargetState, getAllowedTransitionsFrom, stateValidityGuard, dependencyCheckGuard } from "./transitions.js";
|
|
12
|
-
export { generateTasksMd } from "./generator.js";
|
|
12
|
+
export { generateTasksMd, syncTaskHeadingsInMarkdown } from "./generator.js";
|
|
13
13
|
export { importTasksFromMarkdown } from "./importer.js";
|
|
14
14
|
export { getNextActions } from "./suggestions.js";
|
|
15
15
|
function taskStorePath(ctx) {
|
|
@@ -225,8 +225,16 @@ export const taskEngineModule = {
|
|
|
225
225
|
? path.resolve(ctx.workspacePath, args.outputPath)
|
|
226
226
|
: path.resolve(ctx.workspacePath, "docs/maintainers/TASKS.md");
|
|
227
227
|
const tasks = store.getAllTasks();
|
|
228
|
-
const
|
|
228
|
+
const fallbackMarkdown = generateTasksMd(tasks);
|
|
229
|
+
let markdown = fallbackMarkdown;
|
|
230
|
+
const preserveStructure = args.preserveStructure !== false;
|
|
229
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
|
+
}
|
|
230
238
|
await fs.mkdir(path.dirname(outputPath), { recursive: true });
|
|
231
239
|
await fs.writeFile(outputPath, markdown, "utf8");
|
|
232
240
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@workflow-cannon/workspace-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"packageManager": "pnpm@10.0.0",
|
|
6
6
|
"license": "MIT",
|
|
@@ -24,7 +24,14 @@
|
|
|
24
24
|
"test": "pnpm run build && node --test test/**/*.test.mjs",
|
|
25
25
|
"pack:dry-run": "pnpm run build && pnpm pack --pack-destination ./artifacts/workspace-kit-pack",
|
|
26
26
|
"check-release-metadata": "node scripts/check-release-metadata.mjs",
|
|
27
|
-
"parity": "node scripts/run-parity.mjs"
|
|
27
|
+
"parity": "node scripts/run-parity.mjs",
|
|
28
|
+
"check-compatibility": "pnpm run build && node scripts/check-compatibility.mjs",
|
|
29
|
+
"check-planning-consistency": "node scripts/check-planning-doc-consistency.mjs",
|
|
30
|
+
"check-release-channel": "node scripts/check-release-channel.mjs",
|
|
31
|
+
"generate-runtime-diagnostics": "node scripts/generate-runtime-diagnostics.mjs",
|
|
32
|
+
"prune-evidence": "node scripts/prune-evidence.mjs",
|
|
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"
|
|
28
35
|
},
|
|
29
36
|
"devDependencies": {
|
|
30
37
|
"@types/node": "^25.5.0",
|