@akanjs/devkit 2.3.9-rc.8 → 2.3.9-rc.9

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.
@@ -10,7 +10,7 @@ import type {
10
10
  WorkflowDiagnostic,
11
11
  WorkflowPlanInputs,
12
12
  } from "./workflow";
13
- import { buildAkanModuleContextIndex, toolingRolloutGate } from "./workflow";
13
+ import { buildAkanModuleContextIndex, createWorkflowBaselineSummary, toolingRolloutGate } from "./workflow";
14
14
 
15
15
  export type McpToolDefinition = {
16
16
  name: string;
@@ -158,6 +158,8 @@ export const applyFirstPolicy = {
158
158
  mode: "apply-first",
159
159
  directSourceEdits: "fallback-only",
160
160
  applyRequiredWhen: ["plan_workflow returns planPath", "plan_workflow returns next.tool=apply_workflow"],
161
+ approvalMeaning:
162
+ "requiresApproval is an agent/user review signal before apply_workflow mutates files; it is not a separate MCP permission gate.",
161
163
  validationRequiredAfterApply: ["validationTarget", "applyReportPath"],
162
164
  fallbackAllowedWhen: [
163
165
  "list_workflows and explain_workflow show no matching workflow",
@@ -311,7 +313,12 @@ export const readonlyMcpTools: McpToolDefinition[] = [
311
313
  description: "Report workspace diagnostics, optionally split into baseline and workflow scopes.",
312
314
  inputSchema: {
313
315
  type: "object",
314
- properties: { strict: booleanProperty, runIdOrPlan: stringProperty, changedFiles: stringArrayProperty },
316
+ properties: {
317
+ strict: booleanProperty,
318
+ runIdOrPlan: stringProperty,
319
+ changedFiles: stringArrayProperty,
320
+ includeBaselineDetails: booleanProperty,
321
+ },
315
322
  },
316
323
  },
317
324
  {
@@ -359,7 +366,7 @@ export const applyMcpTools: McpToolDefinition[] = [
359
366
  description: "Validate a plan, apply report, validationTarget, or run artifact.",
360
367
  inputSchema: {
361
368
  type: "object",
362
- properties: { runIdOrPlan: stringProperty },
369
+ properties: { runIdOrPlan: stringProperty, includeBaselineDetails: booleanProperty },
363
370
  required: ["runIdOrPlan"],
364
371
  },
365
372
  },
@@ -409,7 +416,17 @@ export const createAkanValidationContract = (listTools = listAkanMcpTools) => ({
409
416
  validationStatuses: {
410
417
  sourceStatus: ["passed", "failed", "unknown"],
411
418
  workspaceStatus: ["passed", "failed", "unknown"],
412
- overallStatus: ["passed", "failed", "blocked-by-workspace-config", "blocked-by-environment"],
419
+ validationCommandsStatus: ["passed", "failed", "unknown"],
420
+ baselineStatus: ["passed", "failed", "unknown"],
421
+ overallStatus: [
422
+ "passed",
423
+ "failed",
424
+ "passed-with-baseline-blockers",
425
+ "blocked-by-workspace-config",
426
+ "blocked-by-environment",
427
+ ],
428
+ baselineSummary:
429
+ "Default MCP validation output summarizes baseline diagnostics; pass includeBaselineDetails=true for full baselineDiagnostics.",
413
430
  knownBlockers: "Repeated workspace-config/environment failures are summarized before command output.",
414
431
  },
415
432
  moduleContextInputs: {
@@ -420,6 +437,7 @@ export const createAkanValidationContract = (listTools = listAkanMcpTools) => ({
420
437
  toolingRolloutGate,
421
438
  directEditFallbackPolicy: applyFirstPolicy,
422
439
  applyReportFields: [
440
+ "summary",
423
441
  "appliedCommands",
424
442
  "recommendedValidationCommands",
425
443
  "commands",
@@ -484,8 +502,20 @@ export const inspectAkanContext = async (
484
502
  strict: true,
485
503
  runIdOrPlan: workspacePath(workspace, props.request.runIdOrPlan),
486
504
  });
505
+ const baselineSummary = createWorkflowBaselineSummary(
506
+ (doctor.baselineDiagnostics ?? []).map(
507
+ (diagnostic): WorkflowDiagnostic => ({
508
+ severity: diagnostic.severity,
509
+ code: diagnostic.code,
510
+ message: diagnostic.message,
511
+ scope: diagnostic.scope,
512
+ context: diagnostic.context,
513
+ }),
514
+ ),
515
+ { detailsIncluded: false },
516
+ );
487
517
  diagnostics.push(
488
- ...doctor.diagnostics.map(
518
+ ...(doctor.workflowDiagnostics ?? doctor.diagnostics).map(
489
519
  (diagnostic): WorkflowDiagnostic => ({
490
520
  severity: diagnostic.severity,
491
521
  code: diagnostic.code,
@@ -516,7 +546,8 @@ export const inspectAkanContext = async (
516
546
  },
517
547
  {
518
548
  status: doctor.status,
519
- baselineDiagnostics: doctor.baselineDiagnostics?.length ?? 0,
549
+ baselineSummary,
550
+ baselineDiagnostics: 0,
520
551
  workflowDiagnostics: doctor.workflowDiagnostics?.length ?? 0,
521
552
  },
522
553
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akanjs/devkit",
3
- "version": "2.3.9-rc.8",
3
+ "version": "2.3.9-rc.9",
4
4
  "sourceType": "module",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -32,7 +32,7 @@
32
32
  "@langchain/openai": "^1.4.6",
33
33
  "@tailwindcss/node": "^4.3.0",
34
34
  "@trapezedev/project": "^7.1.4",
35
- "akanjs": "2.3.9-rc.8",
35
+ "akanjs": "2.3.9-rc.9",
36
36
  "chalk": "^5.6.2",
37
37
  "commander": "^14.0.3",
38
38
  "daisyui": "5.5.23",
@@ -7,6 +7,7 @@ import type {
7
7
  RepairReport,
8
8
  WorkflowApplyCommand,
9
9
  WorkflowApplyReport,
10
+ WorkflowBaselineSummary,
10
11
  WorkflowDiagnostic,
11
12
  WorkflowFailureScope,
12
13
  WorkflowKnownBlocker,
@@ -30,6 +31,12 @@ const sourceChangeBlocked = (diagnostics: readonly WorkflowDiagnostic[]) =>
30
31
  diagnostic.failureScope === "unknown"),
31
32
  );
32
33
 
34
+ export const workflowPlanApproval = {
35
+ required: true,
36
+ meaning: "Review this read-only plan before apply_workflow mutates files.",
37
+ applyTool: "apply_workflow",
38
+ } as const;
39
+
33
40
  const inferNextActionCode = (
34
41
  action: { action?: WorkflowNextActionCode; command: string },
35
42
  diagnostics: readonly WorkflowDiagnostic[],
@@ -72,6 +79,7 @@ export const createWorkflowApplyReport = ({
72
79
  | "applyReportPath"
73
80
  | "validationTarget"
74
81
  | "status"
82
+ | "summary"
75
83
  | "appliedCommands"
76
84
  | "recommendedValidationCommands"
77
85
  | "commands"
@@ -99,13 +107,19 @@ export const createWorkflowApplyReport = ({
99
107
  const orderedNextActions = uniqueBy(nextActionsWithIntent, (action) => action.command).sort(
100
108
  (left, right) => nextActionPriority(left, diagnostics) - nextActionPriority(right, diagnostics),
101
109
  );
110
+ const sourceFilesChanged = uniqueBy(changedFiles, (file) => `${file.action}:${file.path}:${file.reason}`);
111
+ const generatedFilesSynced = uniqueBy(generatedFiles, (file) => `${file.action}:${file.path}:${file.reason}`);
102
112
  return {
103
113
  schemaVersion: 1,
104
114
  workflow,
105
115
  mode,
106
116
  status: workflowStatus(diagnostics),
107
- changedFiles: uniqueBy(changedFiles, (file) => `${file.action}:${file.path}:${file.reason}`),
108
- generatedFiles: uniqueBy(generatedFiles, (file) => `${file.action}:${file.path}:${file.reason}`),
117
+ summary: {
118
+ sourceFilesChanged,
119
+ generatedFilesSynced,
120
+ },
121
+ changedFiles: sourceFilesChanged,
122
+ generatedFiles: generatedFilesSynced,
109
123
  appliedCommands: uniqueBy(appliedCommands, (command) => command.command),
110
124
  recommendedValidationCommands: uniqueBy(validationCommands, (command) => command.command),
111
125
  commands: uniqueBy(validationCommands, (command) => command.command),
@@ -246,6 +260,58 @@ const statusForValidationKind = (
246
260
  return matching.some((command) => command.status === "failed") ? "failed" : "passed";
247
261
  };
248
262
 
263
+ const statusForCommands = (commands: readonly WorkflowValidationCommandResult[]): WorkflowValidationStatus => {
264
+ if (commands.length === 0) return "unknown";
265
+ return commandStatus(commands) === "failed" ? "failed" : "passed";
266
+ };
267
+
268
+ const statusForDiagnostics = (diagnostics: readonly WorkflowDiagnostic[]): WorkflowValidationStatus => {
269
+ if (diagnostics.length === 0) return "unknown";
270
+ return workflowStatus(diagnostics) === "failed" ? "failed" : "passed";
271
+ };
272
+
273
+ const workflowDiagnosticContextPaths = (diagnostics: readonly WorkflowDiagnostic[]) =>
274
+ uniqueBy(
275
+ diagnostics.flatMap((diagnostic) => diagnostic.context?.paths ?? []),
276
+ (filePath) => filePath,
277
+ );
278
+
279
+ export const createWorkflowBaselineSummary = (
280
+ diagnostics: readonly WorkflowDiagnostic[],
281
+ { detailsIncluded = true, knownBlockerCount = 0 }: { detailsIncluded?: boolean; knownBlockerCount?: number } = {},
282
+ ): WorkflowBaselineSummary => {
283
+ const grouped = new Map<string, WorkflowBaselineSummary["byCode"][number]>();
284
+ let totalErrors = 0;
285
+ let totalWarnings = 0;
286
+ for (const diagnostic of diagnostics) {
287
+ if (diagnostic.severity === "error") totalErrors += 1;
288
+ else totalWarnings += 1;
289
+ const existing = grouped.get(diagnostic.code);
290
+ if (existing) {
291
+ existing.count += 1;
292
+ if (existing.severity !== diagnostic.severity) existing.severity = "mixed";
293
+ } else {
294
+ grouped.set(diagnostic.code, {
295
+ code: diagnostic.code,
296
+ severity: diagnostic.severity,
297
+ count: 1,
298
+ sampleMessage: diagnostic.message,
299
+ });
300
+ }
301
+ }
302
+ const contextPaths = workflowDiagnosticContextPaths(diagnostics);
303
+ return {
304
+ status: totalErrors > 0 ? "failed" : diagnostics.length > 0 ? "passed" : "unknown",
305
+ total: diagnostics.length,
306
+ totalErrors,
307
+ totalWarnings,
308
+ detailsIncluded,
309
+ knownBlockerCount,
310
+ byCode: [...grouped.values()],
311
+ ...(contextPaths.length ? { contextPaths } : {}),
312
+ };
313
+ };
314
+
249
315
  const createKnownBlockers = (
250
316
  commands: readonly WorkflowValidationCommandResult[],
251
317
  diagnostics: readonly WorkflowDiagnostic[],
@@ -292,20 +358,37 @@ const createKnownBlockers = (
292
358
 
293
359
  const createValidationStatuses = (
294
360
  commands: readonly WorkflowValidationCommandResult[],
295
- diagnostics: readonly WorkflowDiagnostic[],
361
+ reportDiagnostics: readonly WorkflowDiagnostic[],
362
+ baselineDiagnostics: readonly WorkflowDiagnostic[],
363
+ workflowDiagnostics: readonly WorkflowDiagnostic[],
296
364
  ): {
297
365
  sourceStatus: WorkflowValidationStatus;
298
366
  workspaceStatus: WorkflowValidationStatus;
367
+ validationCommandsStatus: WorkflowValidationStatus;
368
+ baselineStatus: WorkflowValidationStatus;
299
369
  overallStatus: WorkflowOverallStatus;
300
370
  summary: {
301
371
  sourceChange: WorkflowValidationStatus;
302
372
  generatedSync: WorkflowValidationStatus;
373
+ validationCommands: WorkflowValidationStatus;
374
+ baseline: WorkflowValidationStatus;
303
375
  workspaceConfig: WorkflowValidationStatus;
304
376
  environment: WorkflowValidationStatus;
305
377
  };
306
378
  } => {
379
+ const diagnostics = [...reportDiagnostics, ...baselineDiagnostics, ...workflowDiagnostics];
307
380
  const scopes = [...failedCommandScopes(commands), ...errorDiagnosticScopes(diagnostics)];
308
- const sourceStatus = statusForScope(commands, diagnostics, scopes, "source-change");
381
+ const sourceDiagnostics = [...reportDiagnostics, ...workflowDiagnostics].filter(
382
+ (diagnostic) =>
383
+ diagnostic.failureScope === "source-change" ||
384
+ diagnostic.scope === "workflow" ||
385
+ (!diagnostic.failureScope && diagnostic.scope !== "baseline"),
386
+ );
387
+ const sourceScopes = [...failedCommandScopes(commands), ...errorDiagnosticScopes(sourceDiagnostics)];
388
+ const sourceStatus = statusForScope(commands, sourceDiagnostics, sourceScopes, "source-change");
389
+ const validationCommandsStatus = statusForCommands(commands);
390
+ const baselineStatus = statusForDiagnostics(baselineDiagnostics);
391
+ const nonBaselineDiagnostics = [...reportDiagnostics, ...workflowDiagnostics];
309
392
  const workspaceStatus =
310
393
  hasScopeFailure(scopes, "workspace-config", diagnostics) || hasScopeFailure(scopes, "environment", diagnostics)
311
394
  ? "failed"
@@ -314,20 +397,28 @@ const createValidationStatuses = (
314
397
  : "unknown";
315
398
  const overallStatus = hasScopeFailure(scopes, "source-change", diagnostics)
316
399
  ? "failed"
317
- : hasScopeFailure(scopes, "workspace-config", diagnostics)
318
- ? "blocked-by-workspace-config"
319
- : hasScopeFailure(scopes, "environment", diagnostics)
320
- ? "blocked-by-environment"
321
- : workflowStatus(diagnostics) === "failed" || commandStatus(commands) === "failed"
322
- ? "failed"
323
- : "passed";
400
+ : validationCommandsStatus === "passed" &&
401
+ baselineStatus === "failed" &&
402
+ workflowStatus(nonBaselineDiagnostics) !== "failed"
403
+ ? "passed-with-baseline-blockers"
404
+ : hasScopeFailure(scopes, "workspace-config", diagnostics)
405
+ ? "blocked-by-workspace-config"
406
+ : hasScopeFailure(scopes, "environment", diagnostics)
407
+ ? "blocked-by-environment"
408
+ : workflowStatus(diagnostics) === "failed" || commandStatus(commands) === "failed"
409
+ ? "failed"
410
+ : "passed";
324
411
  return {
325
412
  sourceStatus,
326
413
  workspaceStatus,
414
+ validationCommandsStatus,
415
+ baselineStatus,
327
416
  overallStatus,
328
417
  summary: {
329
418
  sourceChange: sourceStatus,
330
419
  generatedSync: statusForValidationKind(commands, "sync"),
420
+ validationCommands: validationCommandsStatus,
421
+ baseline: baselineStatus,
331
422
  workspaceConfig: statusForScope(commands, diagnostics, scopes, "workspace-config"),
332
423
  environment: statusForScope(commands, diagnostics, scopes, "environment"),
333
424
  },
@@ -377,7 +468,8 @@ export const createWorkflowValidationRunReport = async ({
377
468
  );
378
469
  const reportDiagnostics = [...diagnostics, ...commandDiagnostics];
379
470
  const scopedDiagnostics = [...reportDiagnostics, ...baselineDiagnostics, ...workflowDiagnostics];
380
- const statuses = createValidationStatuses(results, scopedDiagnostics);
471
+ const knownBlockers = createKnownBlockers(results, scopedDiagnostics);
472
+ const statuses = createValidationStatuses(results, reportDiagnostics, baselineDiagnostics, workflowDiagnostics);
381
473
  return {
382
474
  schemaVersion: 1,
383
475
  runId,
@@ -386,9 +478,12 @@ export const createWorkflowValidationRunReport = async ({
386
478
  source,
387
479
  status: statuses.overallStatus === "passed" ? "passed" : "failed",
388
480
  ...statuses,
389
- knownBlockers: createKnownBlockers(results, scopedDiagnostics),
481
+ knownBlockers,
390
482
  commands: results,
391
483
  diagnostics: reportDiagnostics,
484
+ baselineSummary: createWorkflowBaselineSummary(baselineDiagnostics, {
485
+ knownBlockerCount: knownBlockers.filter((blocker) => blocker.failureScope === "workspace-config").length,
486
+ }),
392
487
  baselineDiagnostics,
393
488
  workflowDiagnostics,
394
489
  repairActions: uniqueBy(repairActions, (action) => action.command),
@@ -97,8 +97,8 @@ const checkTypeScriptSyntax = async (workspace: Workspace, filePath: string) =>
97
97
  if (!scriptKind) return null;
98
98
  const content = await workspace.readFile(filePath);
99
99
  const source = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true, scriptKind);
100
- const diagnostic =
101
- ((source as ts.SourceFile & { parseDiagnostics?: readonly ts.DiagnosticWithLocation[] }).parseDiagnostics ?? [])[0];
100
+ const diagnostic = ((source as ts.SourceFile & { parseDiagnostics?: readonly ts.DiagnosticWithLocation[] })
101
+ .parseDiagnostics ?? [])[0];
102
102
  if (!diagnostic) return null;
103
103
  const position = diagnostic.file?.getLineAndCharacterOfPosition(diagnostic.start ?? 0);
104
104
  const location = position ? `:${position.line + 1}:${position.character + 1}` : "";
@@ -381,10 +381,10 @@ const addFieldUiSurfaceInspection = (plan: WorkflowPlan): WorkflowStepResult =>
381
381
  kind: "manual-action",
382
382
  target,
383
383
  action: templateRequested
384
- ? `Template was requested for ${field}. If no Template file changed, auto-edit was skipped because the file was missing, the generated ${module}Form/Layout.Template pattern was not found, or ${policy.component} needs option binding. Candidate position: inside Layout.Template near the existing Field components.`
385
- : `Template was not selected, so UI files are intentionally left unchanged. Candidate positions if you expose it later: Layout.Template field list for editing, Light${moduleClassName} projection for list/card data, and Unit/View card sections for display.`,
384
+ ? `Template was requested for ${field}. If no Template file changed, users will not see the field in the form yet because the file was missing, the generated ${module}Form/Layout.Template pattern was not found, or ${policy.component} needs option binding. Add it inside Layout.Template near existing Field components.`
385
+ : `Template was not selected, so users will not see ${field} in the form from this apply. If list/card display is needed, include ${field} in Light${moduleClassName} projection data and place it in the local Unit/View card layout.`,
386
386
  confidence: "medium",
387
- message: `Review UI surfaces for ${module}.${field}; recommended component is ${policy.component}, with manual reasons and candidate positions in action.`,
387
+ message: `Review user-visible UI for ${module}.${field}; recommended component is ${policy.component}.`,
388
388
  },
389
389
  ],
390
390
  nextActions: [
package/workflow/plan.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { capitalize } from "akanjs/common";
2
+ import { workflowPlanApproval } from "./artifacts";
2
3
  import { coerceFieldDefault, moduleSourcePaths } from "./source";
3
4
  import type {
4
5
  WorkflowDiagnostic,
@@ -91,11 +92,59 @@ const createAddFieldDefaultRecommendations = (inputs: Record<string, WorkflowInp
91
92
  kind: "manual-action",
92
93
  confidence: "high",
93
94
  message: `Default for ${field} will be normalized to ${coercion.normalizedType} literal ${coercion.expression}.`,
94
- action: "Review the normalized default in the apply report if this value should stay unset.",
95
+ action:
96
+ "Review the normalized default in the apply report. To leave the field without a default, omit the default input.",
95
97
  },
96
98
  ];
97
99
  };
98
100
 
101
+ const moneyLikeFieldPattern = /(budget|price|amount|cost|rate|fee|salary|balance)/i;
102
+ const wholeNumberFieldPattern = /(count|quantity|total|number|index|rank|order|age)/i;
103
+
104
+ const createAddFieldInputRecommendations = (inputs: Record<string, WorkflowInputValue>): WorkflowRecommendation[] => {
105
+ const field = typeof inputs.field === "string" ? inputs.field : "<field>";
106
+ const typeName = typeof inputs.type === "string" ? inputs.type : null;
107
+ const recommendations: WorkflowRecommendation[] = [];
108
+ if (!typeName) return recommendations;
109
+ if (typeName.toLowerCase() === "number" || typeName.toLowerCase() === "numeric") {
110
+ const recommendedType = moneyLikeFieldPattern.test(field)
111
+ ? "Float"
112
+ : wholeNumberFieldPattern.test(field)
113
+ ? "Int"
114
+ : null;
115
+ recommendations.push({
116
+ code: "add-field-type-choice",
117
+ kind: "input-guidance",
118
+ confidence: recommendedType ? "high" : "medium",
119
+ message: recommendedType
120
+ ? `Use ${recommendedType} for ${field}; Float is recommended for money-like decimal values and Int for whole-number values.`
121
+ : `Choose Int for whole-number ${field} values or Float for decimal ${field} values.`,
122
+ action: recommendedType
123
+ ? `Re-run plan_workflow with type="${recommendedType}".`
124
+ : 'Re-run plan_workflow with type="Int" or type="Float".',
125
+ });
126
+ }
127
+ if (typeName.toLowerCase() === "enum" && !Array.isArray(inputs.values)) {
128
+ recommendations.push({
129
+ code: "add-field-enum-values",
130
+ kind: "input-guidance",
131
+ confidence: "high",
132
+ message: `Enum field ${field} needs values before apply can choose valid dictionary and default behavior.`,
133
+ action: 'Pass values as an array or comma-separated string, for example values=["draft","done"].',
134
+ });
135
+ }
136
+ if (inputs.default === undefined) {
137
+ recommendations.push({
138
+ code: "add-field-default-optional",
139
+ kind: "input-guidance",
140
+ confidence: "medium",
141
+ message: `No default will be written for ${field} because default is omitted.`,
142
+ action: "Keep default omitted when the field should have no default value.",
143
+ });
144
+ }
145
+ return recommendations;
146
+ };
147
+
99
148
  const createAddFieldRecommendations = (inputs: Record<string, WorkflowInputValue>): WorkflowRecommendation[] => {
100
149
  const app = typeof inputs.app === "string" ? inputs.app : "<app-or-lib>";
101
150
  const module = typeof inputs.module === "string" ? inputs.module : "<module>";
@@ -163,9 +212,11 @@ const createAddFieldRecommendations = (inputs: Record<string, WorkflowInputValue
163
212
  code: "add-field-light-projection-choice",
164
213
  kind: "manual-action" as const,
165
214
  target: paths.constant,
166
- action: `Pass includeInLight=true when ${field} should appear in Light${capitalize(module)} list/card projections.`,
215
+ action: `Pass includeInLight=true when users should see ${field} in list/card data. Without it, the field can exist on ${capitalize(
216
+ module,
217
+ )}Input but stay absent from Light${capitalize(module)} projections.`,
167
218
  confidence: "medium" as const,
168
- message: `Light projection exposure for ${module}.${field} is not selected yet.`,
219
+ message: `${module}.${field} is not selected for list/card projection data.`,
169
220
  },
170
221
  ]
171
222
  : []),
@@ -187,7 +238,8 @@ const createAddFieldRecommendations = (inputs: Record<string, WorkflowInputValue
187
238
  kind: "manual-action" as const,
188
239
  target: paths.template,
189
240
  confidence: "medium" as const,
190
- message: `Template is not included in surfaces, so ${module}.${field} will not be auto-rendered there.`,
241
+ message: `Template form auto-edit is skipped for ${module}.${field} because surfaces does not include "template".`,
242
+ action: `Pass surfaces=["template"] when users should enter ${field} in the Template form.`,
191
243
  },
192
244
  ]
193
245
  : []),
@@ -195,13 +247,14 @@ const createAddFieldRecommendations = (inputs: Record<string, WorkflowInputValue
195
247
  code: "add-field-ui-manual-review",
196
248
  kind: "manual-action",
197
249
  target: paths.template,
198
- action: `Review ${app}:${module} UI after apply. Template auto-edit requires a generated ${module}Form hook and Layout.Template field list; Unit/View are not auto-edited because list/card placement depends on local layout. Candidate positions: Layout.Template before the closing tag, Light${capitalize(
250
+ action: `After apply, verify what users can see: Template form auto-edit only runs when a generated ${module}Form hook and Layout.Template field list are present. Unit/View cards are not auto-edited, so ${field} may be stored on ${capitalize(
199
251
  module,
200
- )} projection array for list data, and Unit/View card sections for display.`,
252
+ )} but not shown in list/card UI until you place it in the local card layout.`,
201
253
  confidence: "medium",
202
- message: "UI manual review includes the reason auto-edit may be skipped and candidate insertion positions.",
254
+ message: `${app}:${module}.${field} may need UI review for user-visible Template, Unit, and View behavior.`,
203
255
  },
204
256
  ...createAddFieldDefaultRecommendations(inputs),
257
+ ...createAddFieldInputRecommendations(inputs),
205
258
  ];
206
259
  };
207
260
 
@@ -371,5 +424,6 @@ export const createWorkflowPlan = (spec: WorkflowSpec, rawInputs: Record<string,
371
424
  diagnostics,
372
425
  recommendations: createWorkflowPlanRecommendations(spec, inputs),
373
426
  requiresApproval: true,
427
+ approval: workflowPlanApproval,
374
428
  };
375
429
  };
@@ -51,6 +51,7 @@ export const renderWorkflowPlan = (plan: WorkflowPlan) =>
51
51
  "",
52
52
  `- Mode: ${plan.mode}`,
53
53
  `- Requires approval: ${plan.requiresApproval}`,
54
+ `- Approval: ${plan.approval?.meaning ?? "Review this read-only plan before applying workflow mutations."}`,
54
55
  "",
55
56
  "## Inputs",
56
57
  ...Object.entries(plan.inputs).map(
@@ -108,6 +109,10 @@ const applySourceStatus = (report: WorkflowApplyReport) =>
108
109
  : "passed";
109
110
 
110
111
  export const renderWorkflowApplyReport = (report: WorkflowApplyReport) => {
112
+ const applySummary = {
113
+ sourceFilesChanged: report.summary?.sourceFilesChanged ?? report.changedFiles,
114
+ generatedFilesSynced: report.summary?.generatedFilesSynced ?? report.generatedFiles,
115
+ };
111
116
  const manualReviewItems = [
112
117
  ...report.diagnostics.filter((diagnostic) => diagnostic.severity === "warning").map(renderDiagnostic),
113
118
  ...report.recommendations
@@ -137,6 +142,8 @@ export const renderWorkflowApplyReport = (report: WorkflowApplyReport) => {
137
142
  `- Status: ${report.status}`,
138
143
  `- Source-change status: ${applySourceStatus(report)}`,
139
144
  `- Workspace status: ${validationBlockers.length ? "blocked" : "not blocked during apply"}`,
145
+ `- Source files changed: ${applySummary.sourceFilesChanged.length}`,
146
+ `- Generated files queued for sync: ${applySummary.generatedFilesSynced.length}`,
140
147
  ...(report.validationTarget ? [`- Validation target: ${report.validationTarget}`] : []),
141
148
  "",
142
149
  "## Apply Checks",
@@ -148,13 +155,13 @@ export const renderWorkflowApplyReport = (report: WorkflowApplyReport) => {
148
155
  ...(sourceBlockers.length ? sourceBlockers : ["- none"]),
149
156
  "",
150
157
  "## Automatically Modified",
151
- ...(report.changedFiles.length
152
- ? report.changedFiles.map((file) => `- \`${file.action}\` ${file.path}: ${file.reason}`)
158
+ ...(applySummary.sourceFilesChanged.length
159
+ ? applySummary.sourceFilesChanged.map((file) => `- \`${file.action}\` ${file.path}: ${file.reason}`)
153
160
  : ["- none"]),
154
161
  "",
155
162
  "## Generated Sync",
156
- ...(report.generatedFiles.length
157
- ? report.generatedFiles.map((file) => `- \`${file.action}\` ${file.path}: ${file.reason}`)
163
+ ...(applySummary.generatedFilesSynced.length
164
+ ? applySummary.generatedFilesSynced.map((file) => `- \`${file.action}\` ${file.path}: ${file.reason}`)
158
165
  : ["- none"]),
159
166
  "",
160
167
  "## Applied Commands",
@@ -192,19 +199,32 @@ export const renderWorkflowApplyReport = (report: WorkflowApplyReport) => {
192
199
  export const renderWorkflowApply = (report: WorkflowApplyReport, format: WorkflowFormat = "markdown") =>
193
200
  format === "json" ? jsonText(report) : renderWorkflowApplyReport(report);
194
201
 
195
- export const renderWorkflowValidationRunReport = (report: WorkflowValidationRunReport) =>
196
- [
202
+ export const renderWorkflowValidationRunReport = (report: WorkflowValidationRunReport) => {
203
+ const baselineSummary = report.baselineSummary ?? {
204
+ status: report.baselineDiagnostics?.some((diagnostic) => diagnostic.severity === "error") ? "failed" : "unknown",
205
+ total: report.baselineDiagnostics?.length ?? 0,
206
+ totalErrors: report.baselineDiagnostics?.filter((diagnostic) => diagnostic.severity === "error").length ?? 0,
207
+ totalWarnings: report.baselineDiagnostics?.filter((diagnostic) => diagnostic.severity === "warning").length ?? 0,
208
+ detailsIncluded: true,
209
+ knownBlockerCount: report.knownBlockers.filter((blocker) => blocker.known).length,
210
+ byCode: [],
211
+ };
212
+ return [
197
213
  `# Workflow Validation: ${report.workflow}`,
198
214
  "",
199
215
  `- Run: ${report.runId}`,
200
216
  `- Status: ${report.status}`,
201
217
  `- Source status: ${report.sourceStatus}`,
202
218
  `- Workspace status: ${report.workspaceStatus}`,
219
+ `- Validation commands status: ${report.validationCommandsStatus ?? "unknown"}`,
220
+ `- Baseline status: ${report.baselineStatus ?? baselineSummary.status}`,
203
221
  `- Overall status: ${report.overallStatus}`,
204
222
  "",
205
223
  "## Status Summary",
206
224
  `- Source-change: ${report.summary.sourceChange}`,
207
225
  `- Generated sync: ${report.summary.generatedSync}`,
226
+ `- Validation commands: ${report.summary.validationCommands ?? "unknown"}`,
227
+ `- Baseline: ${report.summary.baseline ?? baselineSummary.status}`,
208
228
  `- Workspace config: ${report.summary.workspaceConfig}`,
209
229
  `- Environment: ${report.summary.environment}`,
210
230
  "",
@@ -216,11 +236,28 @@ export const renderWorkflowValidationRunReport = (report: WorkflowValidationRunR
216
236
  : ["- none"]),
217
237
  "",
218
238
  "## Existing Workspace Blockers",
239
+ `- Status: ${baselineSummary.status}`,
240
+ `- Errors: ${baselineSummary.totalErrors}`,
241
+ `- Warnings: ${baselineSummary.totalWarnings}`,
242
+ ...(baselineSummary.byCode.length
243
+ ? baselineSummary.byCode.map(
244
+ (item) => `- [${item.severity}] ${item.code} (${item.count}x): ${item.sampleMessage}`,
245
+ )
246
+ : ["- none"]),
247
+ ...(baselineSummary.detailsIncluded
248
+ ? []
249
+ : [
250
+ "- Baseline details are summarized by default. Re-run validation with includeBaselineDetails=true for full baselineDiagnostics.",
251
+ ]),
252
+ "",
253
+ "## Existing Workspace Blocker Details",
219
254
  ...(report.baselineDiagnostics?.length
220
255
  ? report.baselineDiagnostics.map(
221
256
  (diagnostic) => `- [${diagnostic.severity}] ${diagnostic.code}: ${diagnostic.message}`,
222
257
  )
223
- : ["- none"]),
258
+ : baselineSummary.detailsIncluded
259
+ ? ["- none"]
260
+ : ["- omitted"]),
224
261
  "",
225
262
  "## Known Blockers",
226
263
  ...(report.knownBlockers.length
@@ -256,6 +293,7 @@ export const renderWorkflowValidationRunReport = (report: WorkflowValidationRunR
256
293
  : ["- none"]),
257
294
  "",
258
295
  ].join("\n");
296
+ };
259
297
 
260
298
  export const renderWorkflowValidation = (report: WorkflowValidationRunReport, format: WorkflowFormat = "markdown") =>
261
299
  format === "json" ? jsonText(report) : renderWorkflowValidationRunReport(report);
@@ -23,6 +23,10 @@ export interface ToolingRolloutViolation {
23
23
  reason: string;
24
24
  }
25
25
 
26
+ type RestrictedToolingRolloutCandidate = ToolingRolloutCandidate & {
27
+ status: Exclude<ToolingRolloutCandidateStatus, "allowed">;
28
+ };
29
+
26
30
  const dependencySections = [
27
31
  "dependencies",
28
32
  "devDependencies",
@@ -90,14 +94,13 @@ export const toolingRolloutGate = {
90
94
  candidates: toolingRolloutCandidates,
91
95
  };
92
96
 
93
- const isRestrictedCandidate = (
94
- candidate: ToolingRolloutCandidate,
95
- ): candidate is ToolingRolloutCandidate & { status: Exclude<ToolingRolloutCandidateStatus, "allowed"> } =>
97
+ const isRestrictedCandidate = (candidate: ToolingRolloutCandidate): candidate is RestrictedToolingRolloutCandidate =>
96
98
  candidate.status !== "allowed";
97
99
 
98
- const restrictedCandidates = new Map(
99
- toolingRolloutCandidates.filter(isRestrictedCandidate).map((candidate) => [candidate.packageName, candidate]),
100
- );
100
+ const restrictedCandidates = new Map<string, RestrictedToolingRolloutCandidate>();
101
+ for (const candidate of toolingRolloutCandidates) {
102
+ if (isRestrictedCandidate(candidate)) restrictedCandidates.set(candidate.packageName, candidate);
103
+ }
101
104
 
102
105
  const blockedTypeScriptVersion = (version: string) =>
103
106
  /(?:^|[^\w])(?:rc|next|beta|canary|insiders)(?:[^\w]|$)/i.test(version) || /typescript@rc/i.test(version);
package/workflow/types.ts CHANGED
@@ -14,10 +14,17 @@ export type WorkflowValidationStatus = "passed" | "failed" | "unknown";
14
14
  export interface WorkflowValidationSummary {
15
15
  sourceChange: WorkflowValidationStatus;
16
16
  generatedSync: WorkflowValidationStatus;
17
+ validationCommands: WorkflowValidationStatus;
18
+ baseline: WorkflowValidationStatus;
17
19
  workspaceConfig: WorkflowValidationStatus;
18
20
  environment: WorkflowValidationStatus;
19
21
  }
20
- export type WorkflowOverallStatus = "passed" | "failed" | "blocked-by-workspace-config" | "blocked-by-environment";
22
+ export type WorkflowOverallStatus =
23
+ | "passed"
24
+ | "failed"
25
+ | "passed-with-baseline-blockers"
26
+ | "blocked-by-workspace-config"
27
+ | "blocked-by-environment";
21
28
 
22
29
  export interface PrimitiveTargetInput {
23
30
  app: string | null;
@@ -102,12 +109,19 @@ export interface WorkflowDiagnostic {
102
109
  export interface WorkflowRecommendation {
103
110
  code: string;
104
111
  message: string;
105
- kind: "auto-apply" | "validation" | "manual-action" | "import" | "placement" | "ui-component";
112
+ kind: "auto-apply" | "validation" | "manual-action" | "input-guidance" | "import" | "placement" | "ui-component";
106
113
  target?: string;
107
114
  action?: string;
108
115
  confidence?: "high" | "medium" | "low";
109
116
  }
110
117
 
118
+ export interface WorkflowPlanApproval {
119
+ required: true;
120
+ meaning: string;
121
+ applyTool: "apply_workflow";
122
+ canApplyWith?: Record<string, string>;
123
+ }
124
+
111
125
  export interface AkanSourceSpan {
112
126
  file: string;
113
127
  startLine: number;
@@ -247,6 +261,7 @@ export interface WorkflowPlan {
247
261
  diagnostics: WorkflowDiagnostic[];
248
262
  recommendations: WorkflowRecommendation[];
249
263
  requiresApproval: true;
264
+ approval?: WorkflowPlanApproval;
250
265
  }
251
266
 
252
267
  export interface WorkflowReport {
@@ -291,6 +306,24 @@ export interface WorkflowKnownBlocker {
291
306
  known?: boolean;
292
307
  }
293
308
 
309
+ export interface WorkflowBaselineSummaryCode {
310
+ code: string;
311
+ severity: "warning" | "error" | "mixed";
312
+ count: number;
313
+ sampleMessage: string;
314
+ }
315
+
316
+ export interface WorkflowBaselineSummary {
317
+ status: WorkflowValidationStatus;
318
+ total: number;
319
+ totalErrors: number;
320
+ totalWarnings: number;
321
+ detailsIncluded: boolean;
322
+ knownBlockerCount: number;
323
+ byCode: WorkflowBaselineSummaryCode[];
324
+ contextPaths?: string[];
325
+ }
326
+
294
327
  export interface RepairAction {
295
328
  command: string;
296
329
  reason: string;
@@ -351,6 +384,10 @@ export interface WorkflowApplyReport {
351
384
  workflow: string;
352
385
  mode: "dry-run" | "apply";
353
386
  status: "passed" | "failed";
387
+ summary: {
388
+ sourceFilesChanged: PrimitiveChangedFile[];
389
+ generatedFilesSynced: PrimitiveGeneratedFile[];
390
+ };
354
391
  changedFiles: PrimitiveChangedFile[];
355
392
  generatedFiles: PrimitiveGeneratedFile[];
356
393
  appliedCommands: WorkflowApplyCommand[];
@@ -372,11 +409,14 @@ export interface WorkflowValidationRunReport {
372
409
  status: "passed" | "failed";
373
410
  sourceStatus: WorkflowValidationStatus;
374
411
  workspaceStatus: WorkflowValidationStatus;
412
+ validationCommandsStatus: WorkflowValidationStatus;
413
+ baselineStatus: WorkflowValidationStatus;
375
414
  summary: WorkflowValidationSummary;
376
415
  overallStatus: WorkflowOverallStatus;
377
416
  knownBlockers: WorkflowKnownBlocker[];
378
417
  commands: WorkflowValidationCommandResult[];
379
418
  diagnostics: WorkflowDiagnostic[];
419
+ baselineSummary: WorkflowBaselineSummary;
380
420
  baselineDiagnostics?: WorkflowDiagnostic[];
381
421
  workflowDiagnostics?: WorkflowDiagnostic[];
382
422
  repairActions: RepairAction[];