@workflow-cannon/workspace-kit 0.10.0 → 0.11.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 CHANGED
@@ -65,6 +65,7 @@ This keeps automation adaptive without sacrificing safety, governance, or develo
65
65
 
66
66
  - **Phases 0–7** are complete through **`v0.9.0`** (see roadmap for slice ids).
67
67
  - **Phase 8** ships maintainer/onboarding hardening (`v0.10.0`): policy denial clarity, runbooks, and doc alignment for CLI vs `run` approval.
68
+ - **Phase 9–10** ship agent/onboarding parity (`v0.11.0`): interactive policy opt-in, strict response-template mode, Agent CLI map (`docs/maintainers/AGENT-CLI-MAP.md`), and CLI-first Cursor guidance.
68
69
 
69
70
  Historical note: this file’s milestone list is not the live queue—always check task state for **`ready`** work.
70
71
 
@@ -0,0 +1,16 @@
1
+ export type PolicyPromptIo = {
2
+ writeError: (message: string) => void;
3
+ readStdinLine?: () => Promise<string | null>;
4
+ };
5
+ /** Enable TTY interactive approval for sensitive `workspace-kit run` when truthy (`on`, `1`, `true`, `yes`). */
6
+ export declare function isInteractiveApprovalEnabled(env: NodeJS.ProcessEnv): boolean;
7
+ export type InteractiveApprovalChoice = {
8
+ kind: "deny";
9
+ } | {
10
+ kind: "approve";
11
+ scope: "once" | "session";
12
+ };
13
+ /**
14
+ * Prompt for Deny / Allow once / Allow for session. Returns deny if user cancels or input unrecognized.
15
+ */
16
+ export declare function promptSensitiveRunApproval(io: PolicyPromptIo, operationId: string, commandLabel: string, env: NodeJS.ProcessEnv): Promise<InteractiveApprovalChoice | null>;
@@ -0,0 +1,53 @@
1
+ import { createInterface } from "node:readline/promises";
2
+ /** Enable TTY interactive approval for sensitive `workspace-kit run` when truthy (`on`, `1`, `true`, `yes`). */
3
+ export function isInteractiveApprovalEnabled(env) {
4
+ const v = env.WORKSPACE_KIT_INTERACTIVE_APPROVAL?.trim().toLowerCase();
5
+ return v === "1" || v === "true" || v === "on" || v === "yes";
6
+ }
7
+ function canUseInteractivePrompt(io) {
8
+ if (io.readStdinLine) {
9
+ return true;
10
+ }
11
+ return process.stdin.isTTY === true && process.stdout.isTTY === true;
12
+ }
13
+ async function readOneLine(io) {
14
+ if (io.readStdinLine) {
15
+ return io.readStdinLine();
16
+ }
17
+ const rl = createInterface({ input: process.stdin, output: process.stderr });
18
+ try {
19
+ const line = await rl.question("");
20
+ return line;
21
+ }
22
+ finally {
23
+ rl.close();
24
+ }
25
+ }
26
+ /**
27
+ * Prompt for Deny / Allow once / Allow for session. Returns deny if user cancels or input unrecognized.
28
+ */
29
+ export async function promptSensitiveRunApproval(io, operationId, commandLabel, env) {
30
+ if (!isInteractiveApprovalEnabled(env) || !canUseInteractivePrompt(io)) {
31
+ return null;
32
+ }
33
+ const session = env.WORKSPACE_KIT_SESSION_ID?.trim() || "default";
34
+ io.writeError(`workspace-kit: sensitive command '${commandLabel}' requires approval (${operationId}).\n` +
35
+ ` [d] Deny [o] Allow once [s] Allow for this session (WORKSPACE_KIT_SESSION_ID=${session})\n` +
36
+ `Choice (d/o/s): `);
37
+ const raw = await readOneLine(io);
38
+ if (raw === null) {
39
+ return { kind: "deny" };
40
+ }
41
+ const c = raw.trim().toLowerCase();
42
+ if (c === "d" || c === "deny" || c === "n" || c === "no") {
43
+ return { kind: "deny" };
44
+ }
45
+ if (c === "o" || c === "once" || c === "1" || c === "y" || c === "yes" || c === "a") {
46
+ return { kind: "approve", scope: "once" };
47
+ }
48
+ if (c === "s" || c === "session" || c === "2") {
49
+ return { kind: "approve", scope: "session" };
50
+ }
51
+ io.writeError(`Unrecognized choice '${raw.trim()}'; treating as deny.\n`);
52
+ return { kind: "deny" };
53
+ }
@@ -1,6 +1,8 @@
1
1
  export type RunCommandIo = {
2
2
  writeLine: (message: string) => void;
3
3
  writeError: (message: string) => void;
4
+ /** Test hook: return one line of simulated stdin for interactive policy approval */
5
+ readStdinLine?: () => Promise<string | null>;
4
6
  };
5
7
  export type RunCommandExitCodes = {
6
8
  success: number;
@@ -1,6 +1,6 @@
1
1
  import { ModuleRegistry } from "../core/module-registry.js";
2
2
  import { ModuleCommandRouter } from "../core/module-command-router.js";
3
- import { appendPolicyTrace, isSensitiveModuleCommandForEffective, parsePolicyApproval, POLICY_APPROVAL_HUMAN_DOC, resolveActorWithFallback, resolvePolicyOperationIdForCommand } from "../core/policy.js";
3
+ import { appendPolicyTrace, isSensitiveModuleCommandForEffective, parsePolicyApproval, AGENT_CLI_MAP_HUMAN_DOC, POLICY_APPROVAL_HUMAN_DOC, resolveActorWithFallback, resolvePolicyOperationIdForCommand } from "../core/policy.js";
4
4
  import { getSessionGrant, recordSessionGrant, resolveSessionId } from "../core/session-policy.js";
5
5
  import { applyResponseTemplateApplication } from "../core/response-template-shaping.js";
6
6
  import { resolveWorkspaceConfigWithLayers } from "../core/workspace-kit-config.js";
@@ -10,6 +10,7 @@ import { approvalsModule } from "../modules/approvals/index.js";
10
10
  import { planningModule } from "../modules/planning/index.js";
11
11
  import { improvementModule } from "../modules/improvement/index.js";
12
12
  import { workspaceConfigModule } from "../modules/workspace-config/index.js";
13
+ import { promptSensitiveRunApproval } from "./interactive-policy.js";
13
14
  export async function handleRunCommand(cwd, args, io, codes) {
14
15
  const { writeLine, writeError } = io;
15
16
  const allModules = [
@@ -31,7 +32,9 @@ export async function handleRunCommand(cwd, args, io, codes) {
31
32
  writeLine(` ${cmd.name} (${cmd.moduleId})${desc}`);
32
33
  }
33
34
  writeLine("");
34
- writeLine("Usage: workspace-kit run <command> [json-args]");
35
+ writeLine(`Usage: workspace-kit run <command> [json-args]`);
36
+ writeLine(`Instruction files: src/modules/<module>/instructions/<command>.md — policy-sensitive runs need JSON policyApproval (${POLICY_APPROVAL_HUMAN_DOC}).`);
37
+ writeLine(`Agent-oriented tier table + copy-paste patterns: ${AGENT_CLI_MAP_HUMAN_DOC}.`);
35
38
  return codes.success;
36
39
  }
37
40
  let commandArgs = {};
@@ -76,6 +79,7 @@ export async function handleRunCommand(cwd, args, io, codes) {
76
79
  const policyOp = resolvePolicyOperationIdForCommand(subcommand, effective);
77
80
  const explicitPolicyApproval = parsePolicyApproval(commandArgs);
78
81
  let resolvedSensitiveApproval = explicitPolicyApproval;
82
+ let interactiveSessionFollowup = false;
79
83
  if (sensitive) {
80
84
  if (!resolvedSensitiveApproval && policyOp) {
81
85
  const grant = await getSessionGrant(cwd, policyOp, sessionId);
@@ -83,6 +87,37 @@ export async function handleRunCommand(cwd, args, io, codes) {
83
87
  resolvedSensitiveApproval = { confirmed: true, rationale: grant.rationale };
84
88
  }
85
89
  }
90
+ if (!resolvedSensitiveApproval && policyOp) {
91
+ const interactive = await promptSensitiveRunApproval({ writeError, readStdinLine: io.readStdinLine }, policyOp, `run ${subcommand}`, process.env);
92
+ if (interactive?.kind === "deny") {
93
+ await appendPolicyTrace(cwd, {
94
+ timestamp: new Date().toISOString(),
95
+ operationId: policyOp,
96
+ command: `run ${subcommand}`,
97
+ actor,
98
+ allowed: false,
99
+ message: "interactive policy approval denied"
100
+ });
101
+ writeLine(JSON.stringify({
102
+ ok: false,
103
+ code: "policy-denied",
104
+ operationId: policyOp,
105
+ remediationDoc: POLICY_APPROVAL_HUMAN_DOC,
106
+ message: "Sensitive command denied at interactive policy prompt.",
107
+ hint: `Set WORKSPACE_KIT_INTERACTIVE_APPROVAL=off or pass policyApproval in JSON. See ${POLICY_APPROVAL_HUMAN_DOC}.`
108
+ }, null, 2));
109
+ return codes.validationFailure;
110
+ }
111
+ if (interactive?.kind === "approve") {
112
+ const rationale = interactive.scope === "session" ? "interactive-approval-session" : "interactive-approval-once";
113
+ resolvedSensitiveApproval = {
114
+ confirmed: true,
115
+ rationale,
116
+ ...(interactive.scope === "session" ? { scope: "session" } : {})
117
+ };
118
+ interactiveSessionFollowup = interactive.scope === "session";
119
+ }
120
+ }
86
121
  if (!resolvedSensitiveApproval) {
87
122
  if (policyOp) {
88
123
  await appendPolicyTrace(cwd, {
@@ -101,7 +136,7 @@ export async function handleRunCommand(cwd, args, io, codes) {
101
136
  remediationDoc: POLICY_APPROVAL_HUMAN_DOC,
102
137
  message: 'Sensitive command requires policyApproval in JSON args (or an existing session grant for this operation). Example: {"policyApproval":{"confirmed":true,"rationale":"why","scope":"session"}}. See remediationDoc for env vs JSON approval surfaces.',
103
138
  hint: policyOp != null
104
- ? `Operation ${policyOp} requires explicit approval; WORKSPACE_KIT_POLICY_APPROVAL is not read for workspace-kit run.`
139
+ ? `Operation ${policyOp} requires explicit approval; WORKSPACE_KIT_POLICY_APPROVAL is not read for workspace-kit run. Optional: set WORKSPACE_KIT_INTERACTIVE_APPROVAL=on in a TTY for a prompt (see ${POLICY_APPROVAL_HUMAN_DOC}).`
105
140
  : "Operation could not be mapped to policyOperationId; check policy.extraSensitiveModuleCommands and pass policyApproval in JSON args."
106
141
  }, null, 2));
107
142
  return codes.validationFailure;
@@ -127,8 +162,10 @@ export async function handleRunCommand(cwd, args, io, codes) {
127
162
  commandOk: rawResult.ok,
128
163
  message: rawResult.message
129
164
  });
130
- if (explicitPolicyApproval?.scope === "session" && rawResult.ok) {
131
- await recordSessionGrant(cwd, policyOp, sessionId, explicitPolicyApproval.rationale);
165
+ const recordSession = rawResult.ok &&
166
+ (explicitPolicyApproval?.scope === "session" || interactiveSessionFollowup);
167
+ if (recordSession) {
168
+ await recordSessionGrant(cwd, policyOp, sessionId, resolvedSensitiveApproval.rationale);
132
169
  }
133
170
  }
134
171
  const result = applyResponseTemplateApplication(subcommand, commandArgs, rawResult, effective);
package/dist/cli.d.ts CHANGED
@@ -9,6 +9,8 @@ export type WorkspaceKitCliOptions = {
9
9
  cwd?: string;
10
10
  writeLine?: (message: string) => void;
11
11
  writeError?: (message: string) => void;
12
+ /** Test hook: simulated stdin lines for interactive sensitive-command approval */
13
+ readStdinLine?: () => Promise<string | null>;
12
14
  };
13
15
  export declare function parseJsonFile(filePath: string): Promise<unknown>;
14
16
  export declare function runCli(args: string[], options?: WorkspaceKitCliOptions): Promise<number>;
package/dist/cli.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import fs from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import { pathToFileURL } from "node:url";
5
- import { appendPolicyTrace, parsePolicyApprovalFromEnv, resolveActorWithFallback } from "./core/policy.js";
5
+ import { AGENT_CLI_MAP_HUMAN_DOC, appendPolicyTrace, parsePolicyApprovalFromEnv, resolveActorWithFallback } from "./core/policy.js";
6
6
  import { runWorkspaceConfigCli } from "./core/config-cli.js";
7
7
  import { handleRunCommand } from "./cli/run-command.js";
8
8
  const EXIT_SUCCESS = 0;
@@ -335,6 +335,7 @@ export async function runCli(args, options = {}) {
335
335
  const cwd = options.cwd ?? process.cwd();
336
336
  const writeLine = options.writeLine ?? console.log;
337
337
  const writeError = options.writeError ?? console.error;
338
+ const readStdinLine = options.readStdinLine;
338
339
  const [command] = args;
339
340
  if (!command) {
340
341
  writeError("Usage: workspace-kit <init|doctor|check|upgrade|drift-check|run|config>");
@@ -571,7 +572,7 @@ export async function runCli(args, options = {}) {
571
572
  return EXIT_SUCCESS;
572
573
  }
573
574
  if (command === "run") {
574
- return handleRunCommand(cwd, args, { writeLine, writeError }, {
575
+ return handleRunCommand(cwd, args, { writeLine, writeError, readStdinLine }, {
575
576
  success: EXIT_SUCCESS,
576
577
  validationFailure: EXIT_VALIDATION_FAILURE,
577
578
  usageError: EXIT_USAGE_ERROR,
@@ -614,6 +615,7 @@ export async function runCli(args, options = {}) {
614
615
  }
615
616
  writeLine("workspace-kit doctor passed.");
616
617
  writeLine("All canonical workspace-kit contract files are present and parseable JSON.");
618
+ writeLine(`Next: workspace-kit run — list module commands; see ${AGENT_CLI_MAP_HUMAN_DOC} for tier/policy copy-paste.`);
617
619
  return EXIT_SUCCESS;
618
620
  }
619
621
  async function main() {
@@ -163,7 +163,7 @@ const REGISTRY = {
163
163
  "responseTemplates.enforcementMode": {
164
164
  key: "responseTemplates.enforcementMode",
165
165
  type: "string",
166
- description: "Whether unknown/mismatched response templates fail commands (`strict`) or only emit warnings (`advisory`).",
166
+ description: "`advisory`: unknown template ids, invalid default/override ids, and explicit-vs-directive template conflicts emit warnings only. `strict`: same conditions fail the command (`response-template-invalid` or `response-template-conflict`) after the module runs; use for CI governance.",
167
167
  default: "advisory",
168
168
  allowedValues: ["advisory", "strict"],
169
169
  domainScope: "project",
@@ -1,6 +1,8 @@
1
1
  export declare const POLICY_TRACE_SCHEMA_VERSION: 1;
2
2
  /** Maintainer doc (repo-relative) linked from policy denial output for `workspace-kit run`. */
3
3
  export declare const POLICY_APPROVAL_HUMAN_DOC = "docs/maintainers/POLICY-APPROVAL.md";
4
+ /** Maintainer doc: tier table + copy-paste patterns for agents (Tier A/B `run` vs CLI env approval). */
5
+ export declare const AGENT_CLI_MAP_HUMAN_DOC = "docs/maintainers/AGENT-CLI-MAP.md";
4
6
  export type PolicyOperationId = "cli.upgrade" | "cli.init" | "cli.config-mutate" | "policy.dynamic-sensitive" | "doc.document-project" | "doc.generate-document" | "tasks.run-transition" | "approvals.review-item" | "improvement.generate-recommendations" | "improvement.ingest-transcripts";
5
7
  export declare function getOperationIdForCommand(commandName: string): PolicyOperationId | undefined;
6
8
  export declare function getExtraSensitiveModuleCommandsFromEffective(effective: Record<string, unknown>): string[];
@@ -4,6 +4,8 @@ import { execFile } from "node:child_process";
4
4
  export const POLICY_TRACE_SCHEMA_VERSION = 1;
5
5
  /** Maintainer doc (repo-relative) linked from policy denial output for `workspace-kit run`. */
6
6
  export const POLICY_APPROVAL_HUMAN_DOC = "docs/maintainers/POLICY-APPROVAL.md";
7
+ /** Maintainer doc: tier table + copy-paste patterns for agents (Tier A/B `run` vs CLI env approval). */
8
+ export const AGENT_CLI_MAP_HUMAN_DOC = "docs/maintainers/AGENT-CLI-MAP.md";
7
9
  const COMMAND_TO_OPERATION = {
8
10
  "document-project": "doc.document-project",
9
11
  "generate-document": "doc.generate-document",
@@ -1,6 +1,7 @@
1
1
  import type { ModuleCommandResult } from "../contracts/module-contract.js";
2
2
  /**
3
3
  * Apply response template metadata and optional presentation hints (T262, T265).
4
- * Advisory mode never flips `ok`. Strict mode fails closed on unknown template ids when a template was explicitly requested or command override is set.
4
+ * Advisory mode never flips `ok` for template issues. Strict mode fails closed on unknown resolved template ids
5
+ * (explicit, override, or default) and on explicit-vs-directive conflicts (`response-template-conflict`).
5
6
  */
6
7
  export declare function applyResponseTemplateApplication(commandName: string, args: Record<string, unknown>, result: ModuleCommandResult, effective: Record<string, unknown>): ModuleCommandResult;
@@ -24,6 +24,7 @@ function readResponseTemplatesConfig(effective) {
24
24
  }
25
25
  function resolveRequestedTemplateId(commandName, args) {
26
26
  const parseWarnings = [];
27
+ const strictViolations = [];
27
28
  const explicit = typeof args.responseTemplateId === "string" && args.responseTemplateId.trim()
28
29
  ? args.responseTemplateId.trim()
29
30
  : null;
@@ -43,12 +44,13 @@ function resolveRequestedTemplateId(commandName, args) {
43
44
  }
44
45
  if (explicit && fromText && explicit !== fromText) {
45
46
  parseWarnings.push(truncateTemplateWarning(`responseTemplateId '${explicit}' disagrees with instruction directive '${fromText}'; using explicit id.`));
47
+ strictViolations.push(truncateTemplateWarning(`In strict mode, responseTemplateId '${explicit}' conflicts with instruction directive '${fromText}'.`));
46
48
  }
47
49
  if (explicit)
48
- return { templateId: explicit, parseWarnings };
50
+ return { templateId: explicit, parseWarnings, strictViolations };
49
51
  if (fromText)
50
- return { templateId: fromText, parseWarnings };
51
- return { templateId: null, parseWarnings };
52
+ return { templateId: fromText, parseWarnings, strictViolations };
53
+ return { templateId: null, parseWarnings, strictViolations };
52
54
  }
53
55
  function attachPresentation(templateId, result) {
54
56
  const def = getResponseTemplateDefinition(templateId);
@@ -74,27 +76,42 @@ function buildMeta(partial, startNs) {
74
76
  }
75
77
  /**
76
78
  * Apply response template metadata and optional presentation hints (T262, T265).
77
- * Advisory mode never flips `ok`. Strict mode fails closed on unknown template ids when a template was explicitly requested or command override is set.
79
+ * Advisory mode never flips `ok` for template issues. Strict mode fails closed on unknown resolved template ids
80
+ * (explicit, override, or default) and on explicit-vs-directive conflicts (`response-template-conflict`).
78
81
  */
79
82
  export function applyResponseTemplateApplication(commandName, args, result, effective) {
80
83
  const startNs = process.hrtime.bigint();
81
84
  const cfg = readResponseTemplatesConfig(effective);
82
- const { templateId: requestedRaw, parseWarnings } = resolveRequestedTemplateId(commandName, args);
85
+ const { templateId: requestedRaw, parseWarnings, strictViolations } = resolveRequestedTemplateId(commandName, args);
86
+ if (cfg.enforcementMode === "strict" && strictViolations.length > 0) {
87
+ const warnings = [...parseWarnings, ...strictViolations];
88
+ return {
89
+ ...result,
90
+ ok: false,
91
+ code: "response-template-conflict",
92
+ message: strictViolations[0],
93
+ responseTemplate: buildMeta({
94
+ requestedTemplateId: requestedRaw,
95
+ appliedTemplateId: null,
96
+ enforcementMode: cfg.enforcementMode,
97
+ warnings
98
+ }, startNs)
99
+ };
100
+ }
83
101
  const override = cfg.commandOverrides[commandName];
84
102
  const chosenId = requestedRaw ?? override ?? cfg.defaultTemplateId ?? "default";
85
103
  const warnings = [...parseWarnings];
86
104
  const def = getResponseTemplateDefinition(chosenId);
87
105
  if (!def) {
88
106
  warnings.push(truncateTemplateWarning(`Unknown response template '${chosenId}'.`));
89
- const explicitRequest = Boolean(requestedRaw || override);
90
- if (cfg.enforcementMode === "strict" && explicitRequest) {
107
+ if (cfg.enforcementMode === "strict") {
91
108
  return {
92
109
  ...result,
93
110
  ok: false,
94
111
  code: "response-template-invalid",
95
112
  message: truncateTemplateWarning(`Unknown response template '${chosenId}'.`),
96
113
  responseTemplate: buildMeta({
97
- requestedTemplateId: requestedRaw ?? override,
114
+ requestedTemplateId: requestedRaw ?? override ?? cfg.defaultTemplateId,
98
115
  appliedTemplateId: null,
99
116
  enforcementMode: cfg.enforcementMode,
100
117
  warnings
@@ -0,0 +1,7 @@
1
+ /** Best-effort parse of maintainer status YAML for dashboard UIs (no full YAML dependency). */
2
+ export type WorkspaceStatusSnapshot = {
3
+ currentKitPhase: string | null;
4
+ activeFocus: string | null;
5
+ lastUpdated: string | null;
6
+ };
7
+ export declare function readWorkspaceStatusSnapshot(workspacePath: string): Promise<WorkspaceStatusSnapshot | null>;
@@ -0,0 +1,19 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ export async function readWorkspaceStatusSnapshot(workspacePath) {
4
+ const filePath = path.join(workspacePath, "docs/maintainers/data/workspace-kit-status.yaml");
5
+ try {
6
+ const raw = await fs.readFile(filePath, "utf8");
7
+ const phaseMatch = raw.match(/^\s*current_kit_phase:\s*["']?([^"'\n#]+?)["']?\s*$/m);
8
+ const focusMatch = raw.match(/^\s*active_focus:\s*"([^"]*)"\s*$/m);
9
+ const updatedMatch = raw.match(/^\s*last_updated:\s*["']?([^"'\n#]+?)["']?\s*$/m);
10
+ return {
11
+ currentKitPhase: phaseMatch?.[1]?.trim() ?? null,
12
+ activeFocus: focusMatch?.[1] ?? null,
13
+ lastUpdated: updatedMatch?.[1]?.trim() ?? null
14
+ };
15
+ }
16
+ catch {
17
+ return null;
18
+ }
19
+ }
@@ -4,4 +4,5 @@ export { TaskStore } from "./store.js";
4
4
  export { TransitionService } from "./service.js";
5
5
  export { TaskEngineError, TransitionValidator, isTransitionAllowed, getTransitionAction, resolveTargetState, getAllowedTransitionsFrom, stateValidityGuard, dependencyCheckGuard } from "./transitions.js";
6
6
  export { getNextActions } from "./suggestions.js";
7
+ export { readWorkspaceStatusSnapshot } from "./dashboard-status.js";
7
8
  export declare const taskEngineModule: WorkflowModule;
@@ -1,12 +1,14 @@
1
1
  import { maybeSpawnTranscriptHookAfterCompletion } from "../../core/transcript-completion-hook.js";
2
2
  import { TaskStore } from "./store.js";
3
3
  import { TransitionService } from "./service.js";
4
- import { TaskEngineError } from "./transitions.js";
4
+ import { TaskEngineError, getAllowedTransitionsFrom } from "./transitions.js";
5
5
  import { getNextActions } from "./suggestions.js";
6
+ import { readWorkspaceStatusSnapshot } from "./dashboard-status.js";
6
7
  export { TaskStore } from "./store.js";
7
8
  export { TransitionService } from "./service.js";
8
9
  export { TaskEngineError, TransitionValidator, isTransitionAllowed, getTransitionAction, resolveTargetState, getAllowedTransitionsFrom, stateValidityGuard, dependencyCheckGuard } from "./transitions.js";
9
10
  export { getNextActions } from "./suggestions.js";
11
+ export { readWorkspaceStatusSnapshot } from "./dashboard-status.js";
10
12
  function taskStorePath(ctx) {
11
13
  const tasks = ctx.effectiveConfig?.tasks;
12
14
  if (!tasks || typeof tasks !== "object" || Array.isArray(tasks)) {
@@ -60,6 +62,11 @@ export const taskEngineModule = {
60
62
  name: "get-next-actions",
61
63
  file: "get-next-actions.md",
62
64
  description: "Get prioritized next-action suggestions with blocking analysis."
65
+ },
66
+ {
67
+ name: "dashboard-summary",
68
+ file: "dashboard-summary.md",
69
+ description: "Stable JSON cockpit summary for UI clients (tasks + maintainer status snapshot)."
63
70
  }
64
71
  ]
65
72
  }
@@ -139,10 +146,63 @@ export const taskEngineModule = {
139
146
  message: `Task '${taskId}' not found`
140
147
  };
141
148
  }
149
+ const historyLimitRaw = args.historyLimit;
150
+ const historyLimit = typeof historyLimitRaw === "number" && Number.isFinite(historyLimitRaw) && historyLimitRaw > 0
151
+ ? Math.min(Math.floor(historyLimitRaw), 200)
152
+ : 50;
153
+ const log = store.getTransitionLog();
154
+ const recentTransitions = log
155
+ .filter((e) => e.taskId === taskId)
156
+ .sort((a, b) => (a.timestamp < b.timestamp ? 1 : -1))
157
+ .slice(0, historyLimit);
158
+ const allowedActions = getAllowedTransitionsFrom(task.status).map(({ to, action }) => ({
159
+ action,
160
+ targetStatus: to
161
+ }));
142
162
  return {
143
163
  ok: true,
144
164
  code: "task-retrieved",
145
- data: { task }
165
+ data: { task, recentTransitions, allowedActions }
166
+ };
167
+ }
168
+ if (command.name === "dashboard-summary") {
169
+ const tasks = store.getAllTasks();
170
+ const suggestion = getNextActions(tasks);
171
+ const workspaceStatus = await readWorkspaceStatusSnapshot(ctx.workspacePath);
172
+ const readyTop = suggestion.readyQueue.slice(0, 15).map((t) => ({
173
+ id: t.id,
174
+ title: t.title,
175
+ priority: t.priority ?? null,
176
+ phase: t.phase ?? null
177
+ }));
178
+ const blockedTop = suggestion.blockingAnalysis.slice(0, 15);
179
+ const data = {
180
+ schemaVersion: 1,
181
+ taskStoreLastUpdated: store.getLastUpdated(),
182
+ workspaceStatus,
183
+ stateSummary: suggestion.stateSummary,
184
+ readyQueueTop: readyTop,
185
+ readyQueueCount: suggestion.readyQueue.length,
186
+ blockedSummary: {
187
+ count: suggestion.blockingAnalysis.length,
188
+ top: blockedTop
189
+ },
190
+ suggestedNext: suggestion.suggestedNext
191
+ ? {
192
+ id: suggestion.suggestedNext.id,
193
+ title: suggestion.suggestedNext.title,
194
+ status: suggestion.suggestedNext.status,
195
+ priority: suggestion.suggestedNext.priority ?? null,
196
+ phase: suggestion.suggestedNext.phase ?? null
197
+ }
198
+ : null,
199
+ blockingAnalysis: suggestion.blockingAnalysis
200
+ };
201
+ return {
202
+ ok: true,
203
+ code: "dashboard-summary",
204
+ message: "Dashboard summary built from task store and maintainer status snapshot",
205
+ data
146
206
  };
147
207
  }
148
208
  if (command.name === "list-tasks") {
@@ -13,4 +13,5 @@ export declare class TaskStore {
13
13
  getTransitionLog(): TransitionEvidence[];
14
14
  replaceAllTasks(tasks: TaskEntity[]): void;
15
15
  getFilePath(): string;
16
+ getLastUpdated(): string;
16
17
  }
@@ -85,4 +85,7 @@ export class TaskStore {
85
85
  getFilePath() {
86
86
  return this.filePath;
87
87
  }
88
+ getLastUpdated() {
89
+ return this.document.lastUpdated;
90
+ }
88
91
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@workflow-cannon/workspace-kit",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "private": false,
5
5
  "packageManager": "pnpm@10.0.0",
6
6
  "license": "MIT",
@@ -32,9 +32,11 @@
32
32
  "prune-evidence": "node scripts/prune-evidence.mjs",
33
33
  "phase4-gates": "pnpm run check-compatibility && pnpm run check-planning-consistency && pnpm run check-release-channel",
34
34
  "phase5-gates": "pnpm run phase4-gates && pnpm run test",
35
+ "advisory:task-state-hand-edit": "node scripts/advisory-task-engine-state-hand-edit.mjs",
35
36
  "pre-release-transcript-hook": "pnpm run build && node scripts/pre-release-transcript-hook.mjs",
36
37
  "transcript:sync": "node scripts/run-transcript-cli.mjs sync-transcripts",
37
- "transcript:ingest": "node scripts/run-transcript-cli.mjs ingest-transcripts"
38
+ "transcript:ingest": "node scripts/run-transcript-cli.mjs ingest-transcripts",
39
+ "ext:compile": "cd extensions/cursor-workflow-cannon && npm run compile"
38
40
  },
39
41
  "devDependencies": {
40
42
  "@types/node": "^25.5.0",