@workflow-cannon/workspace-kit 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/README.md +1 -1
  2. package/dist/cli.js +10 -6
  3. package/dist/core/config-cli.d.ts +6 -0
  4. package/dist/core/config-cli.js +479 -0
  5. package/dist/core/config-metadata.d.ts +35 -0
  6. package/dist/core/config-metadata.js +162 -0
  7. package/dist/core/config-mutations.d.ts +18 -0
  8. package/dist/core/config-mutations.js +32 -0
  9. package/dist/core/index.d.ts +7 -2
  10. package/dist/core/index.js +7 -2
  11. package/dist/core/lineage-contract.d.ts +59 -0
  12. package/dist/core/lineage-contract.js +9 -0
  13. package/dist/core/lineage-store.d.ts +16 -0
  14. package/dist/core/lineage-store.js +74 -0
  15. package/dist/core/policy.d.ts +12 -2
  16. package/dist/core/policy.js +37 -2
  17. package/dist/core/workspace-kit-config.d.ts +9 -1
  18. package/dist/core/workspace-kit-config.js +52 -4
  19. package/dist/modules/approvals/decisions-store.d.ts +20 -0
  20. package/dist/modules/approvals/decisions-store.js +52 -0
  21. package/dist/modules/approvals/index.js +57 -1
  22. package/dist/modules/approvals/review-runtime.d.ts +24 -0
  23. package/dist/modules/approvals/review-runtime.js +146 -0
  24. package/dist/modules/improvement/confidence.d.ts +23 -0
  25. package/dist/modules/improvement/confidence.js +50 -0
  26. package/dist/modules/improvement/generate-recommendations-runtime.d.ts +13 -0
  27. package/dist/modules/improvement/generate-recommendations-runtime.js +92 -0
  28. package/dist/modules/improvement/improvement-state.d.ts +11 -0
  29. package/dist/modules/improvement/improvement-state.js +42 -0
  30. package/dist/modules/improvement/index.js +47 -1
  31. package/dist/modules/improvement/ingest.d.ts +18 -0
  32. package/dist/modules/improvement/ingest.js +223 -0
  33. package/dist/modules/index.d.ts +1 -0
  34. package/dist/modules/index.js +1 -0
  35. package/dist/modules/task-engine/index.js +1 -1
  36. package/dist/modules/task-engine/transitions.js +1 -0
  37. package/dist/modules/workspace-config/index.js +37 -9
  38. package/package.json +1 -1
@@ -0,0 +1,223 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { execFileSync } from "node:child_process";
4
+ import crypto from "node:crypto";
5
+ import { computeHeuristicConfidence, shouldAdmitRecommendation } from "./confidence.js";
6
+ function sha256Hex(s) {
7
+ return crypto.createHash("sha256").update(s, "utf8").digest("hex");
8
+ }
9
+ function stableEvidenceKey(kind, parts) {
10
+ return `${kind}:${sha256Hex(parts.join("\0")).slice(0, 40)}`;
11
+ }
12
+ async function readJsonlLines(filePath) {
13
+ try {
14
+ const raw = await fs.readFile(filePath, "utf8");
15
+ return raw.split("\n").filter((l) => l.trim().length > 0);
16
+ }
17
+ catch (e) {
18
+ if (e.code === "ENOENT")
19
+ return [];
20
+ throw e;
21
+ }
22
+ }
23
+ async function globJsonlRecursive(dir, acc = []) {
24
+ let entries;
25
+ try {
26
+ entries = await fs.readdir(dir, { withFileTypes: true });
27
+ }
28
+ catch (e) {
29
+ if (e.code === "ENOENT")
30
+ return acc;
31
+ throw e;
32
+ }
33
+ for (const ent of entries) {
34
+ const p = path.join(dir, ent.name);
35
+ if (ent.isDirectory()) {
36
+ await globJsonlRecursive(p, acc);
37
+ }
38
+ else if (ent.isFile() && ent.name.endsWith(".jsonl")) {
39
+ acc.push(p);
40
+ }
41
+ }
42
+ return acc;
43
+ }
44
+ const FRICTION = /\b(error|fail|broken|hosed|bug|crash|denied|invalid|exception)\b/i;
45
+ function scoreTranscriptLine(line) {
46
+ if (!FRICTION.test(line))
47
+ return 0;
48
+ let s = 0.45;
49
+ if (/\b(always|again|still|never)\b/i.test(line))
50
+ s += 0.15;
51
+ return Math.min(1, s);
52
+ }
53
+ export async function ingestAgentTranscripts(workspacePath, transcriptsRootRel, state) {
54
+ const root = path.resolve(workspacePath, transcriptsRootRel);
55
+ const files = await globJsonlRecursive(root);
56
+ const out = [];
57
+ for (const abs of files.sort()) {
58
+ const rel = path.relative(workspacePath, abs);
59
+ const lines = await readJsonlLines(abs);
60
+ const start = state.transcriptLineCursors[rel] ?? 0;
61
+ const slice = lines.slice(start);
62
+ let maxScore = 0;
63
+ let sampleLine = "";
64
+ for (const line of slice) {
65
+ const sc = scoreTranscriptLine(line);
66
+ if (sc > maxScore) {
67
+ maxScore = sc;
68
+ sampleLine = line.slice(0, 200);
69
+ }
70
+ }
71
+ state.transcriptLineCursors[rel] = lines.length;
72
+ if (maxScore === 0)
73
+ continue;
74
+ const evidenceKey = stableEvidenceKey("transcript", [rel, String(start), String(lines.length)]);
75
+ const signals = { transcriptFriction: maxScore };
76
+ const confidence = computeHeuristicConfidence("transcript", signals);
77
+ if (!shouldAdmitRecommendation(confidence))
78
+ continue;
79
+ out.push({
80
+ evidenceKind: "transcript",
81
+ evidenceKey,
82
+ title: `Reduce friction hinted in transcript (${path.basename(rel)})`,
83
+ provenanceRefs: { transcriptPath: rel, sampleLine },
84
+ signals,
85
+ confidence
86
+ });
87
+ }
88
+ return out;
89
+ }
90
+ export function ingestGitDiffBetweenTags(workspacePath, fromTag, toTag) {
91
+ let names;
92
+ try {
93
+ names = execFileSync("git", ["-C", workspacePath, "diff", `${fromTag}..${toTag}`, "--name-only"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
94
+ }
95
+ catch {
96
+ return null;
97
+ }
98
+ const fileList = names.split("\n").filter(Boolean);
99
+ const churn = Math.min(1, fileList.length / 25);
100
+ const impact = 0.4 + churn * 0.55;
101
+ const evidenceKey = stableEvidenceKey("git_diff", [fromTag, toTag, fileList.slice(0, 30).join(",")]);
102
+ const signals = { diffImpact: impact };
103
+ const confidence = computeHeuristicConfidence("git_diff", signals);
104
+ if (!shouldAdmitRecommendation(confidence))
105
+ return null;
106
+ return {
107
+ evidenceKind: "git_diff",
108
+ evidenceKey,
109
+ title: `Review workflow impact of changes ${fromTag} → ${toTag} (${fileList.length} paths)`,
110
+ provenanceRefs: { fromTag, toTag, pathCount: String(fileList.length) },
111
+ signals,
112
+ confidence
113
+ };
114
+ }
115
+ export async function ingestPolicyDenials(workspacePath, state) {
116
+ const fp = path.join(workspacePath, ".workspace-kit/policy/traces.jsonl");
117
+ const lines = await readJsonlLines(fp);
118
+ const start = state.policyTraceLineCursor;
119
+ const slice = lines.slice(start);
120
+ state.policyTraceLineCursor = lines.length;
121
+ const out = [];
122
+ for (const line of slice) {
123
+ let rec;
124
+ try {
125
+ rec = JSON.parse(line);
126
+ }
127
+ catch {
128
+ continue;
129
+ }
130
+ if (rec.allowed !== false)
131
+ continue;
132
+ const op = typeof rec.operationId === "string" ? rec.operationId : "unknown";
133
+ const ts = typeof rec.timestamp === "string" ? rec.timestamp : "";
134
+ const evidenceKey = stableEvidenceKey("policy_deny", [op, ts, line.slice(0, 120)]);
135
+ const hasRationale = typeof rec.rationale === "string" && rec.rationale.length > 0;
136
+ const policyDenial = hasRationale ? 0.72 : 0.55;
137
+ const signals = { policyDenial };
138
+ const confidence = computeHeuristicConfidence("policy_deny", signals);
139
+ if (!shouldAdmitRecommendation(confidence))
140
+ continue;
141
+ out.push({
142
+ evidenceKind: "policy_deny",
143
+ evidenceKey,
144
+ title: `Soften or document policy friction for ${op}`,
145
+ provenanceRefs: {
146
+ operationId: op,
147
+ traceTimestamp: ts,
148
+ command: typeof rec.command === "string" ? rec.command : ""
149
+ },
150
+ signals,
151
+ confidence
152
+ });
153
+ }
154
+ return out;
155
+ }
156
+ export async function ingestConfigMutations(workspacePath, state) {
157
+ const fp = path.join(workspacePath, ".workspace-kit/config/mutations.jsonl");
158
+ const lines = await readJsonlLines(fp);
159
+ const start = state.mutationLineCursor;
160
+ const slice = lines.slice(start);
161
+ state.mutationLineCursor = lines.length;
162
+ const out = [];
163
+ for (const line of slice) {
164
+ let rec;
165
+ try {
166
+ rec = JSON.parse(line);
167
+ }
168
+ catch {
169
+ continue;
170
+ }
171
+ if (rec.ok === true)
172
+ continue;
173
+ const ts = typeof rec.timestamp === "string" ? rec.timestamp : "";
174
+ const k = typeof rec.key === "string" ? rec.key : "";
175
+ const evidenceKey = stableEvidenceKey("config_mutation", ["mutations.jsonl", ts, k, String(rec.code ?? "")]);
176
+ const mutationRejection = 0.62;
177
+ const signals = { mutationRejection };
178
+ const confidence = computeHeuristicConfidence("config_mutation", signals);
179
+ if (!shouldAdmitRecommendation(confidence))
180
+ continue;
181
+ out.push({
182
+ evidenceKind: "config_mutation",
183
+ evidenceKey,
184
+ title: `Improve config UX or validation for key ${k || "(unknown)"}`,
185
+ provenanceRefs: { mutationsFile: "mutations.jsonl", timestamp: ts, key: k },
186
+ signals,
187
+ confidence
188
+ });
189
+ }
190
+ return out;
191
+ }
192
+ export function ingestTaskTransitionFriction(transitionLog, state) {
193
+ const start = state.transitionLogLengthCursor;
194
+ const slice = transitionLog.slice(start);
195
+ state.transitionLogLengthCursor = transitionLog.length;
196
+ const counts = new Map();
197
+ for (const ev of slice) {
198
+ counts.set(ev.taskId, (counts.get(ev.taskId) ?? 0) + 1);
199
+ }
200
+ const out = [];
201
+ for (const [taskId, count] of counts) {
202
+ if (count < 4)
203
+ continue;
204
+ const taskFriction = Math.min(1, 0.38 + count * 0.08);
205
+ const evidenceKey = stableEvidenceKey("task_transition", [taskId, String(count)]);
206
+ const signals = { taskFriction };
207
+ const confidence = computeHeuristicConfidence("task_transition", signals);
208
+ if (!shouldAdmitRecommendation(confidence))
209
+ continue;
210
+ out.push({
211
+ evidenceKind: "task_transition",
212
+ evidenceKey,
213
+ title: `Stabilize transitions for task ${taskId} (high churn: ${count} events)`,
214
+ provenanceRefs: { taskId, transitionEventCount: String(count) },
215
+ signals,
216
+ confidence
217
+ });
218
+ }
219
+ return out;
220
+ }
221
+ export function taskIdForEvidenceKey(evidenceKey) {
222
+ return `imp-${sha256Hex(evidenceKey).slice(0, 14)}`;
223
+ }
@@ -2,6 +2,7 @@ export { approvalsModule } from "./approvals/index.js";
2
2
  export { documentationModule } from "./documentation/index.js";
3
3
  export type { DocumentationConflict, DocumentationGenerateOptions, DocumentationGenerateResult, DocumentationGenerationEvidence, DocumentationValidationIssue } from "./documentation/types.js";
4
4
  export { improvementModule } from "./improvement/index.js";
5
+ export { computeHeuristicConfidence, HEURISTIC_1_ADMISSION_THRESHOLD, shouldAdmitRecommendation, type ConfidenceResult, type ConfidenceSignals, type EvidenceKind } from "./improvement/confidence.js";
5
6
  export { workspaceConfigModule } from "./workspace-config/index.js";
6
7
  export { planningModule } from "./planning/index.js";
7
8
  export { taskEngineModule, TaskStore, TransitionService, TaskEngineError, TransitionValidator, isTransitionAllowed, getTransitionAction, resolveTargetState, getAllowedTransitionsFrom, stateValidityGuard, dependencyCheckGuard, generateTasksMd, importTasksFromMarkdown, getNextActions } from "./task-engine/index.js";
@@ -1,6 +1,7 @@
1
1
  export { approvalsModule } from "./approvals/index.js";
2
2
  export { documentationModule } from "./documentation/index.js";
3
3
  export { improvementModule } from "./improvement/index.js";
4
+ export { computeHeuristicConfidence, HEURISTIC_1_ADMISSION_THRESHOLD, shouldAdmitRecommendation } from "./improvement/confidence.js";
4
5
  export { workspaceConfigModule } from "./workspace-config/index.js";
5
6
  export { planningModule } from "./planning/index.js";
6
7
  export { taskEngineModule, TaskStore, TransitionService, TaskEngineError, TransitionValidator, isTransitionAllowed, getTransitionAction, resolveTargetState, getAllowedTransitionsFrom, stateValidityGuard, dependencyCheckGuard, generateTasksMd, importTasksFromMarkdown, getNextActions } from "./task-engine/index.js";
@@ -23,7 +23,7 @@ function taskStorePath(ctx) {
23
23
  export const taskEngineModule = {
24
24
  registration: {
25
25
  id: "task-engine",
26
- version: "0.4.0",
26
+ version: "0.5.0",
27
27
  contractVersion: "1",
28
28
  capabilities: ["task-engine"],
29
29
  dependsOn: [],
@@ -13,6 +13,7 @@ const ALLOWED_TRANSITIONS = {
13
13
  "ready->blocked": { action: "block" },
14
14
  "ready->cancelled": { action: "cancel" },
15
15
  "in_progress->completed": { action: "complete" },
16
+ "in_progress->cancelled": { action: "decline" },
16
17
  "in_progress->blocked": { action: "block" },
17
18
  "in_progress->ready": { action: "pause" },
18
19
  "blocked->ready": { action: "unblock" },
@@ -1,4 +1,4 @@
1
- import { explainConfigPath, resolveWorkspaceConfigWithLayers } from "../../core/workspace-kit-config.js";
1
+ import { explainConfigPath, normalizeConfigForExport, resolveWorkspaceConfigWithLayers } from "../../core/workspace-kit-config.js";
2
2
  async function handleExplainConfig(args, ctx) {
3
3
  const pathArg = typeof args.path === "string" ? args.path.trim() : "";
4
4
  if (!pathArg) {
@@ -23,6 +23,24 @@ async function handleExplainConfig(args, ctx) {
23
23
  data: explained
24
24
  };
25
25
  }
26
+ async function handleResolveConfig(args, ctx) {
27
+ const invocationConfig = typeof args.config === "object" && args.config !== null && !Array.isArray(args.config)
28
+ ? args.config
29
+ : {};
30
+ const { effective, layers } = await resolveWorkspaceConfigWithLayers({
31
+ workspacePath: ctx.workspacePath,
32
+ registry: ctx.registry,
33
+ invocationConfig
34
+ });
35
+ return {
36
+ ok: true,
37
+ code: "config-resolved",
38
+ data: {
39
+ effective: normalizeConfigForExport(effective),
40
+ layers: layers.map((l) => ({ id: l.id }))
41
+ }
42
+ };
43
+ }
26
44
  export const workspaceConfigModule = {
27
45
  registration: {
28
46
  id: "workspace-config",
@@ -48,25 +66,35 @@ export const workspaceConfigModule = {
48
66
  name: "explain-config",
49
67
  file: "explain-config.md",
50
68
  description: "Agent-first JSON: effective config value and winning layer for a dotted path."
69
+ },
70
+ {
71
+ name: "resolve-config",
72
+ file: "resolve-config.md",
73
+ description: "Agent-first JSON: full effective config (sorted) and merge layer ids."
51
74
  }
52
75
  ]
53
76
  }
54
77
  },
55
78
  async onCommand(command, ctx) {
56
- if (command.name !== "explain-config") {
57
- return { ok: false, code: "unknown-command", message: "workspace-config only implements explain-config" };
58
- }
59
79
  const reg = ctx.moduleRegistry;
60
80
  if (!reg) {
61
81
  return {
62
82
  ok: false,
63
83
  code: "internal-error",
64
- message: "explain-config requires moduleRegistry on context (CLI wiring)"
84
+ message: "workspace-config requires moduleRegistry on context (CLI wiring)"
65
85
  };
66
86
  }
67
- return handleExplainConfig(command.args ?? {}, {
68
- workspacePath: ctx.workspacePath,
69
- registry: reg
70
- });
87
+ const baseCtx = { workspacePath: ctx.workspacePath, registry: reg };
88
+ if (command.name === "explain-config") {
89
+ return handleExplainConfig(command.args ?? {}, baseCtx);
90
+ }
91
+ if (command.name === "resolve-config") {
92
+ return handleResolveConfig(command.args ?? {}, baseCtx);
93
+ }
94
+ return {
95
+ ok: false,
96
+ code: "unknown-command",
97
+ message: `workspace-config: unknown command '${command.name}'`
98
+ };
71
99
  }
72
100
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@workflow-cannon/workspace-kit",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "private": false,
5
5
  "packageManager": "pnpm@10.0.0",
6
6
  "license": "MIT",