mcoda 0.1.21 → 0.1.22

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
@@ -28,12 +28,14 @@ mcoda docs pdr generate --workspace-root . --project WEB --rfp-path docs/rfp/web
28
28
  - Docs: `mcoda docs pdr generate`, `mcoda docs sds generate`
29
29
  - Specs: `mcoda openapi-from-docs`
30
30
  - Planning: `mcoda create-tasks`, `mcoda refine-tasks`, `mcoda order-tasks`
31
- - Execution: `mcoda work-on-tasks`, `mcoda code-review`, `mcoda qa-tasks`
31
+ - Execution: `mcoda add-tests`, `mcoda work-on-tasks`, `mcoda code-review`, `mcoda qa-tasks`
32
32
  - Backlog: `mcoda backlog`, `mcoda task`
33
33
  - Jobs/telemetry: `mcoda jobs`, `mcoda tokens`, `mcoda telemetry`
34
34
  - Agents: `mcoda test-agent`, `mcoda agent-run`
35
35
  - Updates: `mcoda update --check`
36
36
 
37
+ `mcoda work-on-tasks` auto-runs the same test-harness bootstrap logic as `mcoda add-tests` when selected tasks require tests but no runnable harness exists.
38
+
37
39
  ## Configuration
38
40
  Environment variables are optional overrides for workspace settings:
39
41
  - `MCODA_DOCDEX_URL` to point at a docdex server.
@@ -1 +1 @@
1
- {"version":3,"file":"McodaEntrypoint.d.ts","sourceRoot":"","sources":["../../src/bin/McodaEntrypoint.ts"],"names":[],"mappings":";AA4BA,qBAAa,eAAe;WACb,GAAG,CAAC,IAAI,GAAE,MAAM,EAA0B,GAAG,OAAO,CAAC,IAAI,CAAC;CAsLxE"}
1
+ {"version":3,"file":"McodaEntrypoint.d.ts","sourceRoot":"","sources":["../../src/bin/McodaEntrypoint.ts"],"names":[],"mappings":";AA6BA,qBAAa,eAAe;WACb,GAAG,CAAC,IAAI,GAAE,MAAM,EAA0B,GAAG,OAAO,CAAC,IAAI,CAAC;CA0LxE"}
@@ -16,6 +16,7 @@ import { EstimateCommands } from "../commands/estimate/EstimateCommands.js";
16
16
  import { TelemetryCommands } from "../commands/telemetry/TelemetryCommands.js";
17
17
  import { WorkOnTasksCommand } from "../commands/work/WorkOnTasksCommand.js";
18
18
  import { GatewayTrioCommand } from "../commands/work/GatewayTrioCommand.js";
19
+ import { AddTestsCommand } from "../commands/work/AddTestsCommand.js";
19
20
  import { CodeReviewCommand } from "../commands/review/CodeReviewCommand.js";
20
21
  import { QaTasksCommand } from "../commands/planning/QaTasksCommand.js";
21
22
  import { MigrateTasksCommand } from "../commands/planning/MigrateTasksCommand.js";
@@ -80,7 +81,7 @@ export class McodaEntrypoint {
80
81
  return;
81
82
  }
82
83
  if (!command) {
83
- throw new Error("Usage: mcoda <agent|gateway-agent|test-agent|agent-run|routing|docs|openapi|job|jobs|tokens|telemetry|create-tasks|migrate-tasks|refine-tasks|order-tasks|tasks|work-on-tasks|gateway-trio|code-review|qa-tasks|backlog|task|task-detail|estimate|update|set-workspace|project-guidance|pdr|sds> [...args]\n" +
84
+ throw new Error("Usage: mcoda <agent|gateway-agent|test-agent|agent-run|routing|docs|openapi|job|jobs|tokens|telemetry|create-tasks|migrate-tasks|refine-tasks|order-tasks|tasks|add-tests|work-on-tasks|gateway-trio|code-review|qa-tasks|backlog|task|task-detail|estimate|update|set-workspace|project-guidance|pdr|sds> [...args]\n" +
84
85
  "Routing: use `mcoda routing defaults` to view/update workspace/global defaults, `mcoda routing preview|explain` to inspect agent selection/provenance (override → workspace_default → global_default).\n" +
85
86
  "Aliases: `tasks order-by-deps` forwards to `order-tasks` (dependency-aware ordering), `task`/`task-detail` show a single task.\n" +
86
87
  "Job commands (mcoda job --help for details): list|status|watch|logs|inspect|resume|cancel|tokens\n" +
@@ -170,6 +171,10 @@ export class McodaEntrypoint {
170
171
  await WorkOnTasksCommand.run(rest);
171
172
  return;
172
173
  }
174
+ if (command === "add-tests") {
175
+ await AddTestsCommand.run(rest);
176
+ return;
177
+ }
173
178
  if (command === "gateway-trio") {
174
179
  await GatewayTrioCommand.run(rest);
175
180
  return;
@@ -0,0 +1,31 @@
1
+ interface ParsedArgs {
2
+ workspaceRoot?: string;
3
+ projectKey?: string;
4
+ epicKey?: string;
5
+ storyKey?: string;
6
+ taskKeys: string[];
7
+ statusFilter: string[];
8
+ limit?: number;
9
+ noCommit: boolean;
10
+ dryRun: boolean;
11
+ baseBranch?: string;
12
+ json: boolean;
13
+ }
14
+ type ProjectKeyCandidate = {
15
+ key: string;
16
+ createdAt?: string | null;
17
+ };
18
+ export declare const parseAddTestsArgs: (argv: string[]) => ParsedArgs;
19
+ export declare const pickAddTestsProjectKey: (options: {
20
+ requestedKey?: string;
21
+ configuredKey?: string;
22
+ existing: ProjectKeyCandidate[];
23
+ }) => {
24
+ projectKey?: string;
25
+ warnings: string[];
26
+ };
27
+ export declare class AddTestsCommand {
28
+ static run(argv: string[]): Promise<void>;
29
+ }
30
+ export {};
31
+ //# sourceMappingURL=AddTestsCommand.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AddTestsCommand.d.ts","sourceRoot":"","sources":["../../../src/commands/work/AddTestsCommand.ts"],"names":[],"mappings":"AAKA,UAAU,UAAU;IAClB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,OAAO,CAAC;IAClB,MAAM,EAAE,OAAO,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,OAAO,CAAC;CACf;AAED,KAAK,mBAAmB,GAAG;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CAAC;AAqBtE,eAAO,MAAM,iBAAiB,GAAI,MAAM,MAAM,EAAE,KAAG,UAsGlD,CAAC;AAoBF,eAAO,MAAM,sBAAsB,GAAI,SAAS;IAC9C,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,EAAE,mBAAmB,EAAE,CAAC;CACjC,KAAG;IAAE,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,EAAE,CAAA;CAkC5C,CAAC;AAEF,qBAAa,eAAe;WACb,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;CA2GhD"}
@@ -0,0 +1,263 @@
1
+ import path from "node:path";
2
+ import { WorkspaceRepository } from "@mcoda/db";
3
+ import { AddTestsService, WorkspaceResolver } from "@mcoda/core";
4
+ import { WORK_ALLOWED_STATUSES, filterTaskStatuses } from "@mcoda/shared";
5
+ const usage = `mcoda add-tests \\
6
+ [--workspace <PATH>] \\
7
+ [--project <PROJECT_KEY>] \\
8
+ [--task <TASK_KEY> ... | --epic <EPIC_KEY> | --story <STORY_KEY>] \\
9
+ [--status not_started,in_progress,changes_requested] \\
10
+ [--limit N] \\
11
+ [--base-branch <BRANCH>] \\
12
+ [--no-commit] \\
13
+ [--dry-run] \\
14
+ [--json]`;
15
+ const parseCsv = (value) => {
16
+ if (!value)
17
+ return [];
18
+ return value
19
+ .split(",")
20
+ .map((item) => item.trim())
21
+ .filter(Boolean);
22
+ };
23
+ export const parseAddTestsArgs = (argv) => {
24
+ let workspaceRoot;
25
+ let projectKey;
26
+ let epicKey;
27
+ let storyKey;
28
+ const taskKeys = [];
29
+ const statusFilter = [];
30
+ let limit;
31
+ let noCommit = false;
32
+ let dryRun = false;
33
+ let baseBranch;
34
+ let json = false;
35
+ for (let i = 0; i < argv.length; i += 1) {
36
+ const arg = argv[i];
37
+ if (arg.startsWith("--status=")) {
38
+ const [, raw] = arg.split("=", 2);
39
+ statusFilter.push(...parseCsv(raw));
40
+ continue;
41
+ }
42
+ if (arg.startsWith("--task=")) {
43
+ const [, raw] = arg.split("=", 2);
44
+ if (raw)
45
+ taskKeys.push(raw);
46
+ continue;
47
+ }
48
+ switch (arg) {
49
+ case "--workspace":
50
+ case "--workspace-root":
51
+ workspaceRoot = argv[i + 1] ? path.resolve(argv[i + 1]) : undefined;
52
+ i += 1;
53
+ break;
54
+ case "--project":
55
+ case "--project-key":
56
+ projectKey = argv[i + 1];
57
+ i += 1;
58
+ break;
59
+ case "--epic":
60
+ epicKey = argv[i + 1];
61
+ i += 1;
62
+ break;
63
+ case "--story":
64
+ storyKey = argv[i + 1];
65
+ i += 1;
66
+ break;
67
+ case "--task":
68
+ if (argv[i + 1]) {
69
+ taskKeys.push(argv[i + 1]);
70
+ i += 1;
71
+ }
72
+ break;
73
+ case "--status":
74
+ statusFilter.push(...parseCsv(argv[i + 1]));
75
+ i += 1;
76
+ break;
77
+ case "--limit":
78
+ limit = Number(argv[i + 1]);
79
+ i += 1;
80
+ break;
81
+ case "--base-branch":
82
+ case "--branch":
83
+ baseBranch = argv[i + 1];
84
+ i += 1;
85
+ break;
86
+ case "--no-commit":
87
+ noCommit = true;
88
+ break;
89
+ case "--dry-run":
90
+ dryRun = true;
91
+ break;
92
+ case "--json":
93
+ json = true;
94
+ break;
95
+ case "--help":
96
+ case "-h":
97
+ // eslint-disable-next-line no-console
98
+ console.log(usage);
99
+ process.exit(0);
100
+ break;
101
+ default:
102
+ break;
103
+ }
104
+ }
105
+ const { filtered } = filterTaskStatuses(statusFilter.length ? statusFilter : undefined, WORK_ALLOWED_STATUSES, WORK_ALLOWED_STATUSES);
106
+ return {
107
+ workspaceRoot,
108
+ projectKey,
109
+ epicKey,
110
+ storyKey,
111
+ taskKeys,
112
+ statusFilter: filtered,
113
+ limit: Number.isFinite(limit) ? limit : undefined,
114
+ noCommit,
115
+ dryRun,
116
+ baseBranch: baseBranch?.trim() || undefined,
117
+ json,
118
+ };
119
+ };
120
+ const listWorkspaceProjects = async (workspaceRoot) => {
121
+ const repo = await WorkspaceRepository.create(workspaceRoot);
122
+ try {
123
+ const rows = await repo
124
+ .getDb()
125
+ .all(`SELECT key, created_at FROM projects ORDER BY created_at ASC, key ASC`);
126
+ return rows
127
+ .map((row) => ({ key: String(row.key), createdAt: row.created_at ?? null }))
128
+ .filter((row) => row.key.trim().length > 0);
129
+ }
130
+ catch {
131
+ return [];
132
+ }
133
+ finally {
134
+ await repo.close();
135
+ }
136
+ };
137
+ export const pickAddTestsProjectKey = (options) => {
138
+ const warnings = [];
139
+ const requestedKey = options.requestedKey?.trim() || undefined;
140
+ const configuredKey = options.configuredKey?.trim() || undefined;
141
+ const existing = options.existing ?? [];
142
+ const firstExisting = existing[0]?.key;
143
+ if (requestedKey) {
144
+ if (configuredKey && configuredKey !== requestedKey) {
145
+ warnings.push(`Using explicitly requested project key "${requestedKey}"; overriding configured project key "${configuredKey}".`);
146
+ }
147
+ if (firstExisting && requestedKey !== firstExisting) {
148
+ warnings.push(`Using explicitly requested project key "${requestedKey}"; first workspace project is "${firstExisting}".`);
149
+ }
150
+ return { projectKey: requestedKey, warnings };
151
+ }
152
+ if (configuredKey) {
153
+ if (firstExisting && configuredKey !== firstExisting) {
154
+ warnings.push(`Using configured project key "${configuredKey}" instead of first workspace project "${firstExisting}".`);
155
+ }
156
+ return { projectKey: configuredKey, warnings };
157
+ }
158
+ if (firstExisting) {
159
+ warnings.push(`No --project provided; defaulting to first workspace project "${firstExisting}".`);
160
+ return { projectKey: firstExisting, warnings };
161
+ }
162
+ return { projectKey: undefined, warnings };
163
+ };
164
+ export class AddTestsCommand {
165
+ static async run(argv) {
166
+ const parsed = parseAddTestsArgs(argv);
167
+ const workspace = await WorkspaceResolver.resolveWorkspace({
168
+ cwd: process.cwd(),
169
+ explicitWorkspace: parsed.workspaceRoot,
170
+ });
171
+ const existingProjects = parsed.projectKey ? [] : await listWorkspaceProjects(workspace.workspaceRoot);
172
+ const configuredKey = typeof workspace.config?.projectKey === "string" && workspace.config.projectKey.trim().length > 0
173
+ ? workspace.config.projectKey
174
+ : undefined;
175
+ const projectResolution = pickAddTestsProjectKey({
176
+ requestedKey: parsed.projectKey,
177
+ configuredKey,
178
+ existing: existingProjects,
179
+ });
180
+ const commandWarnings = [...projectResolution.warnings];
181
+ if (!projectResolution.projectKey) {
182
+ // eslint-disable-next-line no-console
183
+ console.error("add-tests could not resolve a project key. Provide --project <PROJECT_KEY> or create tasks first.");
184
+ process.exitCode = 1;
185
+ return;
186
+ }
187
+ if (commandWarnings.length > 0 && !parsed.json) {
188
+ // eslint-disable-next-line no-console
189
+ console.warn(commandWarnings.map((warning) => `! ${warning}`).join("\n"));
190
+ }
191
+ const service = await AddTestsService.create(workspace);
192
+ try {
193
+ const result = await service.addTests({
194
+ projectKey: projectResolution.projectKey,
195
+ epicKey: parsed.epicKey,
196
+ storyKey: parsed.storyKey,
197
+ taskKeys: parsed.taskKeys.length ? parsed.taskKeys : undefined,
198
+ statusFilter: parsed.statusFilter,
199
+ ignoreStatusFilter: parsed.taskKeys.length > 0 ? true : undefined,
200
+ ignoreDependencies: true,
201
+ limit: parsed.limit,
202
+ dryRun: parsed.dryRun,
203
+ commit: !parsed.noCommit,
204
+ baseBranch: parsed.baseBranch,
205
+ });
206
+ const warnings = [...commandWarnings, ...result.warnings];
207
+ if (parsed.json) {
208
+ // eslint-disable-next-line no-console
209
+ console.log(JSON.stringify({
210
+ projectKey: result.projectKey,
211
+ selectedTaskCount: result.selectedTaskKeys.length,
212
+ tasksRequiringTests: result.tasksRequiringTests,
213
+ updatedTaskKeys: result.updatedTaskKeys,
214
+ skippedTaskKeys: result.skippedTaskKeys,
215
+ createdFiles: result.createdFiles,
216
+ runAllScriptPath: result.runAllScriptPath ?? null,
217
+ runAllCommand: result.runAllCommand ?? null,
218
+ branch: result.branch ?? null,
219
+ commitSha: result.commitSha ?? null,
220
+ warnings,
221
+ }, null, 2));
222
+ return;
223
+ }
224
+ // eslint-disable-next-line no-console
225
+ console.log([
226
+ `add-tests project=${result.projectKey}`,
227
+ `selected=${result.selectedTaskKeys.length}`,
228
+ `requires_tests=${result.tasksRequiringTests.length}`,
229
+ `updated=${result.updatedTaskKeys.length}`,
230
+ `skipped=${result.skippedTaskKeys.length}`,
231
+ ].join(" "));
232
+ if (result.createdFiles.length > 0) {
233
+ // eslint-disable-next-line no-console
234
+ console.log(`Created: ${result.createdFiles.join(", ")}`);
235
+ }
236
+ if (result.runAllCommand) {
237
+ // eslint-disable-next-line no-console
238
+ console.log(`Run-all command: ${result.runAllCommand}`);
239
+ }
240
+ if (result.commitSha) {
241
+ // eslint-disable-next-line no-console
242
+ console.log(`Committed on ${result.branch ?? "current branch"}: ${result.commitSha}`);
243
+ }
244
+ else if (!parsed.noCommit && !parsed.dryRun && result.createdFiles.length > 0) {
245
+ // eslint-disable-next-line no-console
246
+ console.log("No commit was created.");
247
+ }
248
+ if (warnings.length > 0) {
249
+ // eslint-disable-next-line no-console
250
+ console.warn(warnings.map((warning) => `! ${warning}`).join("\n"));
251
+ }
252
+ }
253
+ catch (error) {
254
+ const message = error instanceof Error ? error.message : String(error);
255
+ // eslint-disable-next-line no-console
256
+ console.error(`add-tests failed: ${message}`);
257
+ process.exitCode = 1;
258
+ }
259
+ finally {
260
+ await service.close();
261
+ }
262
+ }
263
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcoda",
3
- "version": "0.1.21",
3
+ "version": "0.1.22",
4
4
  "description": "Local-first CLI for planning, documentation, and execution workflows with agent assistance.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -45,12 +45,12 @@
45
45
  },
46
46
  "dependencies": {
47
47
  "yaml": "^2.4.2",
48
- "@mcoda/core": "0.1.21",
49
- "@mcoda/shared": "0.1.21"
48
+ "@mcoda/shared": "0.1.22",
49
+ "@mcoda/core": "0.1.22"
50
50
  },
51
51
  "devDependencies": {
52
- "@mcoda/db": "0.1.21",
53
- "@mcoda/integrations": "0.1.21"
52
+ "@mcoda/db": "0.1.22",
53
+ "@mcoda/integrations": "0.1.22"
54
54
  },
55
55
  "scripts": {
56
56
  "build": "tsc -p tsconfig.json",