@workflow-cannon/workspace-kit 0.10.0 → 0.12.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 -0
- package/dist/cli/interactive-policy.d.ts +16 -0
- package/dist/cli/interactive-policy.js +53 -0
- package/dist/cli/run-command.d.ts +2 -0
- package/dist/cli/run-command.js +49 -7
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +4 -2
- package/dist/core/config-metadata.js +1 -1
- package/dist/core/module-registry.d.ts +1 -1
- package/dist/core/module-registry.js +2 -2
- package/dist/core/policy.d.ts +2 -0
- package/dist/core/policy.js +2 -0
- package/dist/core/response-template-shaping.d.ts +2 -1
- package/dist/core/response-template-shaping.js +25 -8
- package/dist/modules/task-engine/dashboard-status.d.ts +7 -0
- package/dist/modules/task-engine/dashboard-status.js +19 -0
- package/dist/modules/task-engine/index.d.ts +1 -0
- package/dist/modules/task-engine/index.js +62 -2
- package/dist/modules/task-engine/store.d.ts +1 -0
- package/dist/modules/task-engine/store.js +3 -0
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -65,6 +65,9 @@ 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.
|
|
69
|
+
- **Phase 11** ships architectural review follow-up hardening (`v0.12.0`): policy/session denial edge tests, persistence concurrency semantics, release doc-sweep checklist, and runtime path audit note.
|
|
70
|
+
- **Phase 12** is the active queue: Cursor-native thin-client extension delivery (`T296`–`T310`).
|
|
68
71
|
|
|
69
72
|
Historical note: this file’s milestone list is not the live queue—always check task state for **`ready`** work.
|
|
70
73
|
|
|
@@ -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;
|
package/dist/cli/run-command.js
CHANGED
|
@@ -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(
|
|
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 = {};
|
|
@@ -75,7 +78,9 @@ export async function handleRunCommand(cwd, args, io, codes) {
|
|
|
75
78
|
const sessionId = resolveSessionId(process.env);
|
|
76
79
|
const policyOp = resolvePolicyOperationIdForCommand(subcommand, effective);
|
|
77
80
|
const explicitPolicyApproval = parsePolicyApproval(commandArgs);
|
|
81
|
+
const hasPolicyApprovalField = Object.hasOwn(commandArgs, "policyApproval");
|
|
78
82
|
let resolvedSensitiveApproval = explicitPolicyApproval;
|
|
83
|
+
let interactiveSessionFollowup = false;
|
|
79
84
|
if (sensitive) {
|
|
80
85
|
if (!resolvedSensitiveApproval && policyOp) {
|
|
81
86
|
const grant = await getSessionGrant(cwd, policyOp, sessionId);
|
|
@@ -83,6 +88,37 @@ export async function handleRunCommand(cwd, args, io, codes) {
|
|
|
83
88
|
resolvedSensitiveApproval = { confirmed: true, rationale: grant.rationale };
|
|
84
89
|
}
|
|
85
90
|
}
|
|
91
|
+
if (!resolvedSensitiveApproval && policyOp) {
|
|
92
|
+
const interactive = await promptSensitiveRunApproval({ writeError, readStdinLine: io.readStdinLine }, policyOp, `run ${subcommand}`, process.env);
|
|
93
|
+
if (interactive?.kind === "deny") {
|
|
94
|
+
await appendPolicyTrace(cwd, {
|
|
95
|
+
timestamp: new Date().toISOString(),
|
|
96
|
+
operationId: policyOp,
|
|
97
|
+
command: `run ${subcommand}`,
|
|
98
|
+
actor,
|
|
99
|
+
allowed: false,
|
|
100
|
+
message: "interactive policy approval denied"
|
|
101
|
+
});
|
|
102
|
+
writeLine(JSON.stringify({
|
|
103
|
+
ok: false,
|
|
104
|
+
code: "policy-denied",
|
|
105
|
+
operationId: policyOp,
|
|
106
|
+
remediationDoc: POLICY_APPROVAL_HUMAN_DOC,
|
|
107
|
+
message: "Sensitive command denied at interactive policy prompt.",
|
|
108
|
+
hint: `Set WORKSPACE_KIT_INTERACTIVE_APPROVAL=off or pass policyApproval in JSON. See ${POLICY_APPROVAL_HUMAN_DOC}.`
|
|
109
|
+
}, null, 2));
|
|
110
|
+
return codes.validationFailure;
|
|
111
|
+
}
|
|
112
|
+
if (interactive?.kind === "approve") {
|
|
113
|
+
const rationale = interactive.scope === "session" ? "interactive-approval-session" : "interactive-approval-once";
|
|
114
|
+
resolvedSensitiveApproval = {
|
|
115
|
+
confirmed: true,
|
|
116
|
+
rationale,
|
|
117
|
+
...(interactive.scope === "session" ? { scope: "session" } : {})
|
|
118
|
+
};
|
|
119
|
+
interactiveSessionFollowup = interactive.scope === "session";
|
|
120
|
+
}
|
|
121
|
+
}
|
|
86
122
|
if (!resolvedSensitiveApproval) {
|
|
87
123
|
if (policyOp) {
|
|
88
124
|
await appendPolicyTrace(cwd, {
|
|
@@ -91,7 +127,9 @@ export async function handleRunCommand(cwd, args, io, codes) {
|
|
|
91
127
|
command: `run ${subcommand}`,
|
|
92
128
|
actor,
|
|
93
129
|
allowed: false,
|
|
94
|
-
message:
|
|
130
|
+
message: hasPolicyApprovalField
|
|
131
|
+
? "invalid policyApproval in JSON args"
|
|
132
|
+
: "missing policyApproval in JSON args"
|
|
95
133
|
});
|
|
96
134
|
}
|
|
97
135
|
writeLine(JSON.stringify({
|
|
@@ -99,9 +137,11 @@ export async function handleRunCommand(cwd, args, io, codes) {
|
|
|
99
137
|
code: "policy-denied",
|
|
100
138
|
operationId: policyOp ?? null,
|
|
101
139
|
remediationDoc: POLICY_APPROVAL_HUMAN_DOC,
|
|
102
|
-
message:
|
|
140
|
+
message: hasPolicyApprovalField
|
|
141
|
+
? 'Sensitive command received an invalid policyApproval object. Use {"policyApproval":{"confirmed":true,"rationale":"why","scope":"session"}} (scope optional) or use an existing session grant for this operation.'
|
|
142
|
+
: '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
143
|
hint: policyOp != null
|
|
104
|
-
? `Operation ${policyOp} requires explicit approval; WORKSPACE_KIT_POLICY_APPROVAL is not read for workspace-kit run.`
|
|
144
|
+
? `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
145
|
: "Operation could not be mapped to policyOperationId; check policy.extraSensitiveModuleCommands and pass policyApproval in JSON args."
|
|
106
146
|
}, null, 2));
|
|
107
147
|
return codes.validationFailure;
|
|
@@ -127,8 +167,10 @@ export async function handleRunCommand(cwd, args, io, codes) {
|
|
|
127
167
|
commandOk: rawResult.ok,
|
|
128
168
|
message: rawResult.message
|
|
129
169
|
});
|
|
130
|
-
|
|
131
|
-
|
|
170
|
+
const recordSession = rawResult.ok &&
|
|
171
|
+
(explicitPolicyApproval?.scope === "session" || interactiveSessionFollowup);
|
|
172
|
+
if (recordSession) {
|
|
173
|
+
await recordSessionGrant(cwd, policyOp, sessionId, resolvedSensitiveApproval.rationale);
|
|
132
174
|
}
|
|
133
175
|
}
|
|
134
176
|
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: "
|
|
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",
|
|
@@ -3,7 +3,7 @@ export declare class ModuleRegistryError extends Error {
|
|
|
3
3
|
readonly code: string;
|
|
4
4
|
constructor(code: string, message: string);
|
|
5
5
|
}
|
|
6
|
-
export declare function validateModuleSet(modules: WorkflowModule[]): void;
|
|
6
|
+
export declare function validateModuleSet(modules: WorkflowModule[], workspacePath?: string): void;
|
|
7
7
|
export type ModuleRegistryOptions = {
|
|
8
8
|
enabledModules?: string[];
|
|
9
9
|
disabledModules?: string[];
|
|
@@ -142,10 +142,10 @@ function validateInstructionContracts(moduleMap, workspacePath) {
|
|
|
142
142
|
}
|
|
143
143
|
}
|
|
144
144
|
}
|
|
145
|
-
export function validateModuleSet(modules) {
|
|
145
|
+
export function validateModuleSet(modules, workspacePath) {
|
|
146
146
|
const moduleMap = buildModuleMap(modules);
|
|
147
147
|
validateDependencies(moduleMap);
|
|
148
|
-
validateInstructionContracts(moduleMap, process.cwd());
|
|
148
|
+
validateInstructionContracts(moduleMap, workspacePath ?? process.cwd());
|
|
149
149
|
topologicalSort(moduleMap);
|
|
150
150
|
}
|
|
151
151
|
export class ModuleRegistry {
|
package/dist/core/policy.d.ts
CHANGED
|
@@ -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[];
|
package/dist/core/policy.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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") {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@workflow-cannon/workspace-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.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",
|