@workflow-cannon/workspace-kit 0.8.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 (42) hide show
  1. package/README.md +2 -1
  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 -145
  5. package/dist/core/config-cli.js +4 -4
  6. package/dist/core/config-metadata.js +3 -3
  7. package/dist/core/policy.d.ts +9 -1
  8. package/dist/core/policy.js +39 -22
  9. package/dist/core/transcript-completion-hook.js +41 -3
  10. package/dist/core/workspace-kit-config.d.ts +2 -1
  11. package/dist/core/workspace-kit-config.js +4 -29
  12. package/dist/modules/approvals/index.js +2 -2
  13. package/dist/modules/documentation/runtime.js +30 -6
  14. package/dist/modules/improvement/index.js +15 -3
  15. package/dist/modules/improvement/transcript-sync-runtime.d.ts +5 -0
  16. package/dist/modules/improvement/transcript-sync-runtime.js +17 -0
  17. package/package.json +2 -1
  18. package/src/modules/documentation/README.md +39 -0
  19. package/src/modules/documentation/RULES.md +70 -0
  20. package/src/modules/documentation/config.md +14 -0
  21. package/src/modules/documentation/index.ts +120 -0
  22. package/src/modules/documentation/instructions/document-project.md +44 -0
  23. package/src/modules/documentation/instructions/documentation-maintainer.md +81 -0
  24. package/src/modules/documentation/instructions/generate-document.md +44 -0
  25. package/src/modules/documentation/runtime.ts +870 -0
  26. package/src/modules/documentation/schemas/documentation-schema.md +54 -0
  27. package/src/modules/documentation/state.md +8 -0
  28. package/src/modules/documentation/templates/AGENTS.md +84 -0
  29. package/src/modules/documentation/templates/ARCHITECTURE.md +71 -0
  30. package/src/modules/documentation/templates/PRINCIPLES.md +122 -0
  31. package/src/modules/documentation/templates/RELEASING.md +96 -0
  32. package/src/modules/documentation/templates/ROADMAP.md +131 -0
  33. package/src/modules/documentation/templates/SECURITY.md +53 -0
  34. package/src/modules/documentation/templates/SUPPORT.md +40 -0
  35. package/src/modules/documentation/templates/TERMS.md +61 -0
  36. package/src/modules/documentation/templates/runbooks/consumer-cadence.md +55 -0
  37. package/src/modules/documentation/templates/runbooks/parity-validation-flow.md +68 -0
  38. package/src/modules/documentation/templates/runbooks/release-channels.md +30 -0
  39. package/src/modules/documentation/templates/workbooks/phase2-config-policy-workbook.md +42 -0
  40. package/src/modules/documentation/templates/workbooks/task-engine-workbook.md +42 -0
  41. package/src/modules/documentation/templates/workbooks/transcript-automation-baseline.md +68 -0
  42. package/src/modules/documentation/types.ts +51 -0
package/README.md CHANGED
@@ -104,8 +104,9 @@ npm install @workflow-cannon/workspace-kit
104
104
  - Glossary and agent-guidance terms: `docs/maintainers/TERMS.md`
105
105
  - Architecture direction: `docs/maintainers/ARCHITECTURE.md`
106
106
  - Project decisions: `docs/maintainers/DECISIONS.md`
107
- - Contribution guidelines: `docs/maintainers/CONTRIBUTING.md`
107
+ - Governance policy surface: `docs/maintainers/GOVERNANCE.md`
108
108
  - Release process and gates: `docs/maintainers/RELEASING.md`
109
+ - Canonical changelog: `docs/maintainers/CHANGELOG.md` (`CHANGELOG.md` at repo root is pointer-only)
109
110
  - Canonical AI module build guidance: `.ai/module-build.md`
110
111
  - Human module build guide: `docs/maintainers/module-build-guide.md`
111
112
  - Security, support, and governance: `docs/maintainers/SECURITY.md`, `docs/maintainers/SUPPORT.md`, `docs/maintainers/GOVERNANCE.md`
@@ -0,0 +1,11 @@
1
+ export type RunCommandIo = {
2
+ writeLine: (message: string) => void;
3
+ writeError: (message: string) => void;
4
+ };
5
+ export type RunCommandExitCodes = {
6
+ success: number;
7
+ validationFailure: number;
8
+ usageError: number;
9
+ internalError: number;
10
+ };
11
+ export declare function handleRunCommand(cwd: string, args: string[], io: RunCommandIo, codes: RunCommandExitCodes): Promise<number>;
@@ -0,0 +1,138 @@
1
+ import { ModuleRegistry } from "../core/module-registry.js";
2
+ import { ModuleCommandRouter } from "../core/module-command-router.js";
3
+ import { appendPolicyTrace, isSensitiveModuleCommandForEffective, parsePolicyApproval, resolveActorWithFallback, resolvePolicyOperationIdForCommand } from "../core/policy.js";
4
+ import { getSessionGrant, recordSessionGrant, resolveSessionId } from "../core/session-policy.js";
5
+ import { applyResponseTemplateApplication } from "../core/response-template-shaping.js";
6
+ import { resolveWorkspaceConfigWithLayers } from "../core/workspace-kit-config.js";
7
+ import { documentationModule } from "../modules/documentation/index.js";
8
+ import { taskEngineModule } from "../modules/task-engine/index.js";
9
+ import { approvalsModule } from "../modules/approvals/index.js";
10
+ import { planningModule } from "../modules/planning/index.js";
11
+ import { improvementModule } from "../modules/improvement/index.js";
12
+ import { workspaceConfigModule } from "../modules/workspace-config/index.js";
13
+ export async function handleRunCommand(cwd, args, io, codes) {
14
+ const { writeLine, writeError } = io;
15
+ const allModules = [
16
+ workspaceConfigModule,
17
+ documentationModule,
18
+ taskEngineModule,
19
+ approvalsModule,
20
+ planningModule,
21
+ improvementModule
22
+ ];
23
+ const registry = new ModuleRegistry(allModules);
24
+ const router = new ModuleCommandRouter(registry);
25
+ const subcommand = args[1];
26
+ if (!subcommand) {
27
+ const commands = router.listCommands();
28
+ writeLine("Available module commands:");
29
+ for (const cmd of commands) {
30
+ const desc = cmd.description ? ` — ${cmd.description}` : "";
31
+ writeLine(` ${cmd.name} (${cmd.moduleId})${desc}`);
32
+ }
33
+ writeLine("");
34
+ writeLine("Usage: workspace-kit run <command> [json-args]");
35
+ return codes.success;
36
+ }
37
+ let commandArgs = {};
38
+ if (args[2]) {
39
+ try {
40
+ const parsed = JSON.parse(args[2]);
41
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
42
+ commandArgs = parsed;
43
+ }
44
+ else {
45
+ writeError("Command args must be a JSON object.");
46
+ return codes.usageError;
47
+ }
48
+ }
49
+ catch {
50
+ writeError(`Invalid JSON args: ${args[2]}`);
51
+ return codes.usageError;
52
+ }
53
+ }
54
+ const invocationConfig = typeof commandArgs.config === "object" &&
55
+ commandArgs.config !== null &&
56
+ !Array.isArray(commandArgs.config)
57
+ ? commandArgs.config
58
+ : {};
59
+ let effective;
60
+ try {
61
+ const resolved = await resolveWorkspaceConfigWithLayers({
62
+ workspacePath: cwd,
63
+ registry,
64
+ invocationConfig
65
+ });
66
+ effective = resolved.effective;
67
+ }
68
+ catch (err) {
69
+ const message = err instanceof Error ? err.message : String(err);
70
+ writeError(`Config resolution failed: ${message}`);
71
+ return codes.validationFailure;
72
+ }
73
+ const actor = await resolveActorWithFallback(cwd, commandArgs, process.env);
74
+ const sensitive = isSensitiveModuleCommandForEffective(subcommand, commandArgs, effective);
75
+ const sessionId = resolveSessionId(process.env);
76
+ const policyOp = resolvePolicyOperationIdForCommand(subcommand, effective);
77
+ const explicitPolicyApproval = parsePolicyApproval(commandArgs);
78
+ let resolvedSensitiveApproval = explicitPolicyApproval;
79
+ if (sensitive) {
80
+ if (!resolvedSensitiveApproval && policyOp) {
81
+ const grant = await getSessionGrant(cwd, policyOp, sessionId);
82
+ if (grant) {
83
+ resolvedSensitiveApproval = { confirmed: true, rationale: grant.rationale };
84
+ }
85
+ }
86
+ if (!resolvedSensitiveApproval) {
87
+ if (policyOp) {
88
+ await appendPolicyTrace(cwd, {
89
+ timestamp: new Date().toISOString(),
90
+ operationId: policyOp,
91
+ command: `run ${subcommand}`,
92
+ actor,
93
+ allowed: false,
94
+ message: "missing policyApproval in JSON args"
95
+ });
96
+ }
97
+ writeLine(JSON.stringify({
98
+ ok: false,
99
+ code: "policy-denied",
100
+ message: 'Sensitive command requires policyApproval in JSON args (or an existing session grant for this operation): {"policyApproval":{"confirmed":true,"rationale":"why","scope":"session"}}'
101
+ }, null, 2));
102
+ return codes.validationFailure;
103
+ }
104
+ }
105
+ const ctx = {
106
+ runtimeVersion: "0.1",
107
+ workspacePath: cwd,
108
+ effectiveConfig: effective,
109
+ resolvedActor: actor,
110
+ moduleRegistry: registry
111
+ };
112
+ try {
113
+ const rawResult = await router.execute(subcommand, commandArgs, ctx);
114
+ if (sensitive && resolvedSensitiveApproval && policyOp) {
115
+ await appendPolicyTrace(cwd, {
116
+ timestamp: new Date().toISOString(),
117
+ operationId: policyOp,
118
+ command: `run ${subcommand}`,
119
+ actor,
120
+ allowed: true,
121
+ rationale: resolvedSensitiveApproval.rationale,
122
+ commandOk: rawResult.ok,
123
+ message: rawResult.message
124
+ });
125
+ if (explicitPolicyApproval?.scope === "session" && rawResult.ok) {
126
+ await recordSessionGrant(cwd, policyOp, sessionId, explicitPolicyApproval.rationale);
127
+ }
128
+ }
129
+ const result = applyResponseTemplateApplication(subcommand, commandArgs, rawResult, effective);
130
+ writeLine(JSON.stringify(result, null, 2));
131
+ return result.ok ? codes.success : codes.validationFailure;
132
+ }
133
+ catch (error) {
134
+ const message = error instanceof Error ? error.message : String(error);
135
+ writeError(`Module command failed: ${message}`);
136
+ return codes.internalError;
137
+ }
138
+ }
package/dist/cli.js CHANGED
@@ -2,23 +2,14 @@
2
2
  import fs from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import { pathToFileURL } from "node:url";
5
- import { ModuleRegistry } from "./core/module-registry.js";
6
- import { ModuleCommandRouter } from "./core/module-command-router.js";
7
- import { appendPolicyTrace, isSensitiveModuleCommandForEffective, parsePolicyApproval, parsePolicyApprovalFromEnv, resolveActor, resolvePolicyOperationIdForCommand } from "./core/policy.js";
8
- import { getSessionGrant, recordSessionGrant, resolveSessionId } from "./core/session-policy.js";
9
- import { applyResponseTemplateApplication } from "./core/response-template-shaping.js";
5
+ import { appendPolicyTrace, parsePolicyApprovalFromEnv, resolveActorWithFallback } from "./core/policy.js";
10
6
  import { runWorkspaceConfigCli } from "./core/config-cli.js";
11
- import { resolveWorkspaceConfigWithLayers } from "./core/workspace-kit-config.js";
12
- import { documentationModule } from "./modules/documentation/index.js";
13
- import { taskEngineModule } from "./modules/task-engine/index.js";
14
- import { approvalsModule } from "./modules/approvals/index.js";
15
- import { planningModule } from "./modules/planning/index.js";
16
- import { improvementModule } from "./modules/improvement/index.js";
17
- import { workspaceConfigModule } from "./modules/workspace-config/index.js";
7
+ import { handleRunCommand } from "./cli/run-command.js";
18
8
  const EXIT_SUCCESS = 0;
19
9
  const EXIT_VALIDATION_FAILURE = 1;
20
10
  const EXIT_USAGE_ERROR = 2;
21
11
  const EXIT_INTERNAL_ERROR = 3;
12
+ const CANONICAL_KIT_NAME = "@workflow-cannon/workspace-kit";
22
13
  export const defaultWorkspaceKitPaths = {
23
14
  profile: "workspace-kit.profile.json",
24
15
  profileSchema: "schemas/workspace-kit-profile.schema.json",
@@ -108,7 +99,7 @@ async function requireCliPolicyApproval(cwd, operationId, commandLabel, writeErr
108
99
  timestamp: new Date().toISOString(),
109
100
  operationId,
110
101
  command: commandLabel,
111
- actor: resolveActor(cwd, {}, process.env),
102
+ actor: await resolveActorWithFallback(cwd, {}, process.env),
112
103
  allowed: false,
113
104
  message: "missing WORKSPACE_KIT_POLICY_APPROVAL"
114
105
  });
@@ -121,7 +112,7 @@ async function recordCliPolicySuccess(cwd, operationId, commandLabel, rationale,
121
112
  timestamp: new Date().toISOString(),
122
113
  operationId,
123
114
  command: commandLabel,
124
- actor: resolveActor(cwd, {}, process.env),
115
+ actor: await resolveActorWithFallback(cwd, {}, process.env),
125
116
  allowed: true,
126
117
  rationale,
127
118
  commandOk
@@ -275,7 +266,8 @@ async function resolvePackageVersion(cwd) {
275
266
  const version = parsed.version;
276
267
  const name = parsed.name;
277
268
  if (typeof version === "string" && version.length > 0 && typeof name === "string") {
278
- if (name === "quicktask-workspace-kit" ||
269
+ if (name === CANONICAL_KIT_NAME ||
270
+ name === "quicktask-workspace-kit" ||
279
271
  candidatePath.endsWith("packages/workspace-kit/package.json")) {
280
272
  return version;
281
273
  }
@@ -420,11 +412,7 @@ export async function runCli(args, options = {}) {
420
412
  const mergedManifest = {
421
413
  schemaVersion: 1,
422
414
  kit: {
423
- name: typeof existingManifest.kit === "object" &&
424
- existingManifest.kit &&
425
- typeof existingManifest.kit.name === "string"
426
- ? existingManifest.kit.name
427
- : "quicktask-workspace-kit",
415
+ name: CANONICAL_KIT_NAME,
428
416
  version: typeof existingManifest.kit === "object" &&
429
417
  existingManifest.kit &&
430
418
  typeof existingManifest.kit.version === "string"
@@ -554,12 +542,14 @@ export async function runCli(args, options = {}) {
554
542
  if (!manifestKitName || !manifestKitVersion) {
555
543
  driftFindings.push(`${defaultWorkspaceKitPaths.manifest}: kit.name and kit.version are required`);
556
544
  }
557
- else if (manifestKitName === "quicktask-workspace-kit" && packageVersion) {
545
+ else if ((manifestKitName === CANONICAL_KIT_NAME || manifestKitName === "quicktask-workspace-kit") &&
546
+ packageVersion) {
558
547
  if (manifestKitVersion !== packageVersion) {
559
548
  driftFindings.push(`${defaultWorkspaceKitPaths.manifest}: kit.version (${manifestKitVersion}) does not match package version (${packageVersion})`);
560
549
  }
561
550
  }
562
- else if (manifestKitName !== "quicktask-workspace-kit") {
551
+ else if (manifestKitName !== CANONICAL_KIT_NAME &&
552
+ manifestKitName !== "quicktask-workspace-kit") {
563
553
  warnings.push(`${defaultWorkspaceKitPaths.manifest}: kit.name is '${manifestKitName}', skipping package-version drift comparison`);
564
554
  }
565
555
  }
@@ -581,129 +571,12 @@ export async function runCli(args, options = {}) {
581
571
  return EXIT_SUCCESS;
582
572
  }
583
573
  if (command === "run") {
584
- const allModules = [
585
- workspaceConfigModule,
586
- documentationModule,
587
- taskEngineModule,
588
- approvalsModule,
589
- planningModule,
590
- improvementModule
591
- ];
592
- const registry = new ModuleRegistry(allModules);
593
- const router = new ModuleCommandRouter(registry);
594
- const subcommand = args[1];
595
- if (!subcommand) {
596
- const commands = router.listCommands();
597
- writeLine("Available module commands:");
598
- for (const cmd of commands) {
599
- const desc = cmd.description ? ` — ${cmd.description}` : "";
600
- writeLine(` ${cmd.name} (${cmd.moduleId})${desc}`);
601
- }
602
- writeLine("");
603
- writeLine("Usage: workspace-kit run <command> [json-args]");
604
- return EXIT_SUCCESS;
605
- }
606
- let commandArgs = {};
607
- if (args[2]) {
608
- try {
609
- const parsed = JSON.parse(args[2]);
610
- if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
611
- commandArgs = parsed;
612
- }
613
- else {
614
- writeError("Command args must be a JSON object.");
615
- return EXIT_USAGE_ERROR;
616
- }
617
- }
618
- catch {
619
- writeError(`Invalid JSON args: ${args[2]}`);
620
- return EXIT_USAGE_ERROR;
621
- }
622
- }
623
- const invocationConfig = typeof commandArgs.config === "object" &&
624
- commandArgs.config !== null &&
625
- !Array.isArray(commandArgs.config)
626
- ? commandArgs.config
627
- : {};
628
- let effective;
629
- try {
630
- const resolved = await resolveWorkspaceConfigWithLayers({
631
- workspacePath: cwd,
632
- registry,
633
- invocationConfig
634
- });
635
- effective = resolved.effective;
636
- }
637
- catch (err) {
638
- const message = err instanceof Error ? err.message : String(err);
639
- writeError(`Config resolution failed: ${message}`);
640
- return EXIT_VALIDATION_FAILURE;
641
- }
642
- const actor = resolveActor(cwd, commandArgs, process.env);
643
- const sensitive = isSensitiveModuleCommandForEffective(subcommand, commandArgs, effective);
644
- const sessionId = resolveSessionId(process.env);
645
- const policyOp = resolvePolicyOperationIdForCommand(subcommand, effective);
646
- const explicitPolicyApproval = parsePolicyApproval(commandArgs);
647
- let resolvedSensitiveApproval = explicitPolicyApproval;
648
- if (sensitive) {
649
- if (!resolvedSensitiveApproval && policyOp) {
650
- const grant = await getSessionGrant(cwd, policyOp, sessionId);
651
- if (grant) {
652
- resolvedSensitiveApproval = { confirmed: true, rationale: grant.rationale };
653
- }
654
- }
655
- if (!resolvedSensitiveApproval) {
656
- if (policyOp) {
657
- await appendPolicyTrace(cwd, {
658
- timestamp: new Date().toISOString(),
659
- operationId: policyOp,
660
- command: `run ${subcommand}`,
661
- actor,
662
- allowed: false,
663
- message: "missing policyApproval in JSON args"
664
- });
665
- }
666
- writeLine(JSON.stringify({
667
- ok: false,
668
- code: "policy-denied",
669
- message: 'Sensitive command requires policyApproval in JSON args (or an existing session grant for this operation): {"policyApproval":{"confirmed":true,"rationale":"why","scope":"session"}}'
670
- }, null, 2));
671
- return EXIT_VALIDATION_FAILURE;
672
- }
673
- }
674
- const ctx = {
675
- runtimeVersion: "0.1",
676
- workspacePath: cwd,
677
- effectiveConfig: effective,
678
- resolvedActor: actor,
679
- moduleRegistry: registry
680
- };
681
- try {
682
- const rawResult = await router.execute(subcommand, commandArgs, ctx);
683
- if (sensitive && resolvedSensitiveApproval && policyOp) {
684
- await appendPolicyTrace(cwd, {
685
- timestamp: new Date().toISOString(),
686
- operationId: policyOp,
687
- command: `run ${subcommand}`,
688
- actor,
689
- allowed: true,
690
- rationale: resolvedSensitiveApproval.rationale,
691
- commandOk: rawResult.ok,
692
- message: rawResult.message
693
- });
694
- if (explicitPolicyApproval?.scope === "session" && rawResult.ok) {
695
- await recordSessionGrant(cwd, policyOp, sessionId, explicitPolicyApproval.rationale);
696
- }
697
- }
698
- const result = applyResponseTemplateApplication(subcommand, commandArgs, rawResult, effective);
699
- writeLine(JSON.stringify(result, null, 2));
700
- return result.ok ? EXIT_SUCCESS : EXIT_VALIDATION_FAILURE;
701
- }
702
- catch (error) {
703
- const message = error instanceof Error ? error.message : String(error);
704
- writeError(`Module command failed: ${message}`);
705
- return EXIT_INTERNAL_ERROR;
706
- }
574
+ return handleRunCommand(cwd, args, { writeLine, writeError }, {
575
+ success: EXIT_SUCCESS,
576
+ validationFailure: EXIT_VALIDATION_FAILURE,
577
+ usageError: EXIT_USAGE_ERROR,
578
+ internalError: EXIT_INTERNAL_ERROR
579
+ });
707
580
  }
708
581
  if (command !== "doctor") {
709
582
  writeError(`Unknown command '${command}'. Supported commands: init, doctor, check, upgrade, drift-check, run, config.`);
@@ -3,7 +3,7 @@ import { createInterface } from "node:readline/promises";
3
3
  import { stdin as processStdin, stdout as processStdout } from "node:process";
4
4
  import path from "node:path";
5
5
  import { ModuleRegistry } from "./module-registry.js";
6
- import { appendPolicyTrace, parsePolicyApprovalFromEnv, resolveActor } from "./policy.js";
6
+ import { appendPolicyTrace, parsePolicyApprovalFromEnv, resolveActorWithFallback } from "./policy.js";
7
7
  import { appendConfigMutation, summarizeForEvidence } from "./config-mutations.js";
8
8
  import { assertWritableKey, getConfigKeyMetadata, listConfigMetadata, validatePersistedConfigDocument, validateValueForMetadata } from "./config-metadata.js";
9
9
  import { explainConfigPath, getAtPath, getProjectConfigPath, getUserConfigFilePath, resolveWorkspaceConfigWithLayers, stableStringifyConfig } from "./workspace-kit-config.js";
@@ -114,7 +114,7 @@ async function requireConfigApproval(cwd, commandLabel, writeError) {
114
114
  timestamp: new Date().toISOString(),
115
115
  operationId: "cli.config-mutate",
116
116
  command: commandLabel,
117
- actor: resolveActor(cwd, {}, process.env),
117
+ actor: await resolveActorWithFallback(cwd, {}, process.env),
118
118
  allowed: false,
119
119
  message: "missing WORKSPACE_KIT_POLICY_APPROVAL"
120
120
  });
@@ -303,7 +303,7 @@ export async function runWorkspaceConfigCli(cwd, argv, io) {
303
303
  const prevVal = getAtPath(before, key);
304
304
  const next = setDeep(before, key, parsed);
305
305
  validatePersistedConfigDocument(next, scope === "project" ? ".workspace-kit/config.json" : "user config");
306
- const actor = resolveActor(cwd, {}, process.env);
306
+ const actor = await resolveActorWithFallback(cwd, {}, process.env);
307
307
  try {
308
308
  await writeConfigFileAtomic(fp, next);
309
309
  await appendConfigMutation(cwd, {
@@ -380,7 +380,7 @@ export async function runWorkspaceConfigCli(cwd, argv, io) {
380
380
  const prevVal = getAtPath(before, key);
381
381
  const next = unsetDeep(before, key);
382
382
  validatePersistedConfigDocument(next, scope === "project" ? ".workspace-kit/config.json" : "user config");
383
- const actor = resolveActor(cwd, {}, process.env);
383
+ const actor = await resolveActorWithFallback(cwd, {}, process.env);
384
384
  try {
385
385
  if (Object.keys(next).length === 0) {
386
386
  await fs.rm(fp, { force: true });
@@ -32,8 +32,8 @@ const REGISTRY = {
32
32
  "improvement.transcripts.sourcePath": {
33
33
  key: "improvement.transcripts.sourcePath",
34
34
  type: "string",
35
- description: "Relative path to transcript JSONL source files for sync operations.",
36
- default: ".cursor/agent-transcripts",
35
+ description: "Optional relative path to transcript JSONL source. When empty, sync uses discoveryPaths (repo-relative, then Cursor global ~/.cursor/projects/<slug>/agent-transcripts).",
36
+ default: "",
37
37
  domainScope: "project",
38
38
  owningModule: "improvement",
39
39
  sensitive: false,
@@ -136,7 +136,7 @@ const REGISTRY = {
136
136
  "improvement.transcripts.discoveryPaths": {
137
137
  key: "improvement.transcripts.discoveryPaths",
138
138
  type: "array",
139
- description: "Ordered relative paths tried when improvement.transcripts.sourcePath is unset (first existing wins).",
139
+ description: "Ordered relative paths tried when improvement.transcripts.sourcePath is unset (first existing wins). After these, sync tries Cursor global ~/.cursor/projects/<slug>/agent-transcripts.",
140
140
  default: [],
141
141
  domainScope: "project",
142
142
  owningModule: "improvement",
@@ -17,7 +17,15 @@ export type PolicyApprovalPayload = {
17
17
  };
18
18
  export declare function parsePolicyApprovalFromEnv(env: NodeJS.ProcessEnv): PolicyApprovalPayload | undefined;
19
19
  export declare function parsePolicyApproval(args: Record<string, unknown>): PolicyApprovalPayload | undefined;
20
- export declare function resolveActor(workspacePath: string, args: Record<string, unknown>, env: NodeJS.ProcessEnv): string;
20
+ export declare function resolveActor(_workspacePath: string, args: Record<string, unknown>, env: NodeJS.ProcessEnv): string;
21
+ /**
22
+ * Actor precedence:
23
+ * 1) command args.actor
24
+ * 2) WORKSPACE_KIT_ACTOR
25
+ * 3) bounded git user.email/user.name lookup (unless WORKSPACE_KIT_ACTOR_GIT_LOOKUP=off)
26
+ * 4) "unknown"
27
+ */
28
+ export declare function resolveActorWithFallback(workspacePath: string, args: Record<string, unknown>, env: NodeJS.ProcessEnv): Promise<string>;
21
29
  export type PolicyTraceRecord = {
22
30
  schemaVersion: number;
23
31
  timestamp: string;
@@ -1,6 +1,6 @@
1
1
  import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
- import { execSync } from "node:child_process";
3
+ import { execFile } from "node:child_process";
4
4
  export const POLICY_TRACE_SCHEMA_VERSION = 1;
5
5
  const COMMAND_TO_OPERATION = {
6
6
  "document-project": "doc.document-project",
@@ -83,37 +83,54 @@ export function parsePolicyApproval(args) {
83
83
  const scope = scopeRaw === "session" || scopeRaw === "once" ? scopeRaw : undefined;
84
84
  return { confirmed, rationale, ...(scope ? { scope } : {}) };
85
85
  }
86
- export function resolveActor(workspacePath, args, env) {
86
+ export function resolveActor(_workspacePath, args, env) {
87
87
  if (typeof args.actor === "string" && args.actor.trim().length > 0) {
88
88
  return args.actor.trim();
89
89
  }
90
90
  const fromEnv = env.WORKSPACE_KIT_ACTOR?.trim();
91
91
  if (fromEnv)
92
92
  return fromEnv;
93
- try {
94
- const email = execSync("git config user.email", {
95
- cwd: workspacePath,
96
- encoding: "utf8",
97
- stdio: ["ignore", "pipe", "ignore"]
98
- }).trim();
99
- if (email)
100
- return email;
101
- }
102
- catch {
103
- /* ignore */
104
- }
105
- try {
106
- const name = execSync("git config user.name", {
93
+ return "unknown";
94
+ }
95
+ function runGitConfigValue(workspacePath, key, timeoutMs) {
96
+ return new Promise((resolve) => {
97
+ execFile("git", ["config", key], {
107
98
  cwd: workspacePath,
108
99
  encoding: "utf8",
109
- stdio: ["ignore", "pipe", "ignore"]
110
- }).trim();
111
- if (name)
112
- return name;
100
+ timeout: timeoutMs,
101
+ windowsHide: true
102
+ }, (error, stdout) => {
103
+ if (error) {
104
+ resolve(undefined);
105
+ return;
106
+ }
107
+ const trimmed = stdout.trim();
108
+ resolve(trimmed.length > 0 ? trimmed : undefined);
109
+ });
110
+ });
111
+ }
112
+ /**
113
+ * Actor precedence:
114
+ * 1) command args.actor
115
+ * 2) WORKSPACE_KIT_ACTOR
116
+ * 3) bounded git user.email/user.name lookup (unless WORKSPACE_KIT_ACTOR_GIT_LOOKUP=off)
117
+ * 4) "unknown"
118
+ */
119
+ export async function resolveActorWithFallback(workspacePath, args, env) {
120
+ const explicit = resolveActor(workspacePath, args, env);
121
+ if (explicit !== "unknown") {
122
+ return explicit;
113
123
  }
114
- catch {
115
- /* ignore */
124
+ if (env.WORKSPACE_KIT_ACTOR_GIT_LOOKUP?.trim().toLowerCase() === "off") {
125
+ return "unknown";
116
126
  }
127
+ const timeoutMs = 300;
128
+ const email = await runGitConfigValue(workspacePath, "user.email", timeoutMs);
129
+ if (email)
130
+ return email;
131
+ const name = await runGitConfigValue(workspacePath, "user.name", timeoutMs);
132
+ if (name)
133
+ return name;
117
134
  return "unknown";
118
135
  }
119
136
  /** Policy sensitivity from built-in map plus `policy.extraSensitiveModuleCommands` on effective config. */
@@ -2,6 +2,7 @@ import { existsSync, mkdirSync, statSync, unlinkSync, writeFileSync } from "node
2
2
  import path from "node:path";
3
3
  import { spawn } from "node:child_process";
4
4
  const LOCK_REL = ".workspace-kit/improvement/transcript-hook.lock";
5
+ const EVENT_LOG_REL = ".workspace-kit/improvement/transcript-hook-events.jsonl";
5
6
  const LOCK_STALE_MS = 120_000;
6
7
  export function readAfterTaskCompletedHook(effective) {
7
8
  const imp = effective.improvement;
@@ -39,31 +40,62 @@ function hookLockBusy(workspacePath) {
39
40
  return false;
40
41
  }
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
+ }
42
58
  /**
43
59
  * After a task reaches `completed`, optionally spawn sync/ingest without blocking the transition (T274).
44
60
  * Ingest requires WORKSPACE_KIT_POLICY_APPROVAL in the parent env or falls back to sync.
45
61
  */
46
62
  export function maybeSpawnTranscriptHookAfterCompletion(workspacePath, effective) {
47
63
  const mode = readAfterTaskCompletedHook(effective);
48
- if (mode === "off")
64
+ if (mode === "off") {
65
+ appendHookEvent(workspacePath, "skipped", { reason: "hook-mode-off" });
49
66
  return;
50
- if (hookLockBusy(workspacePath))
67
+ }
68
+ if (hookLockBusy(workspacePath)) {
69
+ appendHookEvent(workspacePath, "skipped", { reason: "lock-busy", mode });
51
70
  return;
71
+ }
52
72
  let subcommand = "sync-transcripts";
53
73
  if (mode === "ingest" && process.env.WORKSPACE_KIT_POLICY_APPROVAL?.trim()) {
54
74
  subcommand = "ingest-transcripts";
55
75
  }
56
76
  const cli = resolveWorkspaceKitCli(workspacePath);
57
- if (!cli)
77
+ if (!cli) {
78
+ appendHookEvent(workspacePath, "failed", {
79
+ reason: "cli-not-found",
80
+ mode,
81
+ subcommand
82
+ });
58
83
  return;
84
+ }
59
85
  const fp = lockPath(workspacePath);
60
86
  try {
61
87
  mkdirSync(path.dirname(fp), { recursive: true });
62
88
  writeFileSync(fp, `${JSON.stringify({ at: new Date().toISOString(), subcommand })}\n`, "utf8");
63
89
  }
64
90
  catch {
91
+ appendHookEvent(workspacePath, "failed", {
92
+ reason: "lock-write-failed",
93
+ mode,
94
+ subcommand
95
+ });
65
96
  return;
66
97
  }
98
+ appendHookEvent(workspacePath, "started", { mode, subcommand });
67
99
  const child = spawn(process.execPath, [cli, "run", subcommand, "{}"], {
68
100
  cwd: workspacePath,
69
101
  detached: true,
@@ -78,6 +110,7 @@ export function maybeSpawnTranscriptHookAfterCompletion(workspacePath, effective
78
110
  catch {
79
111
  /* ignore */
80
112
  }
113
+ appendHookEvent(workspacePath, "completed", { mode, subcommand });
81
114
  });
82
115
  child.on("error", () => {
83
116
  try {
@@ -86,5 +119,10 @@ export function maybeSpawnTranscriptHookAfterCompletion(workspacePath, effective
86
119
  catch {
87
120
  /* ignore */
88
121
  }
122
+ appendHookEvent(workspacePath, "failed", {
123
+ reason: "spawn-error",
124
+ mode,
125
+ subcommand
126
+ });
89
127
  });
90
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>;