@workflow-cannon/workspace-kit 0.3.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/dist/cli.js +115 -3
- package/dist/contracts/index.d.ts +1 -1
- package/dist/contracts/module-contract.d.ts +14 -0
- package/dist/core/config-cli.d.ts +6 -0
- package/dist/core/config-cli.js +479 -0
- package/dist/core/config-metadata.d.ts +35 -0
- package/dist/core/config-metadata.js +162 -0
- package/dist/core/config-mutations.d.ts +18 -0
- package/dist/core/config-mutations.js +32 -0
- package/dist/core/index.d.ts +5 -0
- package/dist/core/index.js +5 -0
- package/dist/core/policy.d.ts +34 -0
- package/dist/core/policy.js +135 -0
- package/dist/core/workspace-kit-config.d.ts +57 -0
- package/dist/core/workspace-kit-config.js +266 -0
- package/dist/modules/index.d.ts +1 -0
- package/dist/modules/index.js +1 -0
- package/dist/modules/task-engine/index.js +15 -3
- package/dist/modules/workspace-config/index.d.ts +2 -0
- package/dist/modules/workspace-config/index.js +100 -0
- package/package.json +1 -1
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export declare const CONFIG_MUTATIONS_SCHEMA_VERSION: 1;
|
|
2
|
+
export type ConfigMutationRecord = {
|
|
3
|
+
schemaVersion: typeof CONFIG_MUTATIONS_SCHEMA_VERSION;
|
|
4
|
+
timestamp: string;
|
|
5
|
+
actor: string;
|
|
6
|
+
key: string;
|
|
7
|
+
layer: "project" | "user";
|
|
8
|
+
operation: "set" | "unset";
|
|
9
|
+
ok: boolean;
|
|
10
|
+
code?: string;
|
|
11
|
+
message?: string;
|
|
12
|
+
previousSummary?: string;
|
|
13
|
+
nextSummary?: string;
|
|
14
|
+
};
|
|
15
|
+
export declare function appendConfigMutation(workspacePath: string, record: Omit<ConfigMutationRecord, "schemaVersion"> & {
|
|
16
|
+
schemaVersion?: number;
|
|
17
|
+
}): Promise<void>;
|
|
18
|
+
export declare function summarizeForEvidence(key: string, sensitive: boolean, value: unknown): string;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
export const CONFIG_MUTATIONS_SCHEMA_VERSION = 1;
|
|
4
|
+
const CONFIG_DIR = ".workspace-kit/config";
|
|
5
|
+
const MUTATIONS_FILE = "mutations.jsonl";
|
|
6
|
+
function summarizeValue(key, value) {
|
|
7
|
+
if (value === undefined)
|
|
8
|
+
return "(unset)";
|
|
9
|
+
if (key === "policy.extraSensitiveModuleCommands" && Array.isArray(value)) {
|
|
10
|
+
return `array(len=${value.length})`;
|
|
11
|
+
}
|
|
12
|
+
if (typeof value === "string") {
|
|
13
|
+
return `string(len=${value.length})`;
|
|
14
|
+
}
|
|
15
|
+
return typeof value;
|
|
16
|
+
}
|
|
17
|
+
export async function appendConfigMutation(workspacePath, record) {
|
|
18
|
+
const dir = path.join(workspacePath, CONFIG_DIR);
|
|
19
|
+
const fp = path.join(dir, MUTATIONS_FILE);
|
|
20
|
+
const line = `${JSON.stringify({
|
|
21
|
+
schemaVersion: CONFIG_MUTATIONS_SCHEMA_VERSION,
|
|
22
|
+
...record
|
|
23
|
+
})}\n`;
|
|
24
|
+
await fs.mkdir(dir, { recursive: true });
|
|
25
|
+
await fs.appendFile(fp, line, "utf8");
|
|
26
|
+
}
|
|
27
|
+
export function summarizeForEvidence(key, sensitive, value) {
|
|
28
|
+
if (sensitive) {
|
|
29
|
+
return value === undefined ? "(unset)" : "(redacted)";
|
|
30
|
+
}
|
|
31
|
+
return summarizeValue(key, value);
|
|
32
|
+
}
|
package/dist/core/index.d.ts
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
1
|
export { ModuleRegistry, ModuleRegistryError, validateModuleSet, type ModuleRegistryOptions } from "./module-registry.js";
|
|
2
2
|
export { ModuleCommandRouter, ModuleCommandRouterError, type ModuleCommandDescriptor, type ModuleCommandRouterOptions } from "./module-command-router.js";
|
|
3
|
+
export { buildBaseConfigLayers, deepMerge, envToConfigOverlay, explainConfigPath, getAtPath, getProjectConfigPath, getUserConfigFilePath, KIT_CONFIG_DEFAULTS, loadUserLayer, mergeConfigLayers, MODULE_CONFIG_CONTRIBUTIONS, normalizeConfigForExport, PROJECT_CONFIG_REL, resolveWorkspaceConfigWithLayers, stableStringifyConfig, type ConfigLayer, type ConfigLayerId, type EffectiveWorkspaceConfig, type ExplainConfigResult, type ResolveWorkspaceConfigOptions } from "./workspace-kit-config.js";
|
|
4
|
+
export { appendPolicyTrace, getExtraSensitiveModuleCommandsFromEffective, getOperationIdForCommand, isSensitiveModuleCommand, isSensitiveModuleCommandForEffective, parsePolicyApproval, parsePolicyApprovalFromEnv, POLICY_TRACE_SCHEMA_VERSION, resolveActor, resolvePolicyOperationIdForCommand, type PolicyOperationId, type PolicyTraceRecord, type PolicyTraceRecordInput } from "./policy.js";
|
|
5
|
+
export { assertWritableKey, getConfigKeyMetadata, getConfigRegistryExport, listConfigMetadata, validatePersistedConfigDocument, validateValueForMetadata, type ConfigKeyExposure, type ConfigKeyMetadata, type ConfigValueType } from "./config-metadata.js";
|
|
6
|
+
export { appendConfigMutation, CONFIG_MUTATIONS_SCHEMA_VERSION, summarizeForEvidence, type ConfigMutationRecord } from "./config-mutations.js";
|
|
7
|
+
export { generateConfigReferenceDocs, runWorkspaceConfigCli, type ConfigCliIo } from "./config-cli.js";
|
|
3
8
|
export type CoreRuntimeVersion = "0.1";
|
package/dist/core/index.js
CHANGED
|
@@ -1,2 +1,7 @@
|
|
|
1
1
|
export { ModuleRegistry, ModuleRegistryError, validateModuleSet } from "./module-registry.js";
|
|
2
2
|
export { ModuleCommandRouter, ModuleCommandRouterError } from "./module-command-router.js";
|
|
3
|
+
export { buildBaseConfigLayers, deepMerge, envToConfigOverlay, explainConfigPath, getAtPath, getProjectConfigPath, getUserConfigFilePath, KIT_CONFIG_DEFAULTS, loadUserLayer, mergeConfigLayers, MODULE_CONFIG_CONTRIBUTIONS, normalizeConfigForExport, PROJECT_CONFIG_REL, resolveWorkspaceConfigWithLayers, stableStringifyConfig } from "./workspace-kit-config.js";
|
|
4
|
+
export { appendPolicyTrace, getExtraSensitiveModuleCommandsFromEffective, getOperationIdForCommand, isSensitiveModuleCommand, isSensitiveModuleCommandForEffective, parsePolicyApproval, parsePolicyApprovalFromEnv, POLICY_TRACE_SCHEMA_VERSION, resolveActor, resolvePolicyOperationIdForCommand } from "./policy.js";
|
|
5
|
+
export { assertWritableKey, getConfigKeyMetadata, getConfigRegistryExport, listConfigMetadata, validatePersistedConfigDocument, validateValueForMetadata } from "./config-metadata.js";
|
|
6
|
+
export { appendConfigMutation, CONFIG_MUTATIONS_SCHEMA_VERSION, summarizeForEvidence } from "./config-mutations.js";
|
|
7
|
+
export { generateConfigReferenceDocs, runWorkspaceConfigCli } from "./config-cli.js";
|
|
@@ -0,0 +1,34 @@
|
|
|
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";
|
|
3
|
+
export declare function getOperationIdForCommand(commandName: string): PolicyOperationId | undefined;
|
|
4
|
+
export declare function getExtraSensitiveModuleCommandsFromEffective(effective: Record<string, unknown>): string[];
|
|
5
|
+
/** Resolve operation id for tracing, including config-declared sensitive module commands. */
|
|
6
|
+
export declare function resolvePolicyOperationIdForCommand(commandName: string, effective: Record<string, unknown>): PolicyOperationId | undefined;
|
|
7
|
+
/**
|
|
8
|
+
* Sensitive when mutation / write is possible. Documentation commands are exempt when dryRun is true.
|
|
9
|
+
*/
|
|
10
|
+
export declare function isSensitiveModuleCommand(commandName: string, args: Record<string, unknown>): boolean;
|
|
11
|
+
export type PolicyApprovalPayload = {
|
|
12
|
+
confirmed: boolean;
|
|
13
|
+
rationale: string;
|
|
14
|
+
};
|
|
15
|
+
export declare function parsePolicyApprovalFromEnv(env: NodeJS.ProcessEnv): PolicyApprovalPayload | undefined;
|
|
16
|
+
export declare function parsePolicyApproval(args: Record<string, unknown>): PolicyApprovalPayload | undefined;
|
|
17
|
+
export declare function resolveActor(workspacePath: string, args: Record<string, unknown>, env: NodeJS.ProcessEnv): string;
|
|
18
|
+
export type PolicyTraceRecord = {
|
|
19
|
+
schemaVersion: number;
|
|
20
|
+
timestamp: string;
|
|
21
|
+
operationId: PolicyOperationId;
|
|
22
|
+
command: string;
|
|
23
|
+
actor: string;
|
|
24
|
+
allowed: boolean;
|
|
25
|
+
rationale?: string;
|
|
26
|
+
commandOk?: boolean;
|
|
27
|
+
message?: string;
|
|
28
|
+
};
|
|
29
|
+
/** Policy sensitivity from built-in map plus `policy.extraSensitiveModuleCommands` on effective config. */
|
|
30
|
+
export declare function isSensitiveModuleCommandForEffective(commandName: string, args: Record<string, unknown>, effective: Record<string, unknown>): boolean;
|
|
31
|
+
export type PolicyTraceRecordInput = Omit<PolicyTraceRecord, "schemaVersion"> & {
|
|
32
|
+
schemaVersion?: number;
|
|
33
|
+
};
|
|
34
|
+
export declare function appendPolicyTrace(workspacePath: string, record: PolicyTraceRecordInput): Promise<void>;
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { execSync } from "node:child_process";
|
|
4
|
+
export const POLICY_TRACE_SCHEMA_VERSION = 1;
|
|
5
|
+
const COMMAND_TO_OPERATION = {
|
|
6
|
+
"document-project": "doc.document-project",
|
|
7
|
+
"generate-document": "doc.generate-document",
|
|
8
|
+
"import-tasks": "tasks.import-tasks",
|
|
9
|
+
"generate-tasks-md": "tasks.generate-tasks-md",
|
|
10
|
+
"run-transition": "tasks.run-transition"
|
|
11
|
+
};
|
|
12
|
+
export function getOperationIdForCommand(commandName) {
|
|
13
|
+
return COMMAND_TO_OPERATION[commandName];
|
|
14
|
+
}
|
|
15
|
+
export function getExtraSensitiveModuleCommandsFromEffective(effective) {
|
|
16
|
+
const policy = effective.policy;
|
|
17
|
+
if (!policy || typeof policy !== "object" || Array.isArray(policy)) {
|
|
18
|
+
return [];
|
|
19
|
+
}
|
|
20
|
+
const raw = policy.extraSensitiveModuleCommands;
|
|
21
|
+
if (!Array.isArray(raw)) {
|
|
22
|
+
return [];
|
|
23
|
+
}
|
|
24
|
+
return raw.filter((x) => typeof x === "string" && x.trim().length > 0);
|
|
25
|
+
}
|
|
26
|
+
/** Resolve operation id for tracing, including config-declared sensitive module commands. */
|
|
27
|
+
export function resolvePolicyOperationIdForCommand(commandName, effective) {
|
|
28
|
+
const builtin = getOperationIdForCommand(commandName);
|
|
29
|
+
if (builtin)
|
|
30
|
+
return builtin;
|
|
31
|
+
if (getExtraSensitiveModuleCommandsFromEffective(effective).includes(commandName)) {
|
|
32
|
+
return "policy.dynamic-sensitive";
|
|
33
|
+
}
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Sensitive when mutation / write is possible. Documentation commands are exempt when dryRun is true.
|
|
38
|
+
*/
|
|
39
|
+
export function isSensitiveModuleCommand(commandName, args) {
|
|
40
|
+
const op = COMMAND_TO_OPERATION[commandName];
|
|
41
|
+
if (!op)
|
|
42
|
+
return false;
|
|
43
|
+
if (op === "doc.document-project" || op === "doc.generate-document") {
|
|
44
|
+
const options = typeof args.options === "object" && args.options !== null
|
|
45
|
+
? args.options
|
|
46
|
+
: {};
|
|
47
|
+
if (options.dryRun === true) {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
export function parsePolicyApprovalFromEnv(env) {
|
|
54
|
+
const raw = env.WORKSPACE_KIT_POLICY_APPROVAL?.trim();
|
|
55
|
+
if (!raw)
|
|
56
|
+
return undefined;
|
|
57
|
+
try {
|
|
58
|
+
const o = JSON.parse(raw);
|
|
59
|
+
if (o.confirmed !== true)
|
|
60
|
+
return undefined;
|
|
61
|
+
const rationale = typeof o.rationale === "string" ? o.rationale.trim() : "";
|
|
62
|
+
if (!rationale)
|
|
63
|
+
return undefined;
|
|
64
|
+
return { confirmed: true, rationale };
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
export function parsePolicyApproval(args) {
|
|
71
|
+
const raw = args.policyApproval;
|
|
72
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
const o = raw;
|
|
76
|
+
const confirmed = o.confirmed === true;
|
|
77
|
+
const rationale = typeof o.rationale === "string" ? o.rationale.trim() : "";
|
|
78
|
+
if (!confirmed || rationale.length === 0) {
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
return { confirmed, rationale };
|
|
82
|
+
}
|
|
83
|
+
export function resolveActor(workspacePath, args, env) {
|
|
84
|
+
if (typeof args.actor === "string" && args.actor.trim().length > 0) {
|
|
85
|
+
return args.actor.trim();
|
|
86
|
+
}
|
|
87
|
+
const fromEnv = env.WORKSPACE_KIT_ACTOR?.trim();
|
|
88
|
+
if (fromEnv)
|
|
89
|
+
return fromEnv;
|
|
90
|
+
try {
|
|
91
|
+
const email = execSync("git config user.email", {
|
|
92
|
+
cwd: workspacePath,
|
|
93
|
+
encoding: "utf8",
|
|
94
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
95
|
+
}).trim();
|
|
96
|
+
if (email)
|
|
97
|
+
return email;
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
/* ignore */
|
|
101
|
+
}
|
|
102
|
+
try {
|
|
103
|
+
const name = execSync("git config user.name", {
|
|
104
|
+
cwd: workspacePath,
|
|
105
|
+
encoding: "utf8",
|
|
106
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
107
|
+
}).trim();
|
|
108
|
+
if (name)
|
|
109
|
+
return name;
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
/* ignore */
|
|
113
|
+
}
|
|
114
|
+
return "unknown";
|
|
115
|
+
}
|
|
116
|
+
/** Policy sensitivity from built-in map plus `policy.extraSensitiveModuleCommands` on effective config. */
|
|
117
|
+
export function isSensitiveModuleCommandForEffective(commandName, args, effective) {
|
|
118
|
+
if (isSensitiveModuleCommand(commandName, args)) {
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
return getExtraSensitiveModuleCommandsFromEffective(effective).includes(commandName);
|
|
122
|
+
}
|
|
123
|
+
const POLICY_DIR = ".workspace-kit/policy";
|
|
124
|
+
const TRACE_FILE = "traces.jsonl";
|
|
125
|
+
export async function appendPolicyTrace(workspacePath, record) {
|
|
126
|
+
const dir = path.join(workspacePath, POLICY_DIR);
|
|
127
|
+
const fp = path.join(workspacePath, POLICY_DIR, TRACE_FILE);
|
|
128
|
+
const full = {
|
|
129
|
+
...record,
|
|
130
|
+
schemaVersion: record.schemaVersion ?? POLICY_TRACE_SCHEMA_VERSION
|
|
131
|
+
};
|
|
132
|
+
const line = `${JSON.stringify(full)}\n`;
|
|
133
|
+
await fs.mkdir(dir, { recursive: true });
|
|
134
|
+
await fs.appendFile(fp, line, "utf8");
|
|
135
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { ConfigRegistryView } from "../contracts/module-contract.js";
|
|
2
|
+
export type ConfigLayerId = "kit-default" | `module:${string}` | "user" | "project" | "env" | "invocation";
|
|
3
|
+
export type ConfigLayer = {
|
|
4
|
+
id: ConfigLayerId;
|
|
5
|
+
data: Record<string, unknown>;
|
|
6
|
+
};
|
|
7
|
+
/** Effective workspace config: domain keys + optional modules map (project file). */
|
|
8
|
+
export type EffectiveWorkspaceConfig = Record<string, unknown>;
|
|
9
|
+
export declare const PROJECT_CONFIG_REL = ".workspace-kit/config.json";
|
|
10
|
+
export declare function getProjectConfigPath(workspacePath: string): string;
|
|
11
|
+
/** Built-in defaults (lowest layer). */
|
|
12
|
+
export declare const KIT_CONFIG_DEFAULTS: Record<string, unknown>;
|
|
13
|
+
/**
|
|
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.
|
|
16
|
+
*/
|
|
17
|
+
export declare const MODULE_CONFIG_CONTRIBUTIONS: Record<string, Record<string, unknown>>;
|
|
18
|
+
export declare function deepMerge(target: Record<string, unknown>, source: Record<string, unknown>): Record<string, unknown>;
|
|
19
|
+
export declare function getAtPath(root: Record<string, unknown>, dotted: string): unknown;
|
|
20
|
+
export declare function deepEqual(a: unknown, b: unknown): boolean;
|
|
21
|
+
/** Resolved home for user-level config (`~/.workspace-kit/config.json`). Override with `WORKSPACE_KIT_HOME` (tests). */
|
|
22
|
+
export declare function getUserConfigFilePath(): string;
|
|
23
|
+
export declare function loadUserLayer(): Promise<ConfigLayer>;
|
|
24
|
+
/**
|
|
25
|
+
* Parse WORKSPACE_KIT_* env into a nested object (double-underscore → path under domains).
|
|
26
|
+
* Example: WORKSPACE_KIT_TASKS__STORE_PATH -> { tasks: { storeRelativePath: "..." } }
|
|
27
|
+
* Uses segment after prefix; known domain prefix sets first key.
|
|
28
|
+
*/
|
|
29
|
+
export declare function envToConfigOverlay(env: NodeJS.ProcessEnv): Record<string, unknown>;
|
|
30
|
+
/** Kit defaults + module contributions (topological order). Project/env/invocation added separately. */
|
|
31
|
+
export declare function buildBaseConfigLayers(registry: ConfigRegistryView): ConfigLayer[];
|
|
32
|
+
export declare function loadProjectLayer(workspacePath: string): Promise<ConfigLayer>;
|
|
33
|
+
export declare function mergeConfigLayers(layers: ConfigLayer[]): Record<string, unknown>;
|
|
34
|
+
export type ResolveWorkspaceConfigOptions = {
|
|
35
|
+
workspacePath: string;
|
|
36
|
+
registry: ConfigRegistryView;
|
|
37
|
+
env?: NodeJS.ProcessEnv;
|
|
38
|
+
/** Merged last (from `workspace-kit run` top-level `config` key). */
|
|
39
|
+
invocationConfig?: Record<string, unknown>;
|
|
40
|
+
};
|
|
41
|
+
export declare function resolveWorkspaceConfigWithLayers(options: ResolveWorkspaceConfigOptions): Promise<{
|
|
42
|
+
effective: EffectiveWorkspaceConfig;
|
|
43
|
+
layers: ConfigLayer[];
|
|
44
|
+
}>;
|
|
45
|
+
export declare function normalizeConfigForExport(value: unknown): unknown;
|
|
46
|
+
/** Deterministic JSON for agents and tests (sorted keys, trailing newline). */
|
|
47
|
+
export declare function stableStringifyConfig(value: unknown): string;
|
|
48
|
+
export type ExplainConfigResult = {
|
|
49
|
+
path: string;
|
|
50
|
+
effectiveValue: unknown;
|
|
51
|
+
winningLayer: ConfigLayerId;
|
|
52
|
+
alternates: {
|
|
53
|
+
layer: ConfigLayerId;
|
|
54
|
+
value: unknown;
|
|
55
|
+
}[];
|
|
56
|
+
};
|
|
57
|
+
export declare function explainConfigPath(dottedPath: string, layers: ConfigLayer[]): ExplainConfigResult;
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { validatePersistedConfigDocument } from "./config-metadata.js";
|
|
6
|
+
export const PROJECT_CONFIG_REL = ".workspace-kit/config.json";
|
|
7
|
+
export function getProjectConfigPath(workspacePath) {
|
|
8
|
+
return path.join(workspacePath, PROJECT_CONFIG_REL);
|
|
9
|
+
}
|
|
10
|
+
/** Built-in defaults (lowest layer). */
|
|
11
|
+
export const KIT_CONFIG_DEFAULTS = {
|
|
12
|
+
core: {},
|
|
13
|
+
tasks: {
|
|
14
|
+
storeRelativePath: ".workspace-kit/tasks/state.json"
|
|
15
|
+
},
|
|
16
|
+
documentation: {}
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Static module-level defaults keyed by module id (merged in registry startup order).
|
|
20
|
+
* Each value is deep-merged into the aggregate before project/env/invocation.
|
|
21
|
+
*/
|
|
22
|
+
export const MODULE_CONFIG_CONTRIBUTIONS = {
|
|
23
|
+
"task-engine": {
|
|
24
|
+
tasks: {
|
|
25
|
+
storeRelativePath: ".workspace-kit/tasks/state.json"
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
documentation: {
|
|
29
|
+
documentation: {}
|
|
30
|
+
},
|
|
31
|
+
approvals: {},
|
|
32
|
+
planning: {},
|
|
33
|
+
improvement: {}
|
|
34
|
+
};
|
|
35
|
+
export function deepMerge(target, source) {
|
|
36
|
+
const out = { ...target };
|
|
37
|
+
for (const [k, v] of Object.entries(source)) {
|
|
38
|
+
if (v !== null && typeof v === "object" && !Array.isArray(v)) {
|
|
39
|
+
const prev = out[k];
|
|
40
|
+
const prevObj = prev !== null && typeof prev === "object" && !Array.isArray(prev)
|
|
41
|
+
? prev
|
|
42
|
+
: {};
|
|
43
|
+
out[k] = deepMerge(prevObj, v);
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
out[k] = v;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return out;
|
|
50
|
+
}
|
|
51
|
+
function cloneDeep(obj) {
|
|
52
|
+
return JSON.parse(JSON.stringify(obj));
|
|
53
|
+
}
|
|
54
|
+
export function getAtPath(root, dotted) {
|
|
55
|
+
const parts = dotted.split(".").filter(Boolean);
|
|
56
|
+
let cur = root;
|
|
57
|
+
for (const p of parts) {
|
|
58
|
+
if (cur === null || cur === undefined || typeof cur !== "object" || Array.isArray(cur)) {
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
cur = cur[p];
|
|
62
|
+
}
|
|
63
|
+
return cur;
|
|
64
|
+
}
|
|
65
|
+
export function deepEqual(a, b) {
|
|
66
|
+
if (a === b)
|
|
67
|
+
return true;
|
|
68
|
+
if (a === null || b === null || typeof a !== "object" || typeof b !== "object") {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
if (Array.isArray(a) !== Array.isArray(b))
|
|
72
|
+
return false;
|
|
73
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
74
|
+
if (a.length !== b.length)
|
|
75
|
+
return false;
|
|
76
|
+
return a.every((v, i) => deepEqual(v, b[i]));
|
|
77
|
+
}
|
|
78
|
+
const ak = Object.keys(a).sort();
|
|
79
|
+
const bk = Object.keys(b).sort();
|
|
80
|
+
if (ak.length !== bk.length)
|
|
81
|
+
return false;
|
|
82
|
+
for (let i = 0; i < ak.length; i++) {
|
|
83
|
+
if (ak[i] !== bk[i])
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
for (const k of ak) {
|
|
87
|
+
if (!deepEqual(a[k], b[k])) {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
/** Resolved home for user-level config (`~/.workspace-kit/config.json`). Override with `WORKSPACE_KIT_HOME` (tests). */
|
|
94
|
+
export function getUserConfigFilePath() {
|
|
95
|
+
const home = process.env.WORKSPACE_KIT_HOME?.trim() || os.homedir();
|
|
96
|
+
return path.join(home, ".workspace-kit", "config.json");
|
|
97
|
+
}
|
|
98
|
+
async function readUserConfigFile() {
|
|
99
|
+
const fp = getUserConfigFilePath();
|
|
100
|
+
if (!existsSync(fp)) {
|
|
101
|
+
return {};
|
|
102
|
+
}
|
|
103
|
+
const raw = await fs.readFile(fp, "utf8");
|
|
104
|
+
const parsed = JSON.parse(raw);
|
|
105
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
106
|
+
throw new Error(`config-invalid(user): ${fp} must be a JSON object`);
|
|
107
|
+
}
|
|
108
|
+
const obj = parsed;
|
|
109
|
+
validatePersistedConfigDocument(obj, "user config");
|
|
110
|
+
return obj;
|
|
111
|
+
}
|
|
112
|
+
async function readProjectConfigFile(workspacePath) {
|
|
113
|
+
const fp = path.join(workspacePath, PROJECT_CONFIG_REL);
|
|
114
|
+
if (!existsSync(fp)) {
|
|
115
|
+
return {};
|
|
116
|
+
}
|
|
117
|
+
const raw = await fs.readFile(fp, "utf8");
|
|
118
|
+
const parsed = JSON.parse(raw);
|
|
119
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
120
|
+
throw new Error("config-invalid: .workspace-kit/config.json must be a JSON object");
|
|
121
|
+
}
|
|
122
|
+
const obj = parsed;
|
|
123
|
+
validatePersistedConfigDocument(obj, ".workspace-kit/config.json");
|
|
124
|
+
return obj;
|
|
125
|
+
}
|
|
126
|
+
export async function loadUserLayer() {
|
|
127
|
+
const data = await readUserConfigFile();
|
|
128
|
+
return { id: "user", data };
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Parse WORKSPACE_KIT_* env into a nested object (double-underscore → path under domains).
|
|
132
|
+
* Example: WORKSPACE_KIT_TASKS__STORE_PATH -> { tasks: { storeRelativePath: "..." } }
|
|
133
|
+
* Uses segment after prefix; known domain prefix sets first key.
|
|
134
|
+
*/
|
|
135
|
+
export function envToConfigOverlay(env) {
|
|
136
|
+
const prefix = "WORKSPACE_KIT_";
|
|
137
|
+
const out = {};
|
|
138
|
+
for (const [key, val] of Object.entries(env)) {
|
|
139
|
+
if (!key.startsWith(prefix) || val === undefined)
|
|
140
|
+
continue;
|
|
141
|
+
const rest = key.slice(prefix.length);
|
|
142
|
+
if (!rest || rest === "ACTOR")
|
|
143
|
+
continue;
|
|
144
|
+
const segments = rest.split("__").map((s) => camelCaseEnvSegment(s));
|
|
145
|
+
if (segments.length === 0)
|
|
146
|
+
continue;
|
|
147
|
+
setDeep(out, segments, coerceEnvValue(val));
|
|
148
|
+
}
|
|
149
|
+
return out;
|
|
150
|
+
}
|
|
151
|
+
function camelCaseEnvSegment(s) {
|
|
152
|
+
const lower = s.toLowerCase().replace(/_/g, "");
|
|
153
|
+
// tasks -> tasks; STORE_PATH segments already split — first segment may be TASKS
|
|
154
|
+
if (lower === "tasks")
|
|
155
|
+
return "tasks";
|
|
156
|
+
if (lower === "documentation")
|
|
157
|
+
return "documentation";
|
|
158
|
+
if (lower === "core")
|
|
159
|
+
return "core";
|
|
160
|
+
// e.g. STORE_PATH -> storePath
|
|
161
|
+
return s
|
|
162
|
+
.toLowerCase()
|
|
163
|
+
.split("_")
|
|
164
|
+
.filter(Boolean)
|
|
165
|
+
.map((w, i) => (i === 0 ? w : w.charAt(0).toUpperCase() + w.slice(1)))
|
|
166
|
+
.join("");
|
|
167
|
+
}
|
|
168
|
+
function setDeep(target, segments, value) {
|
|
169
|
+
let cur = target;
|
|
170
|
+
for (let i = 0; i < segments.length - 1; i++) {
|
|
171
|
+
const seg = segments[i];
|
|
172
|
+
const next = cur[seg];
|
|
173
|
+
if (next === undefined || typeof next !== "object" || Array.isArray(next)) {
|
|
174
|
+
cur[seg] = {};
|
|
175
|
+
}
|
|
176
|
+
cur = cur[seg];
|
|
177
|
+
}
|
|
178
|
+
cur[segments[segments.length - 1]] = value;
|
|
179
|
+
}
|
|
180
|
+
function coerceEnvValue(val) {
|
|
181
|
+
if (val === "true")
|
|
182
|
+
return true;
|
|
183
|
+
if (val === "false")
|
|
184
|
+
return false;
|
|
185
|
+
if (/^-?\d+$/.test(val))
|
|
186
|
+
return Number(val);
|
|
187
|
+
return val;
|
|
188
|
+
}
|
|
189
|
+
/** Kit defaults + module contributions (topological order). Project/env/invocation added separately. */
|
|
190
|
+
export function buildBaseConfigLayers(registry) {
|
|
191
|
+
const layers = [];
|
|
192
|
+
layers.push({ id: "kit-default", data: cloneDeep(KIT_CONFIG_DEFAULTS) });
|
|
193
|
+
for (const mod of registry.getStartupOrder()) {
|
|
194
|
+
const contrib = MODULE_CONFIG_CONTRIBUTIONS[mod.registration.id];
|
|
195
|
+
if (contrib && Object.keys(contrib).length > 0) {
|
|
196
|
+
layers.push({ id: `module:${mod.registration.id}`, data: cloneDeep(contrib) });
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return layers;
|
|
200
|
+
}
|
|
201
|
+
export async function loadProjectLayer(workspacePath) {
|
|
202
|
+
const data = await readProjectConfigFile(workspacePath);
|
|
203
|
+
return { id: "project", data };
|
|
204
|
+
}
|
|
205
|
+
export function mergeConfigLayers(layers) {
|
|
206
|
+
let acc = {};
|
|
207
|
+
for (const layer of layers) {
|
|
208
|
+
acc = deepMerge(acc, layer.data);
|
|
209
|
+
}
|
|
210
|
+
return acc;
|
|
211
|
+
}
|
|
212
|
+
export async function resolveWorkspaceConfigWithLayers(options) {
|
|
213
|
+
const { workspacePath, registry, env = process.env, invocationConfig } = options;
|
|
214
|
+
const layers = [...buildBaseConfigLayers(registry)];
|
|
215
|
+
layers.push(await loadUserLayer());
|
|
216
|
+
layers.push(await loadProjectLayer(workspacePath));
|
|
217
|
+
layers.push({ id: "env", data: envToConfigOverlay(env) });
|
|
218
|
+
if (invocationConfig && Object.keys(invocationConfig).length > 0) {
|
|
219
|
+
layers.push({ id: "invocation", data: cloneDeep(invocationConfig) });
|
|
220
|
+
}
|
|
221
|
+
return { effective: mergeConfigLayers(layers), layers };
|
|
222
|
+
}
|
|
223
|
+
export function normalizeConfigForExport(value) {
|
|
224
|
+
return sortKeysDeep(value);
|
|
225
|
+
}
|
|
226
|
+
function sortKeysDeep(value) {
|
|
227
|
+
if (value === null || typeof value !== "object") {
|
|
228
|
+
return value;
|
|
229
|
+
}
|
|
230
|
+
if (Array.isArray(value)) {
|
|
231
|
+
return value.map(sortKeysDeep);
|
|
232
|
+
}
|
|
233
|
+
const o = value;
|
|
234
|
+
const out = {};
|
|
235
|
+
for (const k of Object.keys(o).sort()) {
|
|
236
|
+
out[k] = sortKeysDeep(o[k]);
|
|
237
|
+
}
|
|
238
|
+
return out;
|
|
239
|
+
}
|
|
240
|
+
/** Deterministic JSON for agents and tests (sorted keys, trailing newline). */
|
|
241
|
+
export function stableStringifyConfig(value) {
|
|
242
|
+
return `${JSON.stringify(sortKeysDeep(value), null, 2)}\n`;
|
|
243
|
+
}
|
|
244
|
+
export function explainConfigPath(dottedPath, layers) {
|
|
245
|
+
const mergedFull = mergeConfigLayers(layers);
|
|
246
|
+
const effectiveValue = getAtPath(mergedFull, dottedPath);
|
|
247
|
+
let winningLayer = "kit-default";
|
|
248
|
+
let prevMerged = {};
|
|
249
|
+
for (let i = 0; i < layers.length; i++) {
|
|
250
|
+
const slice = layers.slice(0, i + 1);
|
|
251
|
+
const nextMerged = mergeConfigLayers(slice);
|
|
252
|
+
const prevVal = getAtPath(prevMerged, dottedPath);
|
|
253
|
+
const nextVal = getAtPath(nextMerged, dottedPath);
|
|
254
|
+
if (!deepEqual(prevVal, nextVal)) {
|
|
255
|
+
winningLayer = layers[i].id;
|
|
256
|
+
}
|
|
257
|
+
prevMerged = nextMerged;
|
|
258
|
+
}
|
|
259
|
+
const alternates = [];
|
|
260
|
+
for (let i = 0; i < layers.length; i++) {
|
|
261
|
+
const slice = layers.slice(0, i + 1);
|
|
262
|
+
const m = mergeConfigLayers(slice);
|
|
263
|
+
alternates.push({ layer: layers[i].id, value: getAtPath(m, dottedPath) });
|
|
264
|
+
}
|
|
265
|
+
return { path: dottedPath, effectiveValue, winningLayer, alternates };
|
|
266
|
+
}
|
package/dist/modules/index.d.ts
CHANGED
|
@@ -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 { 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";
|
|
7
8
|
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
|
@@ -1,5 +1,6 @@
|
|
|
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 { workspaceConfigModule } from "./workspace-config/index.js";
|
|
4
5
|
export { planningModule } from "./planning/index.js";
|
|
5
6
|
export { taskEngineModule, TaskStore, TransitionService, TaskEngineError, TransitionValidator, isTransitionAllowed, getTransitionAction, resolveTargetState, getAllowedTransitionsFrom, stateValidityGuard, dependencyCheckGuard, generateTasksMd, importTasksFromMarkdown, getNextActions } from "./task-engine/index.js";
|
|
@@ -12,10 +12,18 @@ export { TaskEngineError, TransitionValidator, isTransitionAllowed, getTransitio
|
|
|
12
12
|
export { generateTasksMd } from "./generator.js";
|
|
13
13
|
export { importTasksFromMarkdown } from "./importer.js";
|
|
14
14
|
export { getNextActions } from "./suggestions.js";
|
|
15
|
+
function taskStorePath(ctx) {
|
|
16
|
+
const tasks = ctx.effectiveConfig?.tasks;
|
|
17
|
+
if (!tasks || typeof tasks !== "object" || Array.isArray(tasks)) {
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
const p = tasks.storeRelativePath;
|
|
21
|
+
return typeof p === "string" && p.trim().length > 0 ? p.trim() : undefined;
|
|
22
|
+
}
|
|
15
23
|
export const taskEngineModule = {
|
|
16
24
|
registration: {
|
|
17
25
|
id: "task-engine",
|
|
18
|
-
version: "0.
|
|
26
|
+
version: "0.4.0",
|
|
19
27
|
contractVersion: "1",
|
|
20
28
|
capabilities: ["task-engine"],
|
|
21
29
|
dependsOn: [],
|
|
@@ -73,7 +81,7 @@ export const taskEngineModule = {
|
|
|
73
81
|
},
|
|
74
82
|
async onCommand(command, ctx) {
|
|
75
83
|
const args = command.args ?? {};
|
|
76
|
-
const store = new TaskStore(ctx.workspacePath);
|
|
84
|
+
const store = new TaskStore(ctx.workspacePath, taskStorePath(ctx));
|
|
77
85
|
try {
|
|
78
86
|
await store.load();
|
|
79
87
|
}
|
|
@@ -90,7 +98,11 @@ export const taskEngineModule = {
|
|
|
90
98
|
if (command.name === "run-transition") {
|
|
91
99
|
const taskId = typeof args.taskId === "string" ? args.taskId : undefined;
|
|
92
100
|
const action = typeof args.action === "string" ? args.action : undefined;
|
|
93
|
-
const actor = typeof args.actor === "string"
|
|
101
|
+
const actor = typeof args.actor === "string"
|
|
102
|
+
? args.actor
|
|
103
|
+
: ctx.resolvedActor !== undefined
|
|
104
|
+
? ctx.resolvedActor
|
|
105
|
+
: undefined;
|
|
94
106
|
if (!taskId || !action) {
|
|
95
107
|
return {
|
|
96
108
|
ok: false,
|