@workflow-cannon/workspace-kit 0.16.1 → 0.18.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.
@@ -56,6 +56,59 @@ const REGISTRY = {
56
56
  exposure: "public",
57
57
  writableLayers: ["project", "user"]
58
58
  },
59
+ "tasks.strictValidation": {
60
+ key: "tasks.strictValidation",
61
+ type: "boolean",
62
+ description: "When true, task mutations validate the full active task set before persistence and fail on invalid task records.",
63
+ default: false,
64
+ domainScope: "project",
65
+ owningModule: "task-engine",
66
+ sensitive: false,
67
+ requiresRestart: false,
68
+ requiresApproval: false,
69
+ exposure: "public",
70
+ writableLayers: ["project", "user"]
71
+ },
72
+ "planning.defaultQuestionDepth": {
73
+ key: "planning.defaultQuestionDepth",
74
+ type: "string",
75
+ description: "Planning interview depth mode: minimal (critical only), guided (critical + static follow-ups), or adaptive (context-driven follow-ups).",
76
+ default: "adaptive",
77
+ allowedValues: ["minimal", "guided", "adaptive"],
78
+ domainScope: "project",
79
+ owningModule: "planning",
80
+ sensitive: false,
81
+ requiresRestart: false,
82
+ requiresApproval: false,
83
+ exposure: "maintainer",
84
+ writableLayers: ["project", "user"]
85
+ },
86
+ "planning.hardBlockCriticalUnknowns": {
87
+ key: "planning.hardBlockCriticalUnknowns",
88
+ type: "boolean",
89
+ description: "When true, planning finalize requests fail until critical unknown questions are answered.",
90
+ default: true,
91
+ domainScope: "project",
92
+ owningModule: "planning",
93
+ sensitive: false,
94
+ requiresRestart: false,
95
+ requiresApproval: false,
96
+ exposure: "maintainer",
97
+ writableLayers: ["project", "user"]
98
+ },
99
+ "planning.rulePacks": {
100
+ key: "planning.rulePacks",
101
+ type: "object",
102
+ description: "Optional object overrides for planning rule packs by workflow type (`baseQuestions` and `adaptiveQuestions`).",
103
+ default: {},
104
+ domainScope: "project",
105
+ owningModule: "planning",
106
+ sensitive: false,
107
+ requiresRestart: false,
108
+ requiresApproval: false,
109
+ exposure: "maintainer",
110
+ writableLayers: ["project", "user"]
111
+ },
59
112
  "policy.extraSensitiveModuleCommands": {
60
113
  key: "policy.extraSensitiveModuleCommands",
61
114
  type: "array",
@@ -11,7 +11,8 @@ export function getProjectConfigPath(workspacePath) {
11
11
  export const KIT_CONFIG_DEFAULTS = {
12
12
  core: {},
13
13
  tasks: {
14
- storeRelativePath: ".workspace-kit/tasks/state.json"
14
+ storeRelativePath: ".workspace-kit/tasks/state.json",
15
+ strictValidation: false
15
16
  },
16
17
  documentation: {},
17
18
  responseTemplates: {
@@ -45,7 +46,11 @@ export const KIT_CONFIG_DEFAULTS = {
45
46
  */
46
47
  export const MODULE_CONFIG_CONTRIBUTIONS = {
47
48
  approvals: {},
48
- planning: {}
49
+ planning: {
50
+ defaultQuestionDepth: "adaptive",
51
+ hardBlockCriticalUnknowns: true,
52
+ rulePacks: {}
53
+ }
49
54
  };
50
55
  export function deepMerge(target, source) {
51
56
  const out = { ...target };
@@ -0,0 +1,19 @@
1
+ import type { PlanningWorkflowType } from "./types.js";
2
+ export type PlanningWishlistArtifact = {
3
+ schemaVersion: 1;
4
+ planningType: PlanningWorkflowType;
5
+ generatedAt: string;
6
+ goals: string[];
7
+ approach: string;
8
+ majorTechnicalDecisions: string[];
9
+ candidateFeaturesOrChanges: string[];
10
+ assumptions: string[];
11
+ openQuestions: string[];
12
+ risksAndConstraints: string[];
13
+ sourceAnswers: Record<string, string>;
14
+ };
15
+ export declare function composePlanningWishlistArtifact(args: {
16
+ planningType: PlanningWorkflowType;
17
+ answers: Record<string, unknown>;
18
+ unresolvedCriticalQuestionIds: string[];
19
+ }): PlanningWishlistArtifact;
@@ -0,0 +1,72 @@
1
+ function answer(answers, key, fallback) {
2
+ const value = answers[key];
3
+ if (typeof value !== "string" || value.trim().length === 0) {
4
+ return fallback;
5
+ }
6
+ return value.trim();
7
+ }
8
+ function list(answers, key) {
9
+ const value = answers[key];
10
+ if (Array.isArray(value)) {
11
+ return value.filter((x) => typeof x === "string" && x.trim().length > 0).map((x) => x.trim());
12
+ }
13
+ if (typeof value === "string" && value.trim().length > 0) {
14
+ return [value.trim()];
15
+ }
16
+ return [];
17
+ }
18
+ export function composePlanningWishlistArtifact(args) {
19
+ const { planningType, answers, unresolvedCriticalQuestionIds } = args;
20
+ const sourceAnswers = {};
21
+ for (const [key, value] of Object.entries(answers)) {
22
+ if (typeof value === "string" && value.trim().length > 0) {
23
+ sourceAnswers[key] = value.trim();
24
+ }
25
+ }
26
+ const goals = list(answers, "goals");
27
+ if (goals.length === 0) {
28
+ const inferredGoal = answer(answers, "goal", "") ||
29
+ answer(answers, "featureGoal", "") ||
30
+ answer(answers, "changeGoal", "");
31
+ if (inferredGoal)
32
+ goals.push(inferredGoal);
33
+ }
34
+ const candidate = list(answers, "candidateFeaturesOrChanges");
35
+ if (candidate.length === 0) {
36
+ const inferred = answer(answers, "scope", "");
37
+ if (inferred)
38
+ candidate.push(inferred);
39
+ }
40
+ const majorTechnicalDecisions = list(answers, "majorTechnicalDecisions");
41
+ const decisionRationale = answer(answers, "decisionRationale", "");
42
+ if (decisionRationale && majorTechnicalDecisions.length === 0) {
43
+ majorTechnicalDecisions.push(decisionRationale);
44
+ }
45
+ const assumptions = list(answers, "assumptions");
46
+ const openQuestions = [
47
+ ...list(answers, "openQuestions"),
48
+ ...unresolvedCriticalQuestionIds.map((id) => `Unresolved critical question: ${id}`)
49
+ ];
50
+ const risksAndConstraints = list(answers, "risksAndConstraints");
51
+ const constraints = answer(answers, "constraints", "");
52
+ if (constraints) {
53
+ risksAndConstraints.push(`Constraint: ${constraints}`);
54
+ }
55
+ const riskPriority = answer(answers, "riskPriority", "");
56
+ if (riskPriority) {
57
+ risksAndConstraints.push(`Risk priority: ${riskPriority}`);
58
+ }
59
+ return {
60
+ schemaVersion: 1,
61
+ planningType,
62
+ generatedAt: new Date().toISOString(),
63
+ goals,
64
+ approach: answer(answers, "approach", "Context-driven workflow guided by planning rules."),
65
+ majorTechnicalDecisions,
66
+ candidateFeaturesOrChanges: candidate,
67
+ assumptions,
68
+ openQuestions,
69
+ risksAndConstraints,
70
+ sourceAnswers
71
+ };
72
+ }
@@ -1,3 +1,36 @@
1
+ import { PLANNING_WORKFLOW_DESCRIPTORS, PLANNING_WORKFLOW_TYPES } from "./types.js";
2
+ import { nextPlanningQuestions, resolvePlanningConfig, resolvePlanningRulePack } from "./question-engine.js";
3
+ import { composePlanningWishlistArtifact } from "./artifact.js";
4
+ import { openPlanningStores } from "../task-engine/planning-open.js";
5
+ import { buildWishlistItemFromIntake, validateWishlistIntakePayload } from "../task-engine/wishlist-validation.js";
6
+ function nextWishlistId(items) {
7
+ let max = 0;
8
+ for (const item of items) {
9
+ const match = /^W(\d+)$/.exec(item.id);
10
+ if (!match)
11
+ continue;
12
+ const parsed = Number(match[1]);
13
+ if (Number.isFinite(parsed)) {
14
+ max = Math.max(max, parsed);
15
+ }
16
+ }
17
+ return `W${max + 1}`;
18
+ }
19
+ function toCliGuidance(args) {
20
+ const { planningType, answers, unresolvedCriticalCount, totalCriticalCount, finalize } = args;
21
+ const answeredCritical = Math.max(0, totalCriticalCount - unresolvedCriticalCount);
22
+ const completionPct = totalCriticalCount > 0 ? Math.round((answeredCritical / totalCriticalCount) * 100) : 100;
23
+ return {
24
+ answeredCritical,
25
+ totalCritical: totalCriticalCount,
26
+ completionPct,
27
+ suggestedNextCommand: `workspace-kit run build-plan '${JSON.stringify({
28
+ planningType,
29
+ answers,
30
+ finalize: finalize === true
31
+ })}'`
32
+ };
33
+ }
1
34
  export const planningModule = {
2
35
  registration: {
3
36
  id: "planning",
@@ -23,8 +56,240 @@ export const planningModule = {
23
56
  name: "build-plan",
24
57
  file: "build-plan.md",
25
58
  description: "Generate a dependency-aware execution plan."
59
+ },
60
+ {
61
+ name: "list-planning-types",
62
+ file: "list-planning-types.md",
63
+ description: "List supported planning workflow types and their intent."
64
+ },
65
+ {
66
+ name: "explain-planning-rules",
67
+ file: "explain-planning-rules.md",
68
+ description: "Explain effective planning defaults and rule packs for a workflow type."
26
69
  }
27
70
  ]
28
71
  }
72
+ },
73
+ async onCommand(command, ctx) {
74
+ if (command.name === "list-planning-types") {
75
+ return {
76
+ ok: true,
77
+ code: "planning-types-listed",
78
+ message: `Found ${PLANNING_WORKFLOW_DESCRIPTORS.length} planning workflow types`,
79
+ data: {
80
+ planningTypes: PLANNING_WORKFLOW_DESCRIPTORS
81
+ }
82
+ };
83
+ }
84
+ if (command.name === "build-plan") {
85
+ const args = command.args ?? {};
86
+ const planningType = typeof args.planningType === "string" ? args.planningType.trim() : "";
87
+ if (!PLANNING_WORKFLOW_TYPES.includes(planningType)) {
88
+ return {
89
+ ok: false,
90
+ code: "invalid-planning-type",
91
+ message: "build-plan requires planningType of: task-breakdown, sprint-phase, task-ordering, new-feature, change"
92
+ };
93
+ }
94
+ const descriptor = PLANNING_WORKFLOW_DESCRIPTORS.find((x) => x.type === planningType);
95
+ const resolvedRulePack = resolvePlanningRulePack(planningType, ctx.effectiveConfig);
96
+ const totalCriticalCount = resolvedRulePack.baseQuestions.length;
97
+ const answers = typeof args.answers === "object" && args.answers !== null && !Array.isArray(args.answers)
98
+ ? args.answers
99
+ : {};
100
+ const createWishlist = args.createWishlist !== false;
101
+ const finalize = args.finalize === true;
102
+ const { missingCritical, adaptiveFollowups } = nextPlanningQuestions(planningType, answers, ctx.effectiveConfig);
103
+ const config = resolvePlanningConfig(ctx.effectiveConfig);
104
+ if (finalize && missingCritical.length > 0) {
105
+ if (!config.hardBlockCriticalUnknowns) {
106
+ return {
107
+ ok: true,
108
+ code: "planning-ready-with-warnings",
109
+ message: `Finalize allowed with unresolved critical questions because planning.hardBlockCriticalUnknowns=false`,
110
+ data: {
111
+ planningType,
112
+ status: "ready-with-warnings",
113
+ unresolvedCritical: missingCritical,
114
+ nextQuestions: [...missingCritical, ...adaptiveFollowups],
115
+ capturedAnswers: answers,
116
+ cliGuidance: toCliGuidance({
117
+ planningType,
118
+ answers,
119
+ unresolvedCriticalCount: missingCritical.length,
120
+ totalCriticalCount
121
+ })
122
+ }
123
+ };
124
+ }
125
+ return {
126
+ ok: false,
127
+ code: "planning-critical-unknowns",
128
+ message: `Cannot finalize ${planningType}: unresolved critical questions (${missingCritical.map((q) => q.id).join(", ")})`,
129
+ data: {
130
+ planningType,
131
+ unresolvedCritical: missingCritical,
132
+ nextQuestions: [...missingCritical, ...adaptiveFollowups],
133
+ cliGuidance: toCliGuidance({
134
+ planningType,
135
+ answers,
136
+ unresolvedCriticalCount: missingCritical.length,
137
+ totalCriticalCount,
138
+ finalize: true
139
+ })
140
+ }
141
+ };
142
+ }
143
+ if (missingCritical.length > 0) {
144
+ return {
145
+ ok: true,
146
+ code: "planning-questions",
147
+ message: `${missingCritical.length} critical planning questions require answers before finalize`,
148
+ data: {
149
+ planningType,
150
+ status: "needs-input",
151
+ unresolvedCritical: missingCritical,
152
+ nextQuestions: [...missingCritical, ...adaptiveFollowups],
153
+ cliGuidance: toCliGuidance({
154
+ planningType,
155
+ answers,
156
+ unresolvedCriticalCount: missingCritical.length,
157
+ totalCriticalCount
158
+ })
159
+ }
160
+ };
161
+ }
162
+ const unresolvedIds = missingCritical.map((q) => q.id);
163
+ const artifact = composePlanningWishlistArtifact({
164
+ planningType: planningType,
165
+ answers,
166
+ unresolvedCriticalQuestionIds: unresolvedIds
167
+ });
168
+ if (!finalize || !createWishlist) {
169
+ return {
170
+ ok: true,
171
+ code: "planning-ready",
172
+ message: `Planning interview complete for ${planningType}; artifact ready`,
173
+ data: {
174
+ planningType,
175
+ descriptor,
176
+ scaffoldVersion: 3,
177
+ status: "ready-for-artifact",
178
+ unresolvedCritical: [],
179
+ adaptiveFollowups,
180
+ capturedAnswers: answers,
181
+ artifact,
182
+ cliGuidance: toCliGuidance({
183
+ planningType,
184
+ answers,
185
+ unresolvedCriticalCount: 0,
186
+ totalCriticalCount,
187
+ finalize: createWishlist
188
+ })
189
+ }
190
+ };
191
+ }
192
+ const stores = await openPlanningStores(ctx);
193
+ const wishlist = await stores.openWishlist();
194
+ const wishlistId = nextWishlistId(wishlist.getAllItems());
195
+ const now = new Date().toISOString();
196
+ const intake = {
197
+ id: wishlistId,
198
+ title: typeof args.title === "string" && args.title.trim().length > 0
199
+ ? args.title.trim()
200
+ : `${descriptor?.title ?? planningType} plan artifact`,
201
+ problemStatement: typeof answers.problemStatement === "string"
202
+ ? answers.problemStatement
203
+ : typeof answers.featureGoal === "string"
204
+ ? answers.featureGoal
205
+ : "Planning artifact generated from guided workflow.",
206
+ expectedOutcome: typeof answers.expectedOutcome === "string"
207
+ ? answers.expectedOutcome
208
+ : "Clear, reviewable planning artifact for execution decomposition.",
209
+ impact: typeof answers.impact === "string" ? answers.impact : "Improved planning quality and delivery confidence.",
210
+ constraints: typeof answers.constraints === "string"
211
+ ? answers.constraints
212
+ : artifact.risksAndConstraints.join("; ") || "None explicitly provided.",
213
+ successSignals: typeof answers.successSignals === "string"
214
+ ? answers.successSignals
215
+ : "Critical questions answered and artifact accepted by operators.",
216
+ requestor: typeof args.requestor === "string" && args.requestor.trim().length > 0
217
+ ? args.requestor.trim()
218
+ : ctx.resolvedActor ?? "planning-module",
219
+ evidenceRef: typeof args.evidenceRef === "string" && args.evidenceRef.trim().length > 0
220
+ ? args.evidenceRef.trim()
221
+ : `planning:${planningType}:${now}`
222
+ };
223
+ const valid = validateWishlistIntakePayload(intake);
224
+ if (!valid.ok) {
225
+ return {
226
+ ok: false,
227
+ code: "invalid-planning-artifact",
228
+ message: valid.errors.join("; ")
229
+ };
230
+ }
231
+ const item = buildWishlistItemFromIntake(intake, now);
232
+ item.updatedAt = now;
233
+ item.metadata = {
234
+ planningType,
235
+ artifactSchemaVersion: artifact.schemaVersion,
236
+ artifact
237
+ };
238
+ wishlist.addItem(item);
239
+ await wishlist.save();
240
+ return {
241
+ ok: true,
242
+ code: "planning-artifact-created",
243
+ message: `Planning artifact created as wishlist item ${wishlistId}`,
244
+ data: {
245
+ planningType,
246
+ descriptor,
247
+ scaffoldVersion: 3,
248
+ status: "artifact-created",
249
+ wishlistId,
250
+ artifact,
251
+ unresolvedCritical: [],
252
+ adaptiveFollowups,
253
+ capturedAnswers: answers,
254
+ cliGuidance: toCliGuidance({
255
+ planningType,
256
+ answers,
257
+ unresolvedCriticalCount: 0,
258
+ totalCriticalCount,
259
+ finalize: true
260
+ })
261
+ }
262
+ };
263
+ }
264
+ if (command.name === "explain-planning-rules") {
265
+ const args = command.args ?? {};
266
+ const planningType = typeof args.planningType === "string" ? args.planningType.trim() : "";
267
+ if (!PLANNING_WORKFLOW_TYPES.includes(planningType)) {
268
+ return {
269
+ ok: false,
270
+ code: "invalid-planning-type",
271
+ message: "explain-planning-rules requires planningType of: task-breakdown, sprint-phase, task-ordering, new-feature, change"
272
+ };
273
+ }
274
+ const config = resolvePlanningConfig(ctx.effectiveConfig);
275
+ const rulePack = resolvePlanningRulePack(planningType, ctx.effectiveConfig);
276
+ return {
277
+ ok: true,
278
+ code: "planning-rules-explained",
279
+ message: `Effective planning rules for ${planningType}`,
280
+ data: {
281
+ planningType,
282
+ defaultQuestionDepth: config.depth,
283
+ hardBlockCriticalUnknowns: config.hardBlockCriticalUnknowns,
284
+ baseQuestions: rulePack.baseQuestions,
285
+ adaptiveQuestions: rulePack.adaptiveQuestions
286
+ }
287
+ };
288
+ }
289
+ return {
290
+ ok: false,
291
+ code: "unsupported-command",
292
+ message: `Planning module does not support command '${command.name}'`
293
+ };
29
294
  }
30
295
  };
@@ -0,0 +1,23 @@
1
+ import { type PlanningWorkflowType } from "./types.js";
2
+ export type PlanningQuestion = {
3
+ id: string;
4
+ prompt: string;
5
+ examples: string[];
6
+ whyItMatters: string;
7
+ critical: boolean;
8
+ };
9
+ export type PlanningQuestionDepth = "minimal" | "guided" | "adaptive";
10
+ export type PlanningRulePack = {
11
+ baseQuestions: PlanningQuestion[];
12
+ adaptiveQuestions: PlanningQuestion[];
13
+ };
14
+ export declare function resolvePlanningConfig(config: Record<string, unknown> | undefined): {
15
+ depth: PlanningQuestionDepth;
16
+ hardBlockCriticalUnknowns: boolean;
17
+ rulePacks: Partial<Record<PlanningWorkflowType, PlanningRulePack>>;
18
+ };
19
+ export declare function resolvePlanningRulePack(planningType: PlanningWorkflowType, config: Record<string, unknown> | undefined): PlanningRulePack;
20
+ export declare function nextPlanningQuestions(planningType: PlanningWorkflowType, answers: Record<string, unknown>, config?: Record<string, unknown>): {
21
+ missingCritical: PlanningQuestion[];
22
+ adaptiveFollowups: PlanningQuestion[];
23
+ };