@workflow-cannon/workspace-kit 0.7.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/README.md +5 -4
  2. package/dist/cli/run-command.d.ts +11 -0
  3. package/dist/cli/run-command.js +138 -0
  4. package/dist/cli.js +18 -135
  5. package/dist/contracts/index.d.ts +1 -1
  6. package/dist/contracts/module-contract.d.ts +13 -0
  7. package/dist/core/config-cli.js +4 -4
  8. package/dist/core/config-metadata.js +199 -5
  9. package/dist/core/index.d.ts +6 -0
  10. package/dist/core/index.js +6 -0
  11. package/dist/core/instruction-template-mapper.d.ts +9 -0
  12. package/dist/core/instruction-template-mapper.js +35 -0
  13. package/dist/core/lineage-contract.d.ts +1 -1
  14. package/dist/core/lineage-contract.js +1 -1
  15. package/dist/core/policy.d.ts +13 -2
  16. package/dist/core/policy.js +42 -25
  17. package/dist/core/response-template-contract.d.ts +15 -0
  18. package/dist/core/response-template-contract.js +10 -0
  19. package/dist/core/response-template-registry.d.ts +4 -0
  20. package/dist/core/response-template-registry.js +44 -0
  21. package/dist/core/response-template-shaping.d.ts +6 -0
  22. package/dist/core/response-template-shaping.js +128 -0
  23. package/dist/core/session-policy.d.ts +18 -0
  24. package/dist/core/session-policy.js +57 -0
  25. package/dist/core/transcript-completion-hook.d.ts +7 -0
  26. package/dist/core/transcript-completion-hook.js +128 -0
  27. package/dist/core/workspace-kit-config.d.ts +2 -1
  28. package/dist/core/workspace-kit-config.js +19 -23
  29. package/dist/modules/approvals/index.js +2 -2
  30. package/dist/modules/documentation/runtime.js +413 -20
  31. package/dist/modules/improvement/generate-recommendations-runtime.d.ts +7 -0
  32. package/dist/modules/improvement/generate-recommendations-runtime.js +37 -4
  33. package/dist/modules/improvement/improvement-state.d.ts +10 -1
  34. package/dist/modules/improvement/improvement-state.js +36 -7
  35. package/dist/modules/improvement/index.js +70 -23
  36. package/dist/modules/improvement/ingest.js +2 -1
  37. package/dist/modules/improvement/transcript-redaction.d.ts +4 -0
  38. package/dist/modules/improvement/transcript-redaction.js +10 -0
  39. package/dist/modules/improvement/transcript-sync-runtime.d.ts +42 -1
  40. package/dist/modules/improvement/transcript-sync-runtime.js +215 -9
  41. package/dist/modules/index.d.ts +1 -1
  42. package/dist/modules/index.js +1 -1
  43. package/dist/modules/task-engine/index.d.ts +0 -2
  44. package/dist/modules/task-engine/index.js +4 -78
  45. package/package.json +6 -2
  46. package/src/modules/documentation/README.md +39 -0
  47. package/src/modules/documentation/RULES.md +70 -0
  48. package/src/modules/documentation/config.md +14 -0
  49. package/src/modules/documentation/index.ts +120 -0
  50. package/src/modules/documentation/instructions/document-project.md +44 -0
  51. package/src/modules/documentation/instructions/documentation-maintainer.md +81 -0
  52. package/src/modules/documentation/instructions/generate-document.md +44 -0
  53. package/src/modules/documentation/runtime.ts +870 -0
  54. package/src/modules/documentation/schemas/documentation-schema.md +54 -0
  55. package/src/modules/documentation/state.md +8 -0
  56. package/src/modules/documentation/templates/AGENTS.md +84 -0
  57. package/src/modules/documentation/templates/ARCHITECTURE.md +71 -0
  58. package/src/modules/documentation/templates/PRINCIPLES.md +122 -0
  59. package/src/modules/documentation/templates/RELEASING.md +96 -0
  60. package/src/modules/documentation/templates/ROADMAP.md +131 -0
  61. package/src/modules/documentation/templates/SECURITY.md +53 -0
  62. package/src/modules/documentation/templates/SUPPORT.md +40 -0
  63. package/src/modules/documentation/templates/TERMS.md +61 -0
  64. package/src/modules/documentation/templates/runbooks/consumer-cadence.md +55 -0
  65. package/src/modules/documentation/templates/runbooks/parity-validation-flow.md +68 -0
  66. package/src/modules/documentation/templates/runbooks/release-channels.md +30 -0
  67. package/src/modules/documentation/templates/workbooks/phase2-config-policy-workbook.md +42 -0
  68. package/src/modules/documentation/templates/workbooks/task-engine-workbook.md +42 -0
  69. package/src/modules/documentation/templates/workbooks/transcript-automation-baseline.md +68 -0
  70. package/src/modules/documentation/types.ts +51 -0
  71. package/dist/modules/task-engine/generator.d.ts +0 -3
  72. package/dist/modules/task-engine/generator.js +0 -118
  73. package/dist/modules/task-engine/importer.d.ts +0 -8
  74. package/dist/modules/task-engine/importer.js +0 -163
@@ -0,0 +1,128 @@
1
+ import { truncateTemplateWarning } from "./response-template-contract.js";
2
+ import { getResponseTemplateDefinition } from "./response-template-registry.js";
3
+ import { parseTemplateDirectiveFromText } from "./instruction-template-mapper.js";
4
+ function readResponseTemplatesConfig(effective) {
5
+ const rt = effective.responseTemplates;
6
+ if (!rt || typeof rt !== "object" || Array.isArray(rt)) {
7
+ return { enforcementMode: "advisory", defaultTemplateId: "default", commandOverrides: {} };
8
+ }
9
+ const o = rt;
10
+ const modeRaw = o.enforcementMode;
11
+ const enforcementMode = modeRaw === "strict" ? "strict" : "advisory";
12
+ const defRaw = o.defaultTemplateId;
13
+ const def = typeof defRaw === "string" && defRaw.trim().length > 0 ? defRaw.trim() : "default";
14
+ const commandOverrides = {};
15
+ const co = o.commandOverrides;
16
+ if (co && typeof co === "object" && !Array.isArray(co)) {
17
+ for (const [k, v] of Object.entries(co)) {
18
+ if (typeof v === "string" && v.trim()) {
19
+ commandOverrides[k.trim()] = v.trim();
20
+ }
21
+ }
22
+ }
23
+ return { enforcementMode, defaultTemplateId: def, commandOverrides };
24
+ }
25
+ function resolveRequestedTemplateId(commandName, args) {
26
+ const parseWarnings = [];
27
+ const explicit = typeof args.responseTemplateId === "string" && args.responseTemplateId.trim()
28
+ ? args.responseTemplateId.trim()
29
+ : null;
30
+ const directiveSources = [
31
+ typeof args.responseTemplateDirective === "string" ? args.responseTemplateDirective : "",
32
+ typeof args.instructionTemplateDirective === "string" ? args.instructionTemplateDirective : "",
33
+ typeof args.instruction === "string" ? args.instruction : ""
34
+ ].filter(Boolean);
35
+ let fromText = null;
36
+ for (const src of directiveSources) {
37
+ const parsed = parseTemplateDirectiveFromText(src);
38
+ parseWarnings.push(...parsed.warnings);
39
+ if (parsed.templateId) {
40
+ fromText = parsed.templateId;
41
+ break;
42
+ }
43
+ }
44
+ if (explicit && fromText && explicit !== fromText) {
45
+ parseWarnings.push(truncateTemplateWarning(`responseTemplateId '${explicit}' disagrees with instruction directive '${fromText}'; using explicit id.`));
46
+ }
47
+ if (explicit)
48
+ return { templateId: explicit, parseWarnings };
49
+ if (fromText)
50
+ return { templateId: fromText, parseWarnings };
51
+ return { templateId: null, parseWarnings };
52
+ }
53
+ function attachPresentation(templateId, result) {
54
+ const def = getResponseTemplateDefinition(templateId);
55
+ if (!def || !result.data || typeof result.data !== "object" || Array.isArray(result.data)) {
56
+ return undefined;
57
+ }
58
+ const data = result.data;
59
+ const matchedSections = def.expectedSections.filter((k) => k in data);
60
+ return {
61
+ templateId: def.id,
62
+ matchedSections
63
+ };
64
+ }
65
+ function buildMeta(partial, startNs) {
66
+ const warningCount = partial.warnings.length;
67
+ return {
68
+ ...partial,
69
+ telemetry: {
70
+ resolveNs: Number(process.hrtime.bigint() - startNs),
71
+ warningCount
72
+ }
73
+ };
74
+ }
75
+ /**
76
+ * Apply response template metadata and optional presentation hints (T262, T265).
77
+ * Advisory mode never flips `ok`. Strict mode fails closed on unknown template ids when a template was explicitly requested or command override is set.
78
+ */
79
+ export function applyResponseTemplateApplication(commandName, args, result, effective) {
80
+ const startNs = process.hrtime.bigint();
81
+ const cfg = readResponseTemplatesConfig(effective);
82
+ const { templateId: requestedRaw, parseWarnings } = resolveRequestedTemplateId(commandName, args);
83
+ const override = cfg.commandOverrides[commandName];
84
+ const chosenId = requestedRaw ?? override ?? cfg.defaultTemplateId ?? "default";
85
+ const warnings = [...parseWarnings];
86
+ const def = getResponseTemplateDefinition(chosenId);
87
+ if (!def) {
88
+ warnings.push(truncateTemplateWarning(`Unknown response template '${chosenId}'.`));
89
+ const explicitRequest = Boolean(requestedRaw || override);
90
+ if (cfg.enforcementMode === "strict" && explicitRequest) {
91
+ return {
92
+ ...result,
93
+ ok: false,
94
+ code: "response-template-invalid",
95
+ message: truncateTemplateWarning(`Unknown response template '${chosenId}'.`),
96
+ responseTemplate: buildMeta({
97
+ requestedTemplateId: requestedRaw ?? override,
98
+ appliedTemplateId: null,
99
+ enforcementMode: cfg.enforcementMode,
100
+ warnings
101
+ }, startNs)
102
+ };
103
+ }
104
+ return {
105
+ ...result,
106
+ responseTemplate: buildMeta({
107
+ requestedTemplateId: requestedRaw ?? override ?? cfg.defaultTemplateId,
108
+ appliedTemplateId: null,
109
+ enforcementMode: cfg.enforcementMode,
110
+ warnings
111
+ }, startNs)
112
+ };
113
+ }
114
+ const presentation = attachPresentation(def.id, result);
115
+ const nextData = presentation && result.data && typeof result.data === "object" && !Array.isArray(result.data)
116
+ ? { ...result.data, presentation }
117
+ : result.data;
118
+ return {
119
+ ...result,
120
+ data: nextData,
121
+ responseTemplate: buildMeta({
122
+ requestedTemplateId: requestedRaw ?? override ?? cfg.defaultTemplateId,
123
+ appliedTemplateId: def.id,
124
+ enforcementMode: cfg.enforcementMode,
125
+ warnings
126
+ }, startNs)
127
+ };
128
+ }
@@ -0,0 +1,18 @@
1
+ import type { PolicyOperationId } from "./policy.js";
2
+ export declare const SESSION_POLICY_SCHEMA_VERSION: 1;
3
+ export type SessionPolicyGrant = {
4
+ rationale: string;
5
+ grantedAt: string;
6
+ };
7
+ export type SessionPolicyDocument = {
8
+ schemaVersion: typeof SESSION_POLICY_SCHEMA_VERSION;
9
+ /** Logical session id (override with WORKSPACE_KIT_SESSION_ID). */
10
+ sessionId: string;
11
+ /** Active grants for sensitive operations within this session file. */
12
+ grants: Partial<Record<PolicyOperationId, SessionPolicyGrant>>;
13
+ };
14
+ export declare function resolveSessionId(env: NodeJS.ProcessEnv): string;
15
+ export declare function loadSessionPolicyDocument(workspacePath: string): Promise<SessionPolicyDocument>;
16
+ export declare function saveSessionPolicyDocument(workspacePath: string, doc: SessionPolicyDocument): Promise<void>;
17
+ export declare function getSessionGrant(workspacePath: string, operationId: PolicyOperationId, sessionId: string): Promise<SessionPolicyGrant | undefined>;
18
+ export declare function recordSessionGrant(workspacePath: string, operationId: PolicyOperationId, sessionId: string, rationale: string): Promise<void>;
@@ -0,0 +1,57 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ export const SESSION_POLICY_SCHEMA_VERSION = 1;
4
+ const REL = ".workspace-kit/policy/session-grants.json";
5
+ function defaultDoc(sessionId) {
6
+ return {
7
+ schemaVersion: SESSION_POLICY_SCHEMA_VERSION,
8
+ sessionId,
9
+ grants: {}
10
+ };
11
+ }
12
+ export function resolveSessionId(env) {
13
+ const raw = env.WORKSPACE_KIT_SESSION_ID?.trim();
14
+ return raw && raw.length > 0 ? raw : "default";
15
+ }
16
+ export async function loadSessionPolicyDocument(workspacePath) {
17
+ const fp = path.join(workspacePath, REL);
18
+ try {
19
+ const raw = await fs.readFile(fp, "utf8");
20
+ const doc = JSON.parse(raw);
21
+ if (doc.schemaVersion !== SESSION_POLICY_SCHEMA_VERSION) {
22
+ return defaultDoc(resolveSessionId(process.env));
23
+ }
24
+ return {
25
+ ...defaultDoc(doc.sessionId ?? resolveSessionId(process.env)),
26
+ ...doc,
27
+ grants: doc.grants && typeof doc.grants === "object" ? doc.grants : {}
28
+ };
29
+ }
30
+ catch (e) {
31
+ if (e.code === "ENOENT") {
32
+ return defaultDoc(resolveSessionId(process.env));
33
+ }
34
+ throw e;
35
+ }
36
+ }
37
+ export async function saveSessionPolicyDocument(workspacePath, doc) {
38
+ const fp = path.join(workspacePath, REL);
39
+ await fs.mkdir(path.dirname(fp), { recursive: true });
40
+ await fs.writeFile(fp, `${JSON.stringify(doc, null, 2)}\n`, "utf8");
41
+ }
42
+ export async function getSessionGrant(workspacePath, operationId, sessionId) {
43
+ const doc = await loadSessionPolicyDocument(workspacePath);
44
+ if (doc.sessionId !== sessionId) {
45
+ return undefined;
46
+ }
47
+ return doc.grants[operationId];
48
+ }
49
+ export async function recordSessionGrant(workspacePath, operationId, sessionId, rationale) {
50
+ const doc = await loadSessionPolicyDocument(workspacePath);
51
+ doc.sessionId = sessionId;
52
+ doc.grants[operationId] = {
53
+ rationale,
54
+ grantedAt: new Date().toISOString()
55
+ };
56
+ await saveSessionPolicyDocument(workspacePath, doc);
57
+ }
@@ -0,0 +1,7 @@
1
+ export declare function readAfterTaskCompletedHook(effective: Record<string, unknown>): "off" | "sync" | "ingest";
2
+ export declare function resolveWorkspaceKitCli(workspacePath: string): string | null;
3
+ /**
4
+ * After a task reaches `completed`, optionally spawn sync/ingest without blocking the transition (T274).
5
+ * Ingest requires WORKSPACE_KIT_POLICY_APPROVAL in the parent env or falls back to sync.
6
+ */
7
+ export declare function maybeSpawnTranscriptHookAfterCompletion(workspacePath: string, effective: Record<string, unknown>): void;
@@ -0,0 +1,128 @@
1
+ import { existsSync, mkdirSync, statSync, unlinkSync, writeFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { spawn } from "node:child_process";
4
+ const LOCK_REL = ".workspace-kit/improvement/transcript-hook.lock";
5
+ const EVENT_LOG_REL = ".workspace-kit/improvement/transcript-hook-events.jsonl";
6
+ const LOCK_STALE_MS = 120_000;
7
+ export function readAfterTaskCompletedHook(effective) {
8
+ const imp = effective.improvement;
9
+ if (!imp || typeof imp !== "object" || Array.isArray(imp))
10
+ return "off";
11
+ const hooks = imp.hooks;
12
+ if (!hooks || typeof hooks !== "object" || Array.isArray(hooks))
13
+ return "off";
14
+ const v = hooks.afterTaskCompleted;
15
+ if (v === "sync" || v === "ingest")
16
+ return v;
17
+ return "off";
18
+ }
19
+ export function resolveWorkspaceKitCli(workspacePath) {
20
+ const candidates = [
21
+ path.join(workspacePath, "node_modules", "@workflow-cannon", "workspace-kit", "dist", "cli.js"),
22
+ path.join(workspacePath, "dist", "cli.js")
23
+ ];
24
+ for (const c of candidates) {
25
+ if (existsSync(c))
26
+ return c;
27
+ }
28
+ return null;
29
+ }
30
+ function lockPath(workspacePath) {
31
+ return path.join(workspacePath, LOCK_REL);
32
+ }
33
+ function hookLockBusy(workspacePath) {
34
+ const fp = lockPath(workspacePath);
35
+ try {
36
+ const st = statSync(fp);
37
+ return Date.now() - st.mtimeMs < LOCK_STALE_MS;
38
+ }
39
+ catch {
40
+ return false;
41
+ }
42
+ }
43
+ function appendHookEvent(workspacePath, event, details) {
44
+ const fp = path.join(workspacePath, EVENT_LOG_REL);
45
+ try {
46
+ mkdirSync(path.dirname(fp), { recursive: true });
47
+ writeFileSync(fp, `${JSON.stringify({
48
+ schemaVersion: 1,
49
+ timestamp: new Date().toISOString(),
50
+ event,
51
+ ...details
52
+ })}\n`, { encoding: "utf8", flag: "a" });
53
+ }
54
+ catch {
55
+ // Observability must never block task transitions.
56
+ }
57
+ }
58
+ /**
59
+ * After a task reaches `completed`, optionally spawn sync/ingest without blocking the transition (T274).
60
+ * Ingest requires WORKSPACE_KIT_POLICY_APPROVAL in the parent env or falls back to sync.
61
+ */
62
+ export function maybeSpawnTranscriptHookAfterCompletion(workspacePath, effective) {
63
+ const mode = readAfterTaskCompletedHook(effective);
64
+ if (mode === "off") {
65
+ appendHookEvent(workspacePath, "skipped", { reason: "hook-mode-off" });
66
+ return;
67
+ }
68
+ if (hookLockBusy(workspacePath)) {
69
+ appendHookEvent(workspacePath, "skipped", { reason: "lock-busy", mode });
70
+ return;
71
+ }
72
+ let subcommand = "sync-transcripts";
73
+ if (mode === "ingest" && process.env.WORKSPACE_KIT_POLICY_APPROVAL?.trim()) {
74
+ subcommand = "ingest-transcripts";
75
+ }
76
+ const cli = resolveWorkspaceKitCli(workspacePath);
77
+ if (!cli) {
78
+ appendHookEvent(workspacePath, "failed", {
79
+ reason: "cli-not-found",
80
+ mode,
81
+ subcommand
82
+ });
83
+ return;
84
+ }
85
+ const fp = lockPath(workspacePath);
86
+ try {
87
+ mkdirSync(path.dirname(fp), { recursive: true });
88
+ writeFileSync(fp, `${JSON.stringify({ at: new Date().toISOString(), subcommand })}\n`, "utf8");
89
+ }
90
+ catch {
91
+ appendHookEvent(workspacePath, "failed", {
92
+ reason: "lock-write-failed",
93
+ mode,
94
+ subcommand
95
+ });
96
+ return;
97
+ }
98
+ appendHookEvent(workspacePath, "started", { mode, subcommand });
99
+ const child = spawn(process.execPath, [cli, "run", subcommand, "{}"], {
100
+ cwd: workspacePath,
101
+ detached: true,
102
+ stdio: "ignore",
103
+ env: process.env
104
+ });
105
+ child.unref();
106
+ child.on("exit", () => {
107
+ try {
108
+ unlinkSync(fp);
109
+ }
110
+ catch {
111
+ /* ignore */
112
+ }
113
+ appendHookEvent(workspacePath, "completed", { mode, subcommand });
114
+ });
115
+ child.on("error", () => {
116
+ try {
117
+ unlinkSync(fp);
118
+ }
119
+ catch {
120
+ /* ignore */
121
+ }
122
+ appendHookEvent(workspacePath, "failed", {
123
+ reason: "spawn-error",
124
+ mode,
125
+ subcommand
126
+ });
127
+ });
128
+ }
@@ -12,7 +12,8 @@ export declare function getProjectConfigPath(workspacePath: string): string;
12
12
  export declare const KIT_CONFIG_DEFAULTS: Record<string, unknown>;
13
13
  /**
14
14
  * Static module-level defaults keyed by module id (merged in registry startup order).
15
- * Each value is deep-merged into the aggregate before project/env/invocation.
15
+ * Keep true defaults in KIT_CONFIG_DEFAULTS as the canonical source; use this map
16
+ * only when a module contributes additional non-default config structure.
16
17
  */
17
18
  export declare const MODULE_CONFIG_CONTRIBUTIONS: Record<string, Record<string, unknown>>;
18
19
  export declare function deepMerge(target: Record<string, unknown>, source: Record<string, unknown>): Record<string, unknown>;
@@ -14,42 +14,38 @@ export const KIT_CONFIG_DEFAULTS = {
14
14
  storeRelativePath: ".workspace-kit/tasks/state.json"
15
15
  },
16
16
  documentation: {},
17
+ responseTemplates: {
18
+ enforcementMode: "advisory",
19
+ defaultTemplateId: "default",
20
+ commandOverrides: {}
21
+ },
17
22
  improvement: {
18
23
  transcripts: {
19
- sourcePath: ".cursor/agent-transcripts",
20
- archivePath: "agent-transcripts"
24
+ sourcePath: "",
25
+ archivePath: "agent-transcripts",
26
+ maxFilesPerSync: 5000,
27
+ maxBytesPerFile: 50_000_000,
28
+ maxTotalScanBytes: 500_000_000,
29
+ discoveryPaths: []
21
30
  },
22
31
  cadence: {
23
32
  minIntervalMinutes: 15,
24
- skipIfNoNewTranscripts: true
33
+ skipIfNoNewTranscripts: true,
34
+ maxRecommendationCandidatesPerRun: 500
35
+ },
36
+ hooks: {
37
+ afterTaskCompleted: "off"
25
38
  }
26
39
  }
27
40
  };
28
41
  /**
29
42
  * Static module-level defaults keyed by module id (merged in registry startup order).
30
- * Each value is deep-merged into the aggregate before project/env/invocation.
43
+ * Keep true defaults in KIT_CONFIG_DEFAULTS as the canonical source; use this map
44
+ * only when a module contributes additional non-default config structure.
31
45
  */
32
46
  export const MODULE_CONFIG_CONTRIBUTIONS = {
33
- "task-engine": {
34
- tasks: {
35
- storeRelativePath: ".workspace-kit/tasks/state.json"
36
- }
37
- },
38
- documentation: {
39
- documentation: {}
40
- },
41
47
  approvals: {},
42
- planning: {},
43
- improvement: {
44
- transcripts: {
45
- sourcePath: ".cursor/agent-transcripts",
46
- archivePath: "agent-transcripts"
47
- },
48
- cadence: {
49
- minIntervalMinutes: 15,
50
- skipIfNoNewTranscripts: true
51
- }
52
- }
48
+ planning: {}
53
49
  };
54
50
  export function deepMerge(target, source) {
55
51
  const out = { ...target };
@@ -1,4 +1,4 @@
1
- import { resolveActor } from "../../core/policy.js";
1
+ import { resolveActorWithFallback } from "../../core/policy.js";
2
2
  import { runReviewItem } from "./review-runtime.js";
3
3
  export const approvalsModule = {
4
4
  registration: {
@@ -40,7 +40,7 @@ export const approvalsModule = {
40
40
  const args = command.args ?? {};
41
41
  const actor = typeof args.actor === "string" && args.actor.trim().length > 0
42
42
  ? args.actor.trim()
43
- : ctx.resolvedActor ?? resolveActor(ctx.workspacePath, args, process.env);
43
+ : ctx.resolvedActor ?? (await resolveActorWithFallback(ctx.workspacePath, args, process.env));
44
44
  const taskId = typeof args.taskId === "string" ? args.taskId : "";
45
45
  const decision = args.decision;
46
46
  if (decision !== "accept" && decision !== "decline" && decision !== "accept_edited") {