@workflow-cannon/workspace-kit 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/dist/cli.js +109 -1
- package/dist/contracts/index.d.ts +1 -1
- package/dist/contracts/module-contract.d.ts +14 -0
- package/dist/core/index.d.ts +2 -0
- package/dist/core/index.js +2 -0
- package/dist/core/policy.d.ts +24 -0
- package/dist/core/policy.js +102 -0
- package/dist/core/workspace-kit-config.d.ts +49 -0
- package/dist/core/workspace-kit-config.js +218 -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 +72 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -61,9 +61,9 @@ This keeps automation adaptive without sacrificing safety, governance, or develo
|
|
|
61
61
|
|
|
62
62
|
## Current Status
|
|
63
63
|
|
|
64
|
-
-
|
|
65
|
-
-
|
|
66
|
-
- Phase
|
|
64
|
+
- **Phase 0** and **Phase 1** (task engine, `v0.3.0`) are complete.
|
|
65
|
+
- **Phase 2** (layered config, policy gates, cutover docs, `v0.4.0`) is complete in-repo; see `docs/maintainers/TASKS.md` and `docs/maintainers/ROADMAP.md`.
|
|
66
|
+
- **Phase 3** (enhancement loop MVP, `v0.5.0`) is next (`T190` onward).
|
|
67
67
|
|
|
68
68
|
## Goals
|
|
69
69
|
|
package/dist/cli.js
CHANGED
|
@@ -4,11 +4,14 @@ import path from "node:path";
|
|
|
4
4
|
import { pathToFileURL } from "node:url";
|
|
5
5
|
import { ModuleRegistry } from "./core/module-registry.js";
|
|
6
6
|
import { ModuleCommandRouter } from "./core/module-command-router.js";
|
|
7
|
+
import { appendPolicyTrace, getOperationIdForCommand, isSensitiveModuleCommand, parsePolicyApproval, parsePolicyApprovalFromEnv, resolveActor } from "./core/policy.js";
|
|
8
|
+
import { resolveWorkspaceConfigWithLayers } from "./core/workspace-kit-config.js";
|
|
7
9
|
import { documentationModule } from "./modules/documentation/index.js";
|
|
8
10
|
import { taskEngineModule } from "./modules/task-engine/index.js";
|
|
9
11
|
import { approvalsModule } from "./modules/approvals/index.js";
|
|
10
12
|
import { planningModule } from "./modules/planning/index.js";
|
|
11
13
|
import { improvementModule } from "./modules/improvement/index.js";
|
|
14
|
+
import { workspaceConfigModule } from "./modules/workspace-config/index.js";
|
|
12
15
|
const EXIT_SUCCESS = 0;
|
|
13
16
|
const EXIT_VALIDATION_FAILURE = 1;
|
|
14
17
|
const EXIT_USAGE_ERROR = 2;
|
|
@@ -94,6 +97,33 @@ export async function parseJsonFile(filePath) {
|
|
|
94
97
|
const raw = await fs.readFile(filePath, "utf8");
|
|
95
98
|
return JSON.parse(raw);
|
|
96
99
|
}
|
|
100
|
+
async function requireCliPolicyApproval(cwd, operationId, commandLabel, writeError) {
|
|
101
|
+
const approval = parsePolicyApprovalFromEnv(process.env);
|
|
102
|
+
if (!approval) {
|
|
103
|
+
writeError(`workspace-kit ${commandLabel} requires WORKSPACE_KIT_POLICY_APPROVAL with JSON {"confirmed":true,"rationale":"..."} (agent-mediated).`);
|
|
104
|
+
await appendPolicyTrace(cwd, {
|
|
105
|
+
timestamp: new Date().toISOString(),
|
|
106
|
+
operationId,
|
|
107
|
+
command: commandLabel,
|
|
108
|
+
actor: resolveActor(cwd, {}, process.env),
|
|
109
|
+
allowed: false,
|
|
110
|
+
message: "missing WORKSPACE_KIT_POLICY_APPROVAL"
|
|
111
|
+
});
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
return { rationale: approval.rationale };
|
|
115
|
+
}
|
|
116
|
+
async function recordCliPolicySuccess(cwd, operationId, commandLabel, rationale, commandOk) {
|
|
117
|
+
await appendPolicyTrace(cwd, {
|
|
118
|
+
timestamp: new Date().toISOString(),
|
|
119
|
+
operationId,
|
|
120
|
+
command: commandLabel,
|
|
121
|
+
actor: resolveActor(cwd, {}, process.env),
|
|
122
|
+
allowed: true,
|
|
123
|
+
rationale,
|
|
124
|
+
commandOk
|
|
125
|
+
});
|
|
126
|
+
}
|
|
97
127
|
function readStringField(objectValue, key, errors, fieldPath) {
|
|
98
128
|
const value = objectValue[key];
|
|
99
129
|
if (typeof value !== "string" || value.trim().length === 0) {
|
|
@@ -316,8 +346,13 @@ export async function runCli(args, options = {}) {
|
|
|
316
346
|
return EXIT_USAGE_ERROR;
|
|
317
347
|
}
|
|
318
348
|
if (command === "init") {
|
|
349
|
+
const approval = await requireCliPolicyApproval(cwd, "cli.init", "init", writeError);
|
|
350
|
+
if (!approval) {
|
|
351
|
+
return EXIT_VALIDATION_FAILURE;
|
|
352
|
+
}
|
|
319
353
|
const { errors, profile } = await validateProfile(cwd);
|
|
320
354
|
if (errors.length > 0 || !profile) {
|
|
355
|
+
await recordCliPolicySuccess(cwd, "cli.init", "init", approval.rationale, false);
|
|
321
356
|
writeError("workspace-kit init failed profile validation.");
|
|
322
357
|
for (const error of errors) {
|
|
323
358
|
writeError(`- ${error}`);
|
|
@@ -328,6 +363,7 @@ export async function runCli(args, options = {}) {
|
|
|
328
363
|
writeLine("workspace-kit init generated profile-driven project context artifacts.");
|
|
329
364
|
writeLine(`- ${path.relative(cwd, artifacts.generatedJsonPath)}`);
|
|
330
365
|
writeLine(`- ${path.relative(cwd, artifacts.generatedRulePath)}`);
|
|
366
|
+
await recordCliPolicySuccess(cwd, "cli.init", "init", approval.rationale, true);
|
|
331
367
|
return EXIT_SUCCESS;
|
|
332
368
|
}
|
|
333
369
|
if (command === "check") {
|
|
@@ -344,8 +380,13 @@ export async function runCli(args, options = {}) {
|
|
|
344
380
|
return EXIT_SUCCESS;
|
|
345
381
|
}
|
|
346
382
|
if (command === "upgrade") {
|
|
383
|
+
const approval = await requireCliPolicyApproval(cwd, "cli.upgrade", "upgrade", writeError);
|
|
384
|
+
if (!approval) {
|
|
385
|
+
return EXIT_VALIDATION_FAILURE;
|
|
386
|
+
}
|
|
347
387
|
const { errors, profile } = await validateProfile(cwd);
|
|
348
388
|
if (errors.length > 0 || !profile) {
|
|
389
|
+
await recordCliPolicySuccess(cwd, "cli.upgrade", "upgrade", approval.rationale, false);
|
|
349
390
|
writeError("workspace-kit upgrade failed profile validation.");
|
|
350
391
|
for (const error of errors) {
|
|
351
392
|
writeError(`- ${error}`);
|
|
@@ -444,6 +485,7 @@ export async function runCli(args, options = {}) {
|
|
|
444
485
|
}
|
|
445
486
|
}
|
|
446
487
|
writeLine(`Backups written under: ${path.relative(cwd, backupRoot)}`);
|
|
488
|
+
await recordCliPolicySuccess(cwd, "cli.upgrade", "upgrade", approval.rationale, true);
|
|
447
489
|
return EXIT_SUCCESS;
|
|
448
490
|
}
|
|
449
491
|
if (command === "drift-check") {
|
|
@@ -534,6 +576,7 @@ export async function runCli(args, options = {}) {
|
|
|
534
576
|
}
|
|
535
577
|
if (command === "run") {
|
|
536
578
|
const allModules = [
|
|
579
|
+
workspaceConfigModule,
|
|
537
580
|
documentationModule,
|
|
538
581
|
taskEngineModule,
|
|
539
582
|
approvalsModule,
|
|
@@ -571,9 +614,74 @@ export async function runCli(args, options = {}) {
|
|
|
571
614
|
return EXIT_USAGE_ERROR;
|
|
572
615
|
}
|
|
573
616
|
}
|
|
574
|
-
const
|
|
617
|
+
const invocationConfig = typeof commandArgs.config === "object" &&
|
|
618
|
+
commandArgs.config !== null &&
|
|
619
|
+
!Array.isArray(commandArgs.config)
|
|
620
|
+
? commandArgs.config
|
|
621
|
+
: {};
|
|
622
|
+
let effective;
|
|
623
|
+
try {
|
|
624
|
+
const resolved = await resolveWorkspaceConfigWithLayers({
|
|
625
|
+
workspacePath: cwd,
|
|
626
|
+
registry,
|
|
627
|
+
invocationConfig
|
|
628
|
+
});
|
|
629
|
+
effective = resolved.effective;
|
|
630
|
+
}
|
|
631
|
+
catch (err) {
|
|
632
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
633
|
+
writeError(`Config resolution failed: ${message}`);
|
|
634
|
+
return EXIT_VALIDATION_FAILURE;
|
|
635
|
+
}
|
|
636
|
+
const actor = resolveActor(cwd, commandArgs, process.env);
|
|
637
|
+
const sensitive = isSensitiveModuleCommand(subcommand, commandArgs);
|
|
638
|
+
if (sensitive) {
|
|
639
|
+
const approval = parsePolicyApproval(commandArgs);
|
|
640
|
+
if (!approval) {
|
|
641
|
+
const op = getOperationIdForCommand(subcommand);
|
|
642
|
+
if (op) {
|
|
643
|
+
await appendPolicyTrace(cwd, {
|
|
644
|
+
timestamp: new Date().toISOString(),
|
|
645
|
+
operationId: op,
|
|
646
|
+
command: `run ${subcommand}`,
|
|
647
|
+
actor,
|
|
648
|
+
allowed: false,
|
|
649
|
+
message: "missing policyApproval in JSON args"
|
|
650
|
+
});
|
|
651
|
+
}
|
|
652
|
+
writeLine(JSON.stringify({
|
|
653
|
+
ok: false,
|
|
654
|
+
code: "policy-denied",
|
|
655
|
+
message: 'Sensitive command requires policyApproval in JSON args: {"policyApproval":{"confirmed":true,"rationale":"user approved in chat"}}'
|
|
656
|
+
}, null, 2));
|
|
657
|
+
return EXIT_VALIDATION_FAILURE;
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
const ctx = {
|
|
661
|
+
runtimeVersion: "0.1",
|
|
662
|
+
workspacePath: cwd,
|
|
663
|
+
effectiveConfig: effective,
|
|
664
|
+
resolvedActor: actor,
|
|
665
|
+
moduleRegistry: registry
|
|
666
|
+
};
|
|
575
667
|
try {
|
|
576
668
|
const result = await router.execute(subcommand, commandArgs, ctx);
|
|
669
|
+
if (sensitive) {
|
|
670
|
+
const approval = parsePolicyApproval(commandArgs);
|
|
671
|
+
const op = getOperationIdForCommand(subcommand);
|
|
672
|
+
if (approval && op) {
|
|
673
|
+
await appendPolicyTrace(cwd, {
|
|
674
|
+
timestamp: new Date().toISOString(),
|
|
675
|
+
operationId: op,
|
|
676
|
+
command: `run ${subcommand}`,
|
|
677
|
+
actor,
|
|
678
|
+
allowed: true,
|
|
679
|
+
rationale: approval.rationale,
|
|
680
|
+
commandOk: result.ok,
|
|
681
|
+
message: result.message
|
|
682
|
+
});
|
|
683
|
+
}
|
|
684
|
+
}
|
|
577
685
|
writeLine(JSON.stringify(result, null, 2));
|
|
578
686
|
return result.ok ? EXIT_SUCCESS : EXIT_VALIDATION_FAILURE;
|
|
579
687
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export type { ModuleCommand, ModuleCommandResult, ModuleCapability, ModuleDocumentContract, ModuleEvent, ModuleInstructionContract, ModuleInstructionEntry, ModuleLifecycleContext, ModuleRegistration, WorkflowModule } from "./module-contract.js";
|
|
1
|
+
export type { ConfigRegistryView, ModuleCommand, ModuleCommandResult, ModuleCapability, ModuleDocumentContract, ModuleEvent, ModuleInstructionContract, ModuleInstructionEntry, ModuleLifecycleContext, ModuleRegistration, WorkflowModule } from "./module-contract.js";
|
|
@@ -36,9 +36,23 @@ export type ModuleCommandResult = {
|
|
|
36
36
|
message?: string;
|
|
37
37
|
data?: Record<string, unknown>;
|
|
38
38
|
};
|
|
39
|
+
/** Subset of module registry used for config layer ordering (avoids core↔contracts cycles). */
|
|
40
|
+
export type ConfigRegistryView = {
|
|
41
|
+
getStartupOrder(): ReadonlyArray<{
|
|
42
|
+
registration: {
|
|
43
|
+
id: string;
|
|
44
|
+
};
|
|
45
|
+
}>;
|
|
46
|
+
};
|
|
39
47
|
export type ModuleLifecycleContext = {
|
|
40
48
|
runtimeVersion: string;
|
|
41
49
|
workspacePath: string;
|
|
50
|
+
/** Merged workspace config (kit → modules → project → env → invocation). */
|
|
51
|
+
effectiveConfig?: Record<string, unknown>;
|
|
52
|
+
/** Resolved actor for policy traces (see phase2 workbook). */
|
|
53
|
+
resolvedActor?: string;
|
|
54
|
+
/** CLI supplies registry for explain-config and config resolution. */
|
|
55
|
+
moduleRegistry?: ConfigRegistryView;
|
|
42
56
|
};
|
|
43
57
|
export type ModuleRegistration = {
|
|
44
58
|
id: string;
|
package/dist/core/index.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
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, KIT_CONFIG_DEFAULTS, mergeConfigLayers, MODULE_CONFIG_CONTRIBUTIONS, resolveWorkspaceConfigWithLayers, type ConfigLayer, type ConfigLayerId, type EffectiveWorkspaceConfig, type ExplainConfigResult, type ResolveWorkspaceConfigOptions } from "./workspace-kit-config.js";
|
|
4
|
+
export { appendPolicyTrace, getOperationIdForCommand, isSensitiveModuleCommand, parsePolicyApproval, parsePolicyApprovalFromEnv, resolveActor, type PolicyOperationId, type PolicyTraceRecord } from "./policy.js";
|
|
3
5
|
export type CoreRuntimeVersion = "0.1";
|
package/dist/core/index.js
CHANGED
|
@@ -1,2 +1,4 @@
|
|
|
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, KIT_CONFIG_DEFAULTS, mergeConfigLayers, MODULE_CONFIG_CONTRIBUTIONS, resolveWorkspaceConfigWithLayers } from "./workspace-kit-config.js";
|
|
4
|
+
export { appendPolicyTrace, getOperationIdForCommand, isSensitiveModuleCommand, parsePolicyApproval, parsePolicyApprovalFromEnv, resolveActor } from "./policy.js";
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export type PolicyOperationId = "cli.upgrade" | "cli.init" | "doc.document-project" | "doc.generate-document" | "tasks.import-tasks" | "tasks.generate-tasks-md" | "tasks.run-transition";
|
|
2
|
+
export declare function getOperationIdForCommand(commandName: string): PolicyOperationId | undefined;
|
|
3
|
+
/**
|
|
4
|
+
* Sensitive when mutation / write is possible. Documentation commands are exempt when dryRun is true.
|
|
5
|
+
*/
|
|
6
|
+
export declare function isSensitiveModuleCommand(commandName: string, args: Record<string, unknown>): boolean;
|
|
7
|
+
export type PolicyApprovalPayload = {
|
|
8
|
+
confirmed: boolean;
|
|
9
|
+
rationale: string;
|
|
10
|
+
};
|
|
11
|
+
export declare function parsePolicyApprovalFromEnv(env: NodeJS.ProcessEnv): PolicyApprovalPayload | undefined;
|
|
12
|
+
export declare function parsePolicyApproval(args: Record<string, unknown>): PolicyApprovalPayload | undefined;
|
|
13
|
+
export declare function resolveActor(workspacePath: string, args: Record<string, unknown>, env: NodeJS.ProcessEnv): string;
|
|
14
|
+
export type PolicyTraceRecord = {
|
|
15
|
+
timestamp: string;
|
|
16
|
+
operationId: PolicyOperationId;
|
|
17
|
+
command: string;
|
|
18
|
+
actor: string;
|
|
19
|
+
allowed: boolean;
|
|
20
|
+
rationale?: string;
|
|
21
|
+
commandOk?: boolean;
|
|
22
|
+
message?: string;
|
|
23
|
+
};
|
|
24
|
+
export declare function appendPolicyTrace(workspacePath: string, record: PolicyTraceRecord): Promise<void>;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { execSync } from "node:child_process";
|
|
4
|
+
const COMMAND_TO_OPERATION = {
|
|
5
|
+
"document-project": "doc.document-project",
|
|
6
|
+
"generate-document": "doc.generate-document",
|
|
7
|
+
"import-tasks": "tasks.import-tasks",
|
|
8
|
+
"generate-tasks-md": "tasks.generate-tasks-md",
|
|
9
|
+
"run-transition": "tasks.run-transition"
|
|
10
|
+
};
|
|
11
|
+
export function getOperationIdForCommand(commandName) {
|
|
12
|
+
return COMMAND_TO_OPERATION[commandName];
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Sensitive when mutation / write is possible. Documentation commands are exempt when dryRun is true.
|
|
16
|
+
*/
|
|
17
|
+
export function isSensitiveModuleCommand(commandName, args) {
|
|
18
|
+
const op = COMMAND_TO_OPERATION[commandName];
|
|
19
|
+
if (!op)
|
|
20
|
+
return false;
|
|
21
|
+
if (op === "doc.document-project" || op === "doc.generate-document") {
|
|
22
|
+
const options = typeof args.options === "object" && args.options !== null
|
|
23
|
+
? args.options
|
|
24
|
+
: {};
|
|
25
|
+
if (options.dryRun === true) {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
export function parsePolicyApprovalFromEnv(env) {
|
|
32
|
+
const raw = env.WORKSPACE_KIT_POLICY_APPROVAL?.trim();
|
|
33
|
+
if (!raw)
|
|
34
|
+
return undefined;
|
|
35
|
+
try {
|
|
36
|
+
const o = JSON.parse(raw);
|
|
37
|
+
if (o.confirmed !== true)
|
|
38
|
+
return undefined;
|
|
39
|
+
const rationale = typeof o.rationale === "string" ? o.rationale.trim() : "";
|
|
40
|
+
if (!rationale)
|
|
41
|
+
return undefined;
|
|
42
|
+
return { confirmed: true, rationale };
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
export function parsePolicyApproval(args) {
|
|
49
|
+
const raw = args.policyApproval;
|
|
50
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
const o = raw;
|
|
54
|
+
const confirmed = o.confirmed === true;
|
|
55
|
+
const rationale = typeof o.rationale === "string" ? o.rationale.trim() : "";
|
|
56
|
+
if (!confirmed || rationale.length === 0) {
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
return { confirmed, rationale };
|
|
60
|
+
}
|
|
61
|
+
export function resolveActor(workspacePath, args, env) {
|
|
62
|
+
if (typeof args.actor === "string" && args.actor.trim().length > 0) {
|
|
63
|
+
return args.actor.trim();
|
|
64
|
+
}
|
|
65
|
+
const fromEnv = env.WORKSPACE_KIT_ACTOR?.trim();
|
|
66
|
+
if (fromEnv)
|
|
67
|
+
return fromEnv;
|
|
68
|
+
try {
|
|
69
|
+
const email = execSync("git config user.email", {
|
|
70
|
+
cwd: workspacePath,
|
|
71
|
+
encoding: "utf8",
|
|
72
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
73
|
+
}).trim();
|
|
74
|
+
if (email)
|
|
75
|
+
return email;
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
/* ignore */
|
|
79
|
+
}
|
|
80
|
+
try {
|
|
81
|
+
const name = execSync("git config user.name", {
|
|
82
|
+
cwd: workspacePath,
|
|
83
|
+
encoding: "utf8",
|
|
84
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
85
|
+
}).trim();
|
|
86
|
+
if (name)
|
|
87
|
+
return name;
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
/* ignore */
|
|
91
|
+
}
|
|
92
|
+
return "unknown";
|
|
93
|
+
}
|
|
94
|
+
const POLICY_DIR = ".workspace-kit/policy";
|
|
95
|
+
const TRACE_FILE = "traces.jsonl";
|
|
96
|
+
export async function appendPolicyTrace(workspacePath, record) {
|
|
97
|
+
const dir = path.join(workspacePath, POLICY_DIR);
|
|
98
|
+
const fp = path.join(workspacePath, POLICY_DIR, TRACE_FILE);
|
|
99
|
+
const line = `${JSON.stringify(record)}\n`;
|
|
100
|
+
await fs.mkdir(dir, { recursive: true });
|
|
101
|
+
await fs.appendFile(fp, line, "utf8");
|
|
102
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { ConfigRegistryView } from "../contracts/module-contract.js";
|
|
2
|
+
export type ConfigLayerId = "kit-default" | `module:${string}` | "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
|
+
/** Built-in defaults (lowest layer). */
|
|
10
|
+
export declare const KIT_CONFIG_DEFAULTS: Record<string, unknown>;
|
|
11
|
+
/**
|
|
12
|
+
* Static module-level defaults keyed by module id (merged in registry startup order).
|
|
13
|
+
* Each value is deep-merged into the aggregate before project/env/invocation.
|
|
14
|
+
*/
|
|
15
|
+
export declare const MODULE_CONFIG_CONTRIBUTIONS: Record<string, Record<string, unknown>>;
|
|
16
|
+
export declare function deepMerge(target: Record<string, unknown>, source: Record<string, unknown>): Record<string, unknown>;
|
|
17
|
+
export declare function getAtPath(root: Record<string, unknown>, dotted: string): unknown;
|
|
18
|
+
export declare function deepEqual(a: unknown, b: unknown): boolean;
|
|
19
|
+
/**
|
|
20
|
+
* Parse WORKSPACE_KIT_* env into a nested object (double-underscore → path under domains).
|
|
21
|
+
* Example: WORKSPACE_KIT_TASKS__STORE_PATH -> { tasks: { storeRelativePath: "..." } }
|
|
22
|
+
* Uses segment after prefix; known domain prefix sets first key.
|
|
23
|
+
*/
|
|
24
|
+
export declare function envToConfigOverlay(env: NodeJS.ProcessEnv): Record<string, unknown>;
|
|
25
|
+
/** Kit defaults + module contributions (topological order). Project/env/invocation added separately. */
|
|
26
|
+
export declare function buildBaseConfigLayers(registry: ConfigRegistryView): ConfigLayer[];
|
|
27
|
+
export declare function loadProjectLayer(workspacePath: string): Promise<ConfigLayer>;
|
|
28
|
+
export declare function mergeConfigLayers(layers: ConfigLayer[]): Record<string, unknown>;
|
|
29
|
+
export type ResolveWorkspaceConfigOptions = {
|
|
30
|
+
workspacePath: string;
|
|
31
|
+
registry: ConfigRegistryView;
|
|
32
|
+
env?: NodeJS.ProcessEnv;
|
|
33
|
+
/** Merged last (from `workspace-kit run` top-level `config` key). */
|
|
34
|
+
invocationConfig?: Record<string, unknown>;
|
|
35
|
+
};
|
|
36
|
+
export declare function resolveWorkspaceConfigWithLayers(options: ResolveWorkspaceConfigOptions): Promise<{
|
|
37
|
+
effective: EffectiveWorkspaceConfig;
|
|
38
|
+
layers: ConfigLayer[];
|
|
39
|
+
}>;
|
|
40
|
+
export type ExplainConfigResult = {
|
|
41
|
+
path: string;
|
|
42
|
+
effectiveValue: unknown;
|
|
43
|
+
winningLayer: ConfigLayerId;
|
|
44
|
+
alternates: {
|
|
45
|
+
layer: ConfigLayerId;
|
|
46
|
+
value: unknown;
|
|
47
|
+
}[];
|
|
48
|
+
};
|
|
49
|
+
export declare function explainConfigPath(dottedPath: string, layers: ConfigLayer[]): ExplainConfigResult;
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
const PROJECT_CONFIG_REL = ".workspace-kit/config.json";
|
|
5
|
+
/** Built-in defaults (lowest layer). */
|
|
6
|
+
export const KIT_CONFIG_DEFAULTS = {
|
|
7
|
+
core: {},
|
|
8
|
+
tasks: {
|
|
9
|
+
storeRelativePath: ".workspace-kit/tasks/state.json"
|
|
10
|
+
},
|
|
11
|
+
documentation: {}
|
|
12
|
+
};
|
|
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 const MODULE_CONFIG_CONTRIBUTIONS = {
|
|
18
|
+
"task-engine": {
|
|
19
|
+
tasks: {
|
|
20
|
+
storeRelativePath: ".workspace-kit/tasks/state.json"
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
documentation: {
|
|
24
|
+
documentation: {}
|
|
25
|
+
},
|
|
26
|
+
approvals: {},
|
|
27
|
+
planning: {},
|
|
28
|
+
improvement: {}
|
|
29
|
+
};
|
|
30
|
+
export function deepMerge(target, source) {
|
|
31
|
+
const out = { ...target };
|
|
32
|
+
for (const [k, v] of Object.entries(source)) {
|
|
33
|
+
if (v !== null && typeof v === "object" && !Array.isArray(v)) {
|
|
34
|
+
const prev = out[k];
|
|
35
|
+
const prevObj = prev !== null && typeof prev === "object" && !Array.isArray(prev)
|
|
36
|
+
? prev
|
|
37
|
+
: {};
|
|
38
|
+
out[k] = deepMerge(prevObj, v);
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
out[k] = v;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return out;
|
|
45
|
+
}
|
|
46
|
+
function cloneDeep(obj) {
|
|
47
|
+
return JSON.parse(JSON.stringify(obj));
|
|
48
|
+
}
|
|
49
|
+
export function getAtPath(root, dotted) {
|
|
50
|
+
const parts = dotted.split(".").filter(Boolean);
|
|
51
|
+
let cur = root;
|
|
52
|
+
for (const p of parts) {
|
|
53
|
+
if (cur === null || cur === undefined || typeof cur !== "object" || Array.isArray(cur)) {
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
cur = cur[p];
|
|
57
|
+
}
|
|
58
|
+
return cur;
|
|
59
|
+
}
|
|
60
|
+
export function deepEqual(a, b) {
|
|
61
|
+
if (a === b)
|
|
62
|
+
return true;
|
|
63
|
+
if (a === null || b === null || typeof a !== "object" || typeof b !== "object") {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
if (Array.isArray(a) !== Array.isArray(b))
|
|
67
|
+
return false;
|
|
68
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
69
|
+
if (a.length !== b.length)
|
|
70
|
+
return false;
|
|
71
|
+
return a.every((v, i) => deepEqual(v, b[i]));
|
|
72
|
+
}
|
|
73
|
+
const ak = Object.keys(a).sort();
|
|
74
|
+
const bk = Object.keys(b).sort();
|
|
75
|
+
if (ak.length !== bk.length)
|
|
76
|
+
return false;
|
|
77
|
+
for (let i = 0; i < ak.length; i++) {
|
|
78
|
+
if (ak[i] !== bk[i])
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
for (const k of ak) {
|
|
82
|
+
if (!deepEqual(a[k], b[k])) {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
async function readProjectConfigFile(workspacePath) {
|
|
89
|
+
const fp = path.join(workspacePath, PROJECT_CONFIG_REL);
|
|
90
|
+
if (!existsSync(fp)) {
|
|
91
|
+
return {};
|
|
92
|
+
}
|
|
93
|
+
const raw = await fs.readFile(fp, "utf8");
|
|
94
|
+
const parsed = JSON.parse(raw);
|
|
95
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
96
|
+
throw new Error("config-invalid: .workspace-kit/config.json must be a JSON object");
|
|
97
|
+
}
|
|
98
|
+
const obj = parsed;
|
|
99
|
+
if (obj.schemaVersion !== undefined && typeof obj.schemaVersion !== "number") {
|
|
100
|
+
throw new Error("config-invalid: schemaVersion must be a number when present");
|
|
101
|
+
}
|
|
102
|
+
return obj;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Parse WORKSPACE_KIT_* env into a nested object (double-underscore → path under domains).
|
|
106
|
+
* Example: WORKSPACE_KIT_TASKS__STORE_PATH -> { tasks: { storeRelativePath: "..." } }
|
|
107
|
+
* Uses segment after prefix; known domain prefix sets first key.
|
|
108
|
+
*/
|
|
109
|
+
export function envToConfigOverlay(env) {
|
|
110
|
+
const prefix = "WORKSPACE_KIT_";
|
|
111
|
+
const out = {};
|
|
112
|
+
for (const [key, val] of Object.entries(env)) {
|
|
113
|
+
if (!key.startsWith(prefix) || val === undefined)
|
|
114
|
+
continue;
|
|
115
|
+
const rest = key.slice(prefix.length);
|
|
116
|
+
if (!rest || rest === "ACTOR")
|
|
117
|
+
continue;
|
|
118
|
+
const segments = rest.split("__").map((s) => camelCaseEnvSegment(s));
|
|
119
|
+
if (segments.length === 0)
|
|
120
|
+
continue;
|
|
121
|
+
setDeep(out, segments, coerceEnvValue(val));
|
|
122
|
+
}
|
|
123
|
+
return out;
|
|
124
|
+
}
|
|
125
|
+
function camelCaseEnvSegment(s) {
|
|
126
|
+
const lower = s.toLowerCase().replace(/_/g, "");
|
|
127
|
+
// tasks -> tasks; STORE_PATH segments already split — first segment may be TASKS
|
|
128
|
+
if (lower === "tasks")
|
|
129
|
+
return "tasks";
|
|
130
|
+
if (lower === "documentation")
|
|
131
|
+
return "documentation";
|
|
132
|
+
if (lower === "core")
|
|
133
|
+
return "core";
|
|
134
|
+
// e.g. STORE_PATH -> storePath
|
|
135
|
+
return s
|
|
136
|
+
.toLowerCase()
|
|
137
|
+
.split("_")
|
|
138
|
+
.filter(Boolean)
|
|
139
|
+
.map((w, i) => (i === 0 ? w : w.charAt(0).toUpperCase() + w.slice(1)))
|
|
140
|
+
.join("");
|
|
141
|
+
}
|
|
142
|
+
function setDeep(target, segments, value) {
|
|
143
|
+
let cur = target;
|
|
144
|
+
for (let i = 0; i < segments.length - 1; i++) {
|
|
145
|
+
const seg = segments[i];
|
|
146
|
+
const next = cur[seg];
|
|
147
|
+
if (next === undefined || typeof next !== "object" || Array.isArray(next)) {
|
|
148
|
+
cur[seg] = {};
|
|
149
|
+
}
|
|
150
|
+
cur = cur[seg];
|
|
151
|
+
}
|
|
152
|
+
cur[segments[segments.length - 1]] = value;
|
|
153
|
+
}
|
|
154
|
+
function coerceEnvValue(val) {
|
|
155
|
+
if (val === "true")
|
|
156
|
+
return true;
|
|
157
|
+
if (val === "false")
|
|
158
|
+
return false;
|
|
159
|
+
if (/^-?\d+$/.test(val))
|
|
160
|
+
return Number(val);
|
|
161
|
+
return val;
|
|
162
|
+
}
|
|
163
|
+
/** Kit defaults + module contributions (topological order). Project/env/invocation added separately. */
|
|
164
|
+
export function buildBaseConfigLayers(registry) {
|
|
165
|
+
const layers = [];
|
|
166
|
+
layers.push({ id: "kit-default", data: cloneDeep(KIT_CONFIG_DEFAULTS) });
|
|
167
|
+
for (const mod of registry.getStartupOrder()) {
|
|
168
|
+
const contrib = MODULE_CONFIG_CONTRIBUTIONS[mod.registration.id];
|
|
169
|
+
if (contrib && Object.keys(contrib).length > 0) {
|
|
170
|
+
layers.push({ id: `module:${mod.registration.id}`, data: cloneDeep(contrib) });
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return layers;
|
|
174
|
+
}
|
|
175
|
+
export async function loadProjectLayer(workspacePath) {
|
|
176
|
+
const data = await readProjectConfigFile(workspacePath);
|
|
177
|
+
return { id: "project", data };
|
|
178
|
+
}
|
|
179
|
+
export function mergeConfigLayers(layers) {
|
|
180
|
+
let acc = {};
|
|
181
|
+
for (const layer of layers) {
|
|
182
|
+
acc = deepMerge(acc, layer.data);
|
|
183
|
+
}
|
|
184
|
+
return acc;
|
|
185
|
+
}
|
|
186
|
+
export async function resolveWorkspaceConfigWithLayers(options) {
|
|
187
|
+
const { workspacePath, registry, env = process.env, invocationConfig } = options;
|
|
188
|
+
const layers = [...buildBaseConfigLayers(registry)];
|
|
189
|
+
layers.push(await loadProjectLayer(workspacePath));
|
|
190
|
+
layers.push({ id: "env", data: envToConfigOverlay(env) });
|
|
191
|
+
if (invocationConfig && Object.keys(invocationConfig).length > 0) {
|
|
192
|
+
layers.push({ id: "invocation", data: cloneDeep(invocationConfig) });
|
|
193
|
+
}
|
|
194
|
+
return { effective: mergeConfigLayers(layers), layers };
|
|
195
|
+
}
|
|
196
|
+
export function explainConfigPath(dottedPath, layers) {
|
|
197
|
+
const mergedFull = mergeConfigLayers(layers);
|
|
198
|
+
const effectiveValue = getAtPath(mergedFull, dottedPath);
|
|
199
|
+
let winningLayer = "kit-default";
|
|
200
|
+
let prevMerged = {};
|
|
201
|
+
for (let i = 0; i < layers.length; i++) {
|
|
202
|
+
const slice = layers.slice(0, i + 1);
|
|
203
|
+
const nextMerged = mergeConfigLayers(slice);
|
|
204
|
+
const prevVal = getAtPath(prevMerged, dottedPath);
|
|
205
|
+
const nextVal = getAtPath(nextMerged, dottedPath);
|
|
206
|
+
if (!deepEqual(prevVal, nextVal)) {
|
|
207
|
+
winningLayer = layers[i].id;
|
|
208
|
+
}
|
|
209
|
+
prevMerged = nextMerged;
|
|
210
|
+
}
|
|
211
|
+
const alternates = [];
|
|
212
|
+
for (let i = 0; i < layers.length; i++) {
|
|
213
|
+
const slice = layers.slice(0, i + 1);
|
|
214
|
+
const m = mergeConfigLayers(slice);
|
|
215
|
+
alternates.push({ layer: layers[i].id, value: getAtPath(m, dottedPath) });
|
|
216
|
+
}
|
|
217
|
+
return { path: dottedPath, effectiveValue, winningLayer, alternates };
|
|
218
|
+
}
|
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,
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { explainConfigPath, resolveWorkspaceConfigWithLayers } from "../../core/workspace-kit-config.js";
|
|
2
|
+
async function handleExplainConfig(args, ctx) {
|
|
3
|
+
const pathArg = typeof args.path === "string" ? args.path.trim() : "";
|
|
4
|
+
if (!pathArg) {
|
|
5
|
+
return {
|
|
6
|
+
ok: false,
|
|
7
|
+
code: "invalid-config-path",
|
|
8
|
+
message: "explain-config requires string 'path' (dot-separated, e.g. tasks.storeRelativePath)"
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
const invocationConfig = typeof args.config === "object" && args.config !== null && !Array.isArray(args.config)
|
|
12
|
+
? args.config
|
|
13
|
+
: {};
|
|
14
|
+
const { layers } = await resolveWorkspaceConfigWithLayers({
|
|
15
|
+
workspacePath: ctx.workspacePath,
|
|
16
|
+
registry: ctx.registry,
|
|
17
|
+
invocationConfig
|
|
18
|
+
});
|
|
19
|
+
const explained = explainConfigPath(pathArg, layers);
|
|
20
|
+
return {
|
|
21
|
+
ok: true,
|
|
22
|
+
code: "config-explained",
|
|
23
|
+
data: explained
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
export const workspaceConfigModule = {
|
|
27
|
+
registration: {
|
|
28
|
+
id: "workspace-config",
|
|
29
|
+
version: "0.4.0",
|
|
30
|
+
contractVersion: "1",
|
|
31
|
+
capabilities: ["diagnostics"],
|
|
32
|
+
dependsOn: [],
|
|
33
|
+
enabledByDefault: true,
|
|
34
|
+
config: {
|
|
35
|
+
path: "src/modules/workspace-config/config.md",
|
|
36
|
+
format: "md",
|
|
37
|
+
description: "Workspace config registry and explain surface."
|
|
38
|
+
},
|
|
39
|
+
state: {
|
|
40
|
+
path: "src/modules/workspace-config/state.md",
|
|
41
|
+
format: "md",
|
|
42
|
+
description: "Workspace config module runtime state (none)."
|
|
43
|
+
},
|
|
44
|
+
instructions: {
|
|
45
|
+
directory: "src/modules/workspace-config/instructions",
|
|
46
|
+
entries: [
|
|
47
|
+
{
|
|
48
|
+
name: "explain-config",
|
|
49
|
+
file: "explain-config.md",
|
|
50
|
+
description: "Agent-first JSON: effective config value and winning layer for a dotted path."
|
|
51
|
+
}
|
|
52
|
+
]
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
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
|
+
const reg = ctx.moduleRegistry;
|
|
60
|
+
if (!reg) {
|
|
61
|
+
return {
|
|
62
|
+
ok: false,
|
|
63
|
+
code: "internal-error",
|
|
64
|
+
message: "explain-config requires moduleRegistry on context (CLI wiring)"
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
return handleExplainConfig(command.args ?? {}, {
|
|
68
|
+
workspacePath: ctx.workspacePath,
|
|
69
|
+
registry: reg
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
};
|