@specship/specship-win32-arm64 0.21.2 → 0.22.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/lib/dist/bin/specship.js +302 -4
- package/lib/dist/bin/specship.js.map +1 -1
- package/lib/dist/commands/specship/jira.md +74 -0
- package/lib/dist/commands/specship/spec.md +61 -1
- package/lib/dist/extraction/index.d.ts +6 -0
- package/lib/dist/extraction/index.d.ts.map +1 -1
- package/lib/dist/extraction/index.js.map +1 -1
- package/lib/dist/extraction/specs/markdown-spec-extractor.d.ts.map +1 -1
- package/lib/dist/extraction/specs/markdown-spec-extractor.js +8 -1
- package/lib/dist/extraction/specs/markdown-spec-extractor.js.map +1 -1
- package/lib/dist/index.d.ts +7 -0
- package/lib/dist/index.d.ts.map +1 -1
- package/lib/dist/index.js +45 -18
- package/lib/dist/index.js.map +1 -1
- package/lib/dist/jira/auto-publish.d.ts +67 -0
- package/lib/dist/jira/auto-publish.d.ts.map +1 -0
- package/lib/dist/jira/auto-publish.js +232 -0
- package/lib/dist/jira/auto-publish.js.map +1 -0
- package/lib/dist/jira/board-first.d.ts +64 -0
- package/lib/dist/jira/board-first.d.ts.map +1 -0
- package/lib/dist/jira/board-first.js +91 -0
- package/lib/dist/jira/board-first.js.map +1 -0
- package/lib/dist/jira/client.d.ts +80 -1
- package/lib/dist/jira/client.d.ts.map +1 -1
- package/lib/dist/jira/client.js +213 -1
- package/lib/dist/jira/client.js.map +1 -1
- package/lib/dist/jira/config.d.ts +1 -1
- package/lib/dist/jira/config.d.ts.map +1 -1
- package/lib/dist/jira/config.js +14 -3
- package/lib/dist/jira/config.js.map +1 -1
- package/lib/dist/jira/coverage.d.ts +105 -0
- package/lib/dist/jira/coverage.d.ts.map +1 -0
- package/lib/dist/jira/coverage.js +160 -0
- package/lib/dist/jira/coverage.js.map +1 -0
- package/lib/dist/jira/index.d.ts +1 -0
- package/lib/dist/jira/index.d.ts.map +1 -1
- package/lib/dist/jira/index.js +1 -0
- package/lib/dist/jira/index.js.map +1 -1
- package/lib/dist/jira/milestone-comment.d.ts +95 -0
- package/lib/dist/jira/milestone-comment.d.ts.map +1 -0
- package/lib/dist/jira/milestone-comment.js +271 -0
- package/lib/dist/jira/milestone-comment.js.map +1 -0
- package/lib/dist/jira/publish.d.ts +69 -2
- package/lib/dist/jira/publish.d.ts.map +1 -1
- package/lib/dist/jira/publish.js +0 -0
- package/lib/dist/jira/publish.js.map +1 -1
- package/lib/dist/jira/published-specs.d.ts +23 -0
- package/lib/dist/jira/published-specs.d.ts.map +1 -0
- package/lib/dist/jira/published-specs.js +106 -0
- package/lib/dist/jira/published-specs.js.map +1 -0
- package/lib/dist/jira/reconcile.d.ts +81 -0
- package/lib/dist/jira/reconcile.d.ts.map +1 -0
- package/lib/dist/jira/reconcile.js +111 -0
- package/lib/dist/jira/reconcile.js.map +1 -0
- package/lib/dist/jira/regression-pack.d.ts +547 -0
- package/lib/dist/jira/regression-pack.d.ts.map +1 -0
- package/lib/dist/jira/regression-pack.js +986 -0
- package/lib/dist/jira/regression-pack.js.map +1 -0
- package/lib/dist/jira/repo-config.d.ts +59 -0
- package/lib/dist/jira/repo-config.d.ts.map +1 -0
- package/lib/dist/jira/repo-config.js +222 -0
- package/lib/dist/jira/repo-config.js.map +1 -0
- package/lib/dist/jira/spec-amend.d.ts +46 -0
- package/lib/dist/jira/spec-amend.d.ts.map +1 -0
- package/lib/dist/jira/spec-amend.js +232 -0
- package/lib/dist/jira/spec-amend.js.map +1 -0
- package/lib/dist/jira/spec-writer.d.ts +22 -0
- package/lib/dist/jira/spec-writer.d.ts.map +1 -1
- package/lib/dist/jira/spec-writer.js +113 -0
- package/lib/dist/jira/spec-writer.js.map +1 -1
- package/lib/dist/jira/types.d.ts +62 -0
- package/lib/dist/jira/types.d.ts.map +1 -1
- package/lib/dist/jira/types.js +15 -1
- package/lib/dist/jira/types.js.map +1 -1
- package/lib/dist/mcp/jira-tools.d.ts +102 -0
- package/lib/dist/mcp/jira-tools.d.ts.map +1 -1
- package/lib/dist/mcp/jira-tools.js +807 -50
- package/lib/dist/mcp/jira-tools.js.map +1 -1
- package/lib/dist/mcp/server-instructions.d.ts +1 -1
- package/lib/dist/mcp/server-instructions.d.ts.map +1 -1
- package/lib/dist/mcp/server-instructions.js +4 -0
- package/lib/dist/mcp/server-instructions.js.map +1 -1
- package/lib/dist/mcp/spec-tools.d.ts.map +1 -1
- package/lib/dist/mcp/spec-tools.js +7 -1
- package/lib/dist/mcp/spec-tools.js.map +1 -1
- package/lib/dist/mcp/tools.d.ts +3 -0
- package/lib/dist/mcp/tools.d.ts.map +1 -1
- package/lib/dist/mcp/tools.js +50 -0
- package/lib/dist/mcp/tools.js.map +1 -1
- package/lib/dist/resolution/spec-link-resolver.d.ts +2 -0
- package/lib/dist/resolution/spec-link-resolver.d.ts.map +1 -1
- package/lib/dist/resolution/spec-link-resolver.js +11 -3
- package/lib/dist/resolution/spec-link-resolver.js.map +1 -1
- package/lib/node_modules/.package-lock.json +1 -1
- package/lib/package.json +1 -1
- package/package.json +1 -1
|
@@ -51,6 +51,8 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
51
51
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
52
52
|
exports.jiraToolDefinitions = void 0;
|
|
53
53
|
exports.handleSpecshipJiraIssues = handleSpecshipJiraIssues;
|
|
54
|
+
exports.handleSpecshipJiraAnchor = handleSpecshipJiraAnchor;
|
|
55
|
+
exports.handleSpecshipJiraEpics = handleSpecshipJiraEpics;
|
|
54
56
|
exports.handleSpecshipJiraIssue = handleSpecshipJiraIssue;
|
|
55
57
|
exports.handleSpecshipJiraTransition = handleSpecshipJiraTransition;
|
|
56
58
|
exports.validateConfiguredTransitions = validateConfiguredTransitions;
|
|
@@ -61,17 +63,26 @@ exports.deriveWorkState = deriveWorkState;
|
|
|
61
63
|
exports.handleSpecshipJiraTrack = handleSpecshipJiraTrack;
|
|
62
64
|
exports.handleSpecshipJiraPublish = handleSpecshipJiraPublish;
|
|
63
65
|
exports.handleSpecshipJiraAddTask = handleSpecshipJiraAddTask;
|
|
66
|
+
exports.handleSpecshipJiraRegressionPack = handleSpecshipJiraRegressionPack;
|
|
67
|
+
exports.handleSpecshipJiraRegressionRecord = handleSpecshipJiraRegressionRecord;
|
|
68
|
+
exports.handleSpecshipJiraCoverage = handleSpecshipJiraCoverage;
|
|
69
|
+
exports.handleSpecshipJiraReconcile = handleSpecshipJiraReconcile;
|
|
64
70
|
const fs = __importStar(require("fs"));
|
|
65
71
|
const path = __importStar(require("path"));
|
|
66
72
|
const crypto_1 = require("crypto");
|
|
67
73
|
const child_process_1 = require("child_process");
|
|
68
74
|
const detect_1 = require("../taskship/detect");
|
|
69
75
|
const config_1 = require("../jira/config");
|
|
76
|
+
const repo_config_1 = require("../jira/repo-config");
|
|
70
77
|
const client_1 = require("../jira/client");
|
|
78
|
+
const board_first_1 = require("../jira/board-first");
|
|
71
79
|
const types_1 = require("../jira/types");
|
|
72
80
|
const spec_writer_1 = require("../jira/spec-writer");
|
|
81
|
+
const regression_pack_1 = require("../jira/regression-pack");
|
|
73
82
|
const spec_generator_1 = require("../jira/spec-generator");
|
|
74
83
|
const publish_1 = require("../jira/publish");
|
|
84
|
+
const published_specs_1 = require("../jira/published-specs");
|
|
85
|
+
const milestone_comment_1 = require("../jira/milestone-comment");
|
|
75
86
|
const pull_request_1 = require("../jira/pull-request");
|
|
76
87
|
function textResult(text) {
|
|
77
88
|
return { content: [{ type: 'text', text }] };
|
|
@@ -147,7 +158,7 @@ async function pushJiraStartStatus(key, make) {
|
|
|
147
158
|
*
|
|
148
159
|
* SECURITY: only the public PR URL + issue key are sent (REQ-JIRA-009).
|
|
149
160
|
*/
|
|
150
|
-
async function pushJiraReviewStatus(key, prUrl, make) {
|
|
161
|
+
async function pushJiraReviewStatus(key, prUrl, make, specId) {
|
|
151
162
|
let ctx;
|
|
152
163
|
try {
|
|
153
164
|
ctx = make();
|
|
@@ -171,9 +182,29 @@ async function pushJiraReviewStatus(key, prUrl, make) {
|
|
|
171
182
|
notes.push(`couldn't transition ${key} (${errMsg(err)})`);
|
|
172
183
|
}
|
|
173
184
|
// The PR-link comment is posted regardless of the transition outcome (A3).
|
|
185
|
+
// Milestone: pr_raised (REQ-JIRATEAM-003.A1/A2). The dispatcher itself is
|
|
186
|
+
// idempotent (single comment per issue+specId), watermarked, and
|
|
187
|
+
// soft-fails to a local note — so a JIRA hiccup here never undoes the
|
|
188
|
+
// raised PR. Fall back to a plain addComment only for fake clients that
|
|
189
|
+
// don't implement the milestone shape.
|
|
190
|
+
const maybe = ctx.client;
|
|
191
|
+
const canMilestone = typeof maybe.listCommentsDetailed === 'function' &&
|
|
192
|
+
typeof maybe.updateComment === 'function' &&
|
|
193
|
+
typeof maybe.addComment === 'function';
|
|
174
194
|
try {
|
|
175
|
-
|
|
176
|
-
|
|
195
|
+
if (canMilestone) {
|
|
196
|
+
const r = await (0, milestone_comment_1.postMilestoneComment)(maybe, key, 'pr_raised', { specId: specId ?? key, prUrl });
|
|
197
|
+
if (r.status === 'soft_failed') {
|
|
198
|
+
notes.push(`couldn't comment the PR link on ${key} (${r.reason ?? 'unknown'})`);
|
|
199
|
+
}
|
|
200
|
+
else {
|
|
201
|
+
notes.push(`commented the PR link on ${key}`);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
else {
|
|
205
|
+
await ctx.client.addComment(key, `SpecShip raised a pull request: ${prUrl}`);
|
|
206
|
+
notes.push(`commented the PR link on ${key}`);
|
|
207
|
+
}
|
|
177
208
|
}
|
|
178
209
|
catch (err) {
|
|
179
210
|
notes.push(`couldn't comment the PR link on ${key} (${errMsg(err)})`);
|
|
@@ -273,6 +304,36 @@ exports.jiraToolDefinitions = [
|
|
|
273
304
|
},
|
|
274
305
|
},
|
|
275
306
|
},
|
|
307
|
+
{
|
|
308
|
+
name: 'specship_jira_coverage',
|
|
309
|
+
description: 'Show a sprint coverage report joining the bound JIRA project\'s active ' +
|
|
310
|
+
'(or named) sprint to spec truth — every issue in the sprint with its ' +
|
|
311
|
+
'repo-side state (unspecced / specced / implemented / verified / drifted / ' +
|
|
312
|
+
'broken) and rollup totals. Read-only over JIRA by default; pass ' +
|
|
313
|
+
'`post: true` with `issue_key` to upsert the report as a single ' +
|
|
314
|
+
'watermarked comment on that issue (edited in place on re-post).',
|
|
315
|
+
inputSchema: {
|
|
316
|
+
type: 'object',
|
|
317
|
+
properties: {
|
|
318
|
+
project: {
|
|
319
|
+
type: 'string',
|
|
320
|
+
description: 'JIRA project key to scope the sprint read (e.g., "PROJ"). Optional when a default project is configured.',
|
|
321
|
+
},
|
|
322
|
+
sprint: {
|
|
323
|
+
type: 'string',
|
|
324
|
+
description: 'Optional named sprint. Omit to use the project\'s currently open sprint(s).',
|
|
325
|
+
},
|
|
326
|
+
post: {
|
|
327
|
+
type: 'boolean',
|
|
328
|
+
description: 'Post the report as a single watermarked JIRA comment (edit-in-place). Defaults to false — never posts unless explicitly requested.',
|
|
329
|
+
},
|
|
330
|
+
issue_key: {
|
|
331
|
+
type: 'string',
|
|
332
|
+
description: 'When `post` is true, the anchor issue (e.g., an epic key) the coverage comment lands on. Required with `post`.',
|
|
333
|
+
},
|
|
334
|
+
},
|
|
335
|
+
},
|
|
336
|
+
},
|
|
276
337
|
{
|
|
277
338
|
name: 'specship_jira_publish',
|
|
278
339
|
description: 'Publish an authored spec to JIRA as a Story whose Sub-tasks mirror the ' +
|
|
@@ -295,6 +356,50 @@ exports.jiraToolDefinitions = [
|
|
|
295
356
|
required: ['spec_id'],
|
|
296
357
|
},
|
|
297
358
|
},
|
|
359
|
+
{
|
|
360
|
+
name: 'specship_jira_reconcile',
|
|
361
|
+
description: "Detect JIRA-side edits to published specs and (only with explicit user " +
|
|
362
|
+
'confirmation) fold them back into the spec (REQ-JIRATEAM-005). Two modes: ' +
|
|
363
|
+
'`mode: "preview"` (default) — read-only; enumerates every published spec ' +
|
|
364
|
+
"whose LIVE JIRA issue diverges from what publish wrote (edited summary/" +
|
|
365
|
+
'description, or a Sub-task added in JIRA with no matching acceptance ' +
|
|
366
|
+
'criterion) and returns a proposed spec amendment. `mode: "apply"` — ' +
|
|
367
|
+
'writes the previewed amendment to the spec file and re-publishes so the ' +
|
|
368
|
+
'fingerprint refreshes. **Preview first, then apply only after the user ' +
|
|
369
|
+
'has explicitly confirmed the exact diff in conversation.** Apply refuses ' +
|
|
370
|
+
"unless `expected_live_fingerprint` matches the issue's current live " +
|
|
371
|
+
'fingerprint — a value the caller MUST have just seen returned by a ' +
|
|
372
|
+
'preview call. No preview → no apply.',
|
|
373
|
+
inputSchema: {
|
|
374
|
+
type: 'object',
|
|
375
|
+
properties: {
|
|
376
|
+
mode: {
|
|
377
|
+
type: 'string',
|
|
378
|
+
enum: ['preview', 'apply'],
|
|
379
|
+
description: 'preview (default) returns divergences without touching any file; apply writes ' +
|
|
380
|
+
'the amendment after preview + explicit user confirmation.',
|
|
381
|
+
},
|
|
382
|
+
issue_key: {
|
|
383
|
+
type: 'string',
|
|
384
|
+
description: 'Optional: narrow preview to a single issue key (e.g., "PROJ-123"). Required for apply.',
|
|
385
|
+
},
|
|
386
|
+
accept_content: {
|
|
387
|
+
type: 'boolean',
|
|
388
|
+
description: 'apply only: fold the edited summary/description back into the spec.',
|
|
389
|
+
},
|
|
390
|
+
accept_subtasks: {
|
|
391
|
+
type: 'array',
|
|
392
|
+
items: { type: 'string' },
|
|
393
|
+
description: 'apply only: JIRA Sub-task keys whose proposed acceptance criteria the user has confirmed.',
|
|
394
|
+
},
|
|
395
|
+
expected_live_fingerprint: {
|
|
396
|
+
type: 'string',
|
|
397
|
+
description: "apply only: the issue's live fingerprint from the preview the user just approved. " +
|
|
398
|
+
'apply refuses when this does not match the current live fingerprint (preview-first gate).',
|
|
399
|
+
},
|
|
400
|
+
},
|
|
401
|
+
},
|
|
402
|
+
},
|
|
298
403
|
{
|
|
299
404
|
name: 'specship_jira_transition',
|
|
300
405
|
description: 'Transition a JIRA issue to a target workflow state (e.g., "In Progress", ' +
|
|
@@ -317,6 +422,42 @@ exports.jiraToolDefinitions = [
|
|
|
317
422
|
required: ['key'],
|
|
318
423
|
},
|
|
319
424
|
},
|
|
425
|
+
{
|
|
426
|
+
name: 'specship_jira_anchor',
|
|
427
|
+
description: 'Resolve the board-first work anchor for the current repo (REQ-JIRATEAM-007). ' +
|
|
428
|
+
'Returns "unbound" when the repo has no JIRA binding (callers proceed unchanged), ' +
|
|
429
|
+
'the anchored issue when one is explicit/picked, or refuses (with the epic-scoped ' +
|
|
430
|
+
'pickable list) when a bound repo has no anchor yet. Read-only: never writes.',
|
|
431
|
+
inputSchema: {
|
|
432
|
+
type: 'object',
|
|
433
|
+
properties: {
|
|
434
|
+
issue_key: {
|
|
435
|
+
type: 'string',
|
|
436
|
+
description: 'Optional: an explicit issue key to anchor on (e.g., "PROJ-123"). Skips the pick step.',
|
|
437
|
+
},
|
|
438
|
+
picked_issue_key: {
|
|
439
|
+
type: 'string',
|
|
440
|
+
description: 'Optional: the key just picked from a specship_jira_issues list — routed as the anchor.',
|
|
441
|
+
},
|
|
442
|
+
},
|
|
443
|
+
},
|
|
444
|
+
},
|
|
445
|
+
{
|
|
446
|
+
name: 'specship_jira_epics',
|
|
447
|
+
description: 'List the open epics for a JIRA project — powers the /specship:jira menu ' +
|
|
448
|
+
"epic-picker (REQ-JIRATEAM-008). With no `project`, uses the repo's bound " +
|
|
449
|
+
'project (specship.config.json → jira.projectKey); errors clearly if neither ' +
|
|
450
|
+
'is set. Read-only: never writes; ordered most-recently-updated first.',
|
|
451
|
+
inputSchema: {
|
|
452
|
+
type: 'object',
|
|
453
|
+
properties: {
|
|
454
|
+
project: {
|
|
455
|
+
type: 'string',
|
|
456
|
+
description: "Optional project key (e.g., \"PROJ\"). Omit to use the repo's bound project.",
|
|
457
|
+
},
|
|
458
|
+
},
|
|
459
|
+
},
|
|
460
|
+
},
|
|
320
461
|
{
|
|
321
462
|
name: 'specship_jira_add_task',
|
|
322
463
|
description: 'Add a task you identified mid-implementation under its epic/story. If ' +
|
|
@@ -355,6 +496,69 @@ exports.jiraToolDefinitions = [
|
|
|
355
496
|
required: ['parent', 'title'],
|
|
356
497
|
},
|
|
357
498
|
},
|
|
499
|
+
{
|
|
500
|
+
name: 'specship_jira_regression_record',
|
|
501
|
+
description: 'Record a regression pack case result on its JIRA issue (REQ-JIRAREG-005): ' +
|
|
502
|
+
'watermarked comment upserted per (case, runId), pass → Done + validates-link ' +
|
|
503
|
+
'verified, fail → In Review + broken + a triage hint, unexecuted → comment only ' +
|
|
504
|
+
'(no transition, no link-state churn). Idempotent per (case, runId).',
|
|
505
|
+
inputSchema: {
|
|
506
|
+
type: 'object',
|
|
507
|
+
properties: {
|
|
508
|
+
case: { type: 'string', description: 'JIRA case issue key (e.g., "PROJ-42").' },
|
|
509
|
+
criterion: {
|
|
510
|
+
type: 'string',
|
|
511
|
+
description: 'Source acceptance criterion id (e.g., "REQ-FOO-001.A2").',
|
|
512
|
+
},
|
|
513
|
+
result: {
|
|
514
|
+
type: 'string',
|
|
515
|
+
enum: ['passed', 'failed', 'unexecuted'],
|
|
516
|
+
description: 'Outcome of the pack case.',
|
|
517
|
+
},
|
|
518
|
+
executor: {
|
|
519
|
+
type: 'string',
|
|
520
|
+
enum: ['human', 'agent'],
|
|
521
|
+
description: 'Who ran the case. Defaults to "human".',
|
|
522
|
+
},
|
|
523
|
+
harness: {
|
|
524
|
+
type: 'string',
|
|
525
|
+
description: 'Optional behaviour harness id (e.g., "behaviour-e2e").',
|
|
526
|
+
},
|
|
527
|
+
run: {
|
|
528
|
+
type: 'string',
|
|
529
|
+
description: 'Stable run identifier — one comment per (case, run) so re-records edit in place.',
|
|
530
|
+
},
|
|
531
|
+
log_ref: { type: 'string', description: 'Optional log pointer for evidence.' },
|
|
532
|
+
error: { type: 'string', description: 'Optional error text (fail path).' },
|
|
533
|
+
},
|
|
534
|
+
required: ['case', 'criterion', 'result'],
|
|
535
|
+
},
|
|
536
|
+
},
|
|
537
|
+
{
|
|
538
|
+
name: 'specship_jira_regression_pack',
|
|
539
|
+
description: 'Generate or refresh the SpecShip Regression Pack in the bound JIRA ' +
|
|
540
|
+
'project (REQ-JIRAREG-001): one watermarked epic → one domain-area ' +
|
|
541
|
+
'Story → one Sub-task per acceptance criterion of every implemented ' +
|
|
542
|
+
'(or verified) requirement. Idempotent — a re-run with nothing changed ' +
|
|
543
|
+
'performs zero JIRA writes; a criterion added, removed, or edited yields ' +
|
|
544
|
+
'exactly the matching create / update. Pass dry_run: true to preview the ' +
|
|
545
|
+
'plan without touching JIRA.',
|
|
546
|
+
inputSchema: {
|
|
547
|
+
type: 'object',
|
|
548
|
+
properties: {
|
|
549
|
+
project: {
|
|
550
|
+
type: 'string',
|
|
551
|
+
description: 'Optional JIRA project key (e.g., "PROJ"). Defaults to the ' +
|
|
552
|
+
'configured publish project when omitted.',
|
|
553
|
+
},
|
|
554
|
+
dry_run: {
|
|
555
|
+
type: 'boolean',
|
|
556
|
+
description: 'When true, build the plan and report counts without creating or ' +
|
|
557
|
+
'updating any JIRA issue. Default: false.',
|
|
558
|
+
},
|
|
559
|
+
},
|
|
560
|
+
},
|
|
561
|
+
},
|
|
358
562
|
];
|
|
359
563
|
/** Escape a value so it can't break the markdown table layout. */
|
|
360
564
|
function cell(s) {
|
|
@@ -381,6 +585,8 @@ function formatIssues(issues, opts) {
|
|
|
381
585
|
const notes = [];
|
|
382
586
|
if (opts?.project)
|
|
383
587
|
notes.push(`Filtered to project ${cell(opts.project)}.`);
|
|
588
|
+
if (opts?.epicKey)
|
|
589
|
+
notes.push(`Scoped to epic ${cell(opts.epicKey)}.`);
|
|
384
590
|
if (issues.length >= client_1.MAX_ISSUE_RESULTS) {
|
|
385
591
|
notes.push(`Showing the ${client_1.MAX_ISSUE_RESULTS} most recently updated.`);
|
|
386
592
|
}
|
|
@@ -442,17 +648,36 @@ async function handleSpecshipJiraIssues(args) {
|
|
|
442
648
|
const notConfigured = notConfiguredResult();
|
|
443
649
|
if (notConfigured)
|
|
444
650
|
return notConfigured;
|
|
445
|
-
const
|
|
651
|
+
const explicitProject = typeof args.project === 'string' && args.project.trim()
|
|
446
652
|
? args.project.trim()
|
|
447
653
|
: undefined;
|
|
448
654
|
// Sprint filter (TASKSHIP-BRIDGE-DOC, REQ-TASKSHIP-001): only "active" is a
|
|
449
655
|
// recognized value; anything else is ignored (falls back to all issues).
|
|
450
656
|
const sprint = args.sprint === 'active' ? 'active' : undefined;
|
|
657
|
+
// Board-first (REQ-JIRATEAM-007.A1): with a repo binding and no explicit
|
|
658
|
+
// overrides, scope to the bound project's epic so the same tool that
|
|
659
|
+
// powers the pick flow surfaces the anchor-eligible open work.
|
|
660
|
+
let epicKey;
|
|
661
|
+
let project = explicitProject;
|
|
662
|
+
if (!explicitProject) {
|
|
663
|
+
try {
|
|
664
|
+
const { binding } = (0, repo_config_1.loadRepoJiraBinding)(process.cwd());
|
|
665
|
+
if (binding) {
|
|
666
|
+
project = binding.projectKey;
|
|
667
|
+
if (binding.epicKey)
|
|
668
|
+
epicKey = binding.epicKey;
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
catch {
|
|
672
|
+
// A malformed repo config already surfaces via other paths; here we
|
|
673
|
+
// just skip the scoping rather than blocking a listing call.
|
|
674
|
+
}
|
|
675
|
+
}
|
|
451
676
|
try {
|
|
452
677
|
const creds = (0, config_1.resolveJiraCredentials)();
|
|
453
678
|
const client = new client_1.JiraClient(creds);
|
|
454
|
-
const result = await client.listMyIssues({ project, sprint });
|
|
455
|
-
return textResult(formatIssues(result.issues, { project }));
|
|
679
|
+
const result = await client.listMyIssues({ project, sprint, epicKey });
|
|
680
|
+
return textResult(formatIssues(result.issues, { project, epicKey }));
|
|
456
681
|
}
|
|
457
682
|
catch (err) {
|
|
458
683
|
// JiraError messages are credential-free by construction (REQ-JIRA-009).
|
|
@@ -463,6 +688,106 @@ async function handleSpecshipJiraIssues(args) {
|
|
|
463
688
|
return errorResult('Failed to list JIRA issues.');
|
|
464
689
|
}
|
|
465
690
|
}
|
|
691
|
+
/**
|
|
692
|
+
* Handle `specship_jira_anchor` (REQ-JIRATEAM-007). Thin wrapper over
|
|
693
|
+
* `resolveWorkAnchor` so slash-commands and external agents can query the
|
|
694
|
+
* board-first gate without duplicating logic. Always returns a plain-text
|
|
695
|
+
* result — anchored → an `Anchor:` line naming the key; refused → the
|
|
696
|
+
* canonical human refusal (with the pickable list when available); unbound
|
|
697
|
+
* → a one-liner so callers no-op cleanly.
|
|
698
|
+
*/
|
|
699
|
+
async function handleSpecshipJiraAnchor(args) {
|
|
700
|
+
const explicitIssueKey = typeof args.issue_key === 'string' && args.issue_key.trim()
|
|
701
|
+
? args.issue_key.trim()
|
|
702
|
+
: undefined;
|
|
703
|
+
const pickedIssueKey = typeof args.picked_issue_key === 'string' && args.picked_issue_key.trim()
|
|
704
|
+
? args.picked_issue_key.trim()
|
|
705
|
+
: undefined;
|
|
706
|
+
try {
|
|
707
|
+
// A resolve that needs to call JIRA (explicit/picked key, or the pickable
|
|
708
|
+
// list on refusal) will hit `resolveJiraCredentials` inside the default
|
|
709
|
+
// makeClient — gate up-front so an unconfigured caller still gets the
|
|
710
|
+
// canonical pointer, not a stack.
|
|
711
|
+
if (explicitIssueKey || pickedIssueKey) {
|
|
712
|
+
const notConfigured = notConfiguredResult();
|
|
713
|
+
if (notConfigured)
|
|
714
|
+
return notConfigured;
|
|
715
|
+
}
|
|
716
|
+
const res = await (0, board_first_1.resolveWorkAnchor)({
|
|
717
|
+
cwd: process.cwd(),
|
|
718
|
+
explicitIssueKey,
|
|
719
|
+
pickedIssueKey,
|
|
720
|
+
});
|
|
721
|
+
if (res.status === 'unbound') {
|
|
722
|
+
return textResult('Anchor: unbound — repo has no JIRA binding; work-creating flows proceed unchanged.');
|
|
723
|
+
}
|
|
724
|
+
if (res.status === 'anchored') {
|
|
725
|
+
return textResult(`Anchor: ${res.anchor.issueKey} — ${res.anchor.summary} (source: ${res.anchor.source})`);
|
|
726
|
+
}
|
|
727
|
+
return errorResult((0, board_first_1.formatRefusal)(res));
|
|
728
|
+
}
|
|
729
|
+
catch (err) {
|
|
730
|
+
if (err instanceof types_1.JiraError)
|
|
731
|
+
return errorResult(err.message);
|
|
732
|
+
return errorResult('Failed to resolve JIRA anchor.');
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
/**
|
|
736
|
+
* Handle `specship_jira_epics` (REQ-JIRATEAM-008). Lists open epics for a
|
|
737
|
+
* project — the picker source for the `/specship:jira` menu's "choose epic"
|
|
738
|
+
* step. With no `project`, falls back to the repo binding's `projectKey` so
|
|
739
|
+
* on a bound repo the caller doesn't have to duplicate the lookup.
|
|
740
|
+
*/
|
|
741
|
+
async function handleSpecshipJiraEpics(args) {
|
|
742
|
+
const notConfigured = notConfiguredResult();
|
|
743
|
+
if (notConfigured)
|
|
744
|
+
return notConfigured;
|
|
745
|
+
let project = typeof args.project === 'string' && args.project.trim()
|
|
746
|
+
? args.project.trim()
|
|
747
|
+
: undefined;
|
|
748
|
+
if (!project) {
|
|
749
|
+
try {
|
|
750
|
+
const { binding } = (0, repo_config_1.loadRepoJiraBinding)(process.cwd());
|
|
751
|
+
if (binding)
|
|
752
|
+
project = binding.projectKey;
|
|
753
|
+
}
|
|
754
|
+
catch (err) {
|
|
755
|
+
if (err instanceof types_1.JiraError)
|
|
756
|
+
return errorResult(err.message);
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
if (!project) {
|
|
760
|
+
return errorResult('No JIRA project. Pass `project`, or bind one with ' +
|
|
761
|
+
'`specship jira bind --project <KEY>` (writes specship.config.json).');
|
|
762
|
+
}
|
|
763
|
+
try {
|
|
764
|
+
const creds = (0, config_1.resolveJiraCredentials)();
|
|
765
|
+
const client = new client_1.JiraClient(creds);
|
|
766
|
+
const epics = await client.listEpics(project);
|
|
767
|
+
return textResult(formatEpics(epics, project));
|
|
768
|
+
}
|
|
769
|
+
catch (err) {
|
|
770
|
+
if (err instanceof types_1.JiraError)
|
|
771
|
+
return errorResult(err.message);
|
|
772
|
+
return errorResult('Failed to list JIRA epics.');
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
function formatEpics(epics, project) {
|
|
776
|
+
if (epics.length === 0) {
|
|
777
|
+
return (`No open epics in project ${cell(project)}.\n\n` +
|
|
778
|
+
'> Note: only epics whose status category is not "Done" are listed.');
|
|
779
|
+
}
|
|
780
|
+
const table = [
|
|
781
|
+
'| Key | Summary | Status |',
|
|
782
|
+
'| --- | --- | --- |',
|
|
783
|
+
...epics.map((e) => `| ${cell(e.key)} | ${cell(e.summary)} | ${cell(e.status)} |`),
|
|
784
|
+
].join('\n');
|
|
785
|
+
const notes = [`Project ${cell(project)}, ordered most-recently-updated first.`];
|
|
786
|
+
if (epics.length >= client_1.MAX_ISSUE_RESULTS) {
|
|
787
|
+
notes.push(`Showing the ${client_1.MAX_ISSUE_RESULTS} most recently updated.`);
|
|
788
|
+
}
|
|
789
|
+
return `${table}\n\n> Note: ${notes.join(' ')}`;
|
|
790
|
+
}
|
|
466
791
|
/**
|
|
467
792
|
* Handle `specship_jira_issue` (REQ-JIRA-003). Fetches a single issue by key
|
|
468
793
|
* and renders its full detail. Independent of the code graph — talks only to
|
|
@@ -751,7 +1076,7 @@ async function handleJiraRunCompletion(run, deps) {
|
|
|
751
1076
|
// Only on a raised PR — a failed raise never advances the issue (below),
|
|
752
1077
|
// matching REQ-JIRA-005.A2 (leave it in-progress). Never throws.
|
|
753
1078
|
const make = deps.makeJiraClient ?? defaultMakeJiraClient;
|
|
754
|
-
const note = await pushJiraReviewStatus(issueKey, outcome.url, make);
|
|
1079
|
+
const note = await pushJiraReviewStatus(issueKey, outcome.url, make, jira.specId);
|
|
755
1080
|
if (note)
|
|
756
1081
|
log(note);
|
|
757
1082
|
}
|
|
@@ -963,7 +1288,7 @@ async function handleSpecshipJiraTrack(args, deps) {
|
|
|
963
1288
|
}
|
|
964
1289
|
// Published specs are tracked too (REQ-JIRAPUB-008) — they may predate any
|
|
965
1290
|
// workflow run, so the empty-view short-circuit must consider both sources.
|
|
966
|
-
const published = deps.projectRoot ? enumeratePublishedSpecs(deps.projectRoot) : [];
|
|
1291
|
+
const published = deps.projectRoot ? (0, published_specs_1.enumeratePublishedSpecs)(deps.projectRoot) : [];
|
|
967
1292
|
if (picked.length === 0 && published.length === 0) {
|
|
968
1293
|
return textResult(formatTrack([]));
|
|
969
1294
|
}
|
|
@@ -1030,10 +1355,24 @@ async function handleSpecshipJiraTrack(args, deps) {
|
|
|
1030
1355
|
});
|
|
1031
1356
|
}
|
|
1032
1357
|
if (pub.fingerprint && live) {
|
|
1033
|
-
|
|
1034
|
-
|
|
1358
|
+
// Delegate the divergence judgement to the reconcile module so the
|
|
1359
|
+
// track view + the reconcile tool never disagree about "what counts as
|
|
1360
|
+
// an edit". Sub-task diffs need the spec's acceptance criteria, which
|
|
1361
|
+
// this row doesn't have — track only reports the content flag; a full
|
|
1362
|
+
// preview lives in `specship_jira_reconcile`.
|
|
1363
|
+
const { diffIssueVsSpec } = await Promise.resolve().then(() => __importStar(require('../jira/reconcile')));
|
|
1364
|
+
const specView = {
|
|
1365
|
+
specRelPath: pub.specRelPath,
|
|
1366
|
+
requirementId: pub.issueKey,
|
|
1367
|
+
title: pub.title,
|
|
1368
|
+
body: '',
|
|
1369
|
+
acceptance: [],
|
|
1370
|
+
};
|
|
1371
|
+
const report = diffIssueVsSpec(live, specView, pub.fingerprint);
|
|
1372
|
+
if (report.content) {
|
|
1035
1373
|
divergences.push(`⚠ ${pub.issueKey} was edited in JIRA after publish (spec: ${pub.specRelPath}) — ` +
|
|
1036
|
-
'
|
|
1374
|
+
'run specship_jira_reconcile to preview the JIRA-side changes, or ' +
|
|
1375
|
+
're-publish with specship_jira_publish to refresh the fingerprint.');
|
|
1037
1376
|
}
|
|
1038
1377
|
}
|
|
1039
1378
|
}
|
|
@@ -1041,45 +1380,6 @@ async function handleSpecshipJiraTrack(args, deps) {
|
|
|
1041
1380
|
const table = formatTrack(rows);
|
|
1042
1381
|
return textResult(divergences.length > 0 ? `${table}\n\n${divergences.join('\n')}` : table);
|
|
1043
1382
|
}
|
|
1044
|
-
/**
|
|
1045
|
-
* Enumerate the specs under `<projectRoot>/specs/` whose frontmatter carries a
|
|
1046
|
-
* `jira_issue:` key (REQ-JIRAPUB-008). Best-effort filesystem scan — unreadable
|
|
1047
|
-
* files are skipped, and an absent specs/ dir yields an empty list.
|
|
1048
|
-
*/
|
|
1049
|
-
function enumeratePublishedSpecs(projectRoot) {
|
|
1050
|
-
const specsDir = path.join(projectRoot, 'specs');
|
|
1051
|
-
let entries = [];
|
|
1052
|
-
try {
|
|
1053
|
-
entries = fs.readdirSync(specsDir);
|
|
1054
|
-
}
|
|
1055
|
-
catch {
|
|
1056
|
-
return [];
|
|
1057
|
-
}
|
|
1058
|
-
const out = [];
|
|
1059
|
-
for (const name of entries) {
|
|
1060
|
-
if (!name.toLowerCase().endsWith('.md'))
|
|
1061
|
-
continue;
|
|
1062
|
-
const full = path.join(specsDir, name);
|
|
1063
|
-
try {
|
|
1064
|
-
if (!fs.statSync(full).isFile())
|
|
1065
|
-
continue;
|
|
1066
|
-
const content = fs.readFileSync(full, 'utf8');
|
|
1067
|
-
const issueKey = (0, publish_1.readFrontmatterValue)(content, 'jira_issue');
|
|
1068
|
-
if (!issueKey)
|
|
1069
|
-
continue;
|
|
1070
|
-
out.push({
|
|
1071
|
-
issueKey,
|
|
1072
|
-
title: readSpecTitle(full, issueKey),
|
|
1073
|
-
specRelPath: path.join('specs', name),
|
|
1074
|
-
fingerprint: (0, publish_1.readFrontmatterValue)(content, 'jira_fingerprint'),
|
|
1075
|
-
});
|
|
1076
|
-
}
|
|
1077
|
-
catch {
|
|
1078
|
-
continue;
|
|
1079
|
-
}
|
|
1080
|
-
}
|
|
1081
|
-
return out;
|
|
1082
|
-
}
|
|
1083
1383
|
/**
|
|
1084
1384
|
* Handle `specship_jira_publish` (REQ-JIRAPUB-001, REQ-JIRAPUB-002): publish a
|
|
1085
1385
|
* requirement spec as a Story + Sub-tasks, then write the JIRA identity back
|
|
@@ -1264,4 +1564,461 @@ async function handleSpecshipJiraAddTask(args, deps) {
|
|
|
1264
1564
|
return errorResult('Failed to create the task in JIRA.');
|
|
1265
1565
|
}
|
|
1266
1566
|
}
|
|
1567
|
+
/**
|
|
1568
|
+
* Handle `specship_jira_regression_pack` (REQ-JIRAREG-001). Builds the model
|
|
1569
|
+
* from the loaded spec set, then upserts it against JIRA idempotently. A
|
|
1570
|
+
* dry-run planning path returns the counts without any JIRA write.
|
|
1571
|
+
*/
|
|
1572
|
+
async function handleSpecshipJiraRegressionPack(args, deps) {
|
|
1573
|
+
const notConfigured = notConfiguredResult();
|
|
1574
|
+
if (notConfigured)
|
|
1575
|
+
return notConfigured;
|
|
1576
|
+
const dryRun = args.dry_run === true;
|
|
1577
|
+
const baseSq = deps.specQueries;
|
|
1578
|
+
const sq = {
|
|
1579
|
+
getAllSpecs: () => baseSq.getAllSpecs(),
|
|
1580
|
+
getSpecsByParent: (pid) => baseSq.getSpecsByParent(pid),
|
|
1581
|
+
getLinksBySpec: (sid) => baseSq.getLinksBySpec(sid),
|
|
1582
|
+
getSpecById: (id) => baseSq.getSpecById(id),
|
|
1583
|
+
...(deps.getLinkedNodesForReq ? { getLinkedNodesForReq: deps.getLinkedNodesForReq } : {}),
|
|
1584
|
+
...(deps.getNeighbourNodes ? { getNeighbourNodes: deps.getNeighbourNodes } : {}),
|
|
1585
|
+
};
|
|
1586
|
+
const model = (0, regression_pack_1.buildRegressionPack)(sq);
|
|
1587
|
+
if (model.cases.length === 0) {
|
|
1588
|
+
return textResult('No implemented (or verified) requirements found — the regression pack ' +
|
|
1589
|
+
'has zero cases. Implement a spec and link its code before generating.');
|
|
1590
|
+
}
|
|
1591
|
+
try {
|
|
1592
|
+
const creds = (0, config_1.resolveJiraCredentials)();
|
|
1593
|
+
const projectKey = (typeof args.project === 'string' && args.project.trim()
|
|
1594
|
+
? args.project.trim()
|
|
1595
|
+
: undefined) ?? creds.project;
|
|
1596
|
+
if (!projectKey) {
|
|
1597
|
+
return textResult('No JIRA project is configured. Pass `project: "PROJ"` or save a ' +
|
|
1598
|
+
'default with `specship jira configure --project <Key>`.');
|
|
1599
|
+
}
|
|
1600
|
+
const client = deps.makeJiraClient
|
|
1601
|
+
? deps.makeJiraClient()
|
|
1602
|
+
: new client_1.JiraClient(creds);
|
|
1603
|
+
const ctx = { projectKey, dryRun };
|
|
1604
|
+
const result = await (0, regression_pack_1.upsertRegressionPack)(client, model, ctx);
|
|
1605
|
+
// Back-link written cases into their spec files (REQ-JIRAREG-001.A3) —
|
|
1606
|
+
// spec-side traceability. Best-effort per case; a failure notes and
|
|
1607
|
+
// moves on so a partial back-link never blocks the pack write itself.
|
|
1608
|
+
const backlinkNotes = [];
|
|
1609
|
+
if (!dryRun) {
|
|
1610
|
+
const specPathById = {};
|
|
1611
|
+
for (const c of model.cases)
|
|
1612
|
+
specPathById[c.criterionId] = c.specPath;
|
|
1613
|
+
for (const [criterionId, issueKey] of Object.entries(result.caseKeysByCriterion)) {
|
|
1614
|
+
const rel = specPathById[criterionId];
|
|
1615
|
+
if (!rel)
|
|
1616
|
+
continue;
|
|
1617
|
+
const absPath = path.isAbsolute(rel) ? rel : path.join(deps.projectRoot, rel);
|
|
1618
|
+
const out = (0, spec_writer_1.writeRegressionCaseKeys)(absPath, criterionId, issueKey);
|
|
1619
|
+
if (!out.ok && out.detail)
|
|
1620
|
+
backlinkNotes.push(` · ${criterionId}: ${out.detail}`);
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
const lines = [];
|
|
1624
|
+
lines.push(`${dryRun ? 'Dry-run plan for' : 'Regression Pack upserted in'} ${projectKey}:`);
|
|
1625
|
+
lines.push(`- Epic: ${result.epicKey ?? '(none)'}${result.epicCreated ? ' (created)' : ''}`);
|
|
1626
|
+
lines.push(`- Stories: ${result.storiesCreated} created, ${result.storiesUpdated} updated, ${result.storiesSkipped} skipped`);
|
|
1627
|
+
lines.push(`- Cases: ${result.casesCreated} created, ${result.casesUpdated} updated, ${result.casesSkipped} skipped (of ${model.cases.length})`);
|
|
1628
|
+
const xrefTotal = result.crossRefsCreated + result.crossRefsUpdated + result.crossRefsSkipped;
|
|
1629
|
+
if (xrefTotal > 0) {
|
|
1630
|
+
lines.push(`- Cross-refs: ${result.crossRefsCreated} created, ${result.crossRefsUpdated} updated, ${result.crossRefsSkipped} skipped`);
|
|
1631
|
+
}
|
|
1632
|
+
if (result.casesObsoleted > 0) {
|
|
1633
|
+
lines.push(`- Obsoleted: ${result.casesObsoleted} case${result.casesObsoleted === 1 ? '' : 's'} (${result.obsoletedCaseKeys.join(', ')}) — kept for run history, labelled specship-reg-obsolete.`);
|
|
1634
|
+
}
|
|
1635
|
+
if (result.orphanedEpicKeys.length > 0) {
|
|
1636
|
+
lines.push(`- Extra pack-epics found (single-epic invariant): ${result.orphanedEpicKeys.join(', ')} — merge or delete them manually.`);
|
|
1637
|
+
}
|
|
1638
|
+
if (backlinkNotes.length > 0) {
|
|
1639
|
+
lines.push('- Back-link notes:');
|
|
1640
|
+
lines.push(...backlinkNotes);
|
|
1641
|
+
}
|
|
1642
|
+
const gapReport = (0, regression_pack_1.renderDomainGapReport)(model.domainGaps);
|
|
1643
|
+
if (gapReport) {
|
|
1644
|
+
lines.push('');
|
|
1645
|
+
lines.push(gapReport);
|
|
1646
|
+
}
|
|
1647
|
+
const rephraseReport = (0, regression_pack_1.renderRephraseReport)(model.rephraseFlags);
|
|
1648
|
+
if (rephraseReport) {
|
|
1649
|
+
lines.push('');
|
|
1650
|
+
lines.push(rephraseReport);
|
|
1651
|
+
}
|
|
1652
|
+
return textResult(lines.join('\n'));
|
|
1653
|
+
}
|
|
1654
|
+
catch (err) {
|
|
1655
|
+
if (err instanceof types_1.JiraError)
|
|
1656
|
+
return errorResult(err.message);
|
|
1657
|
+
return errorResult(`Regression pack failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1660
|
+
/** Handle `specship_jira_regression_record` (REQ-JIRAREG-005). */
|
|
1661
|
+
async function handleSpecshipJiraRegressionRecord(args, deps) {
|
|
1662
|
+
const notConfigured = notConfiguredResult();
|
|
1663
|
+
if (notConfigured)
|
|
1664
|
+
return notConfigured;
|
|
1665
|
+
const caseKey = typeof args.case === 'string' ? args.case.trim() : '';
|
|
1666
|
+
const criterionId = typeof args.criterion === 'string' ? args.criterion.trim() : '';
|
|
1667
|
+
const status = typeof args.result === 'string' ? args.result.trim() : '';
|
|
1668
|
+
if (!caseKey || !criterionId || !status) {
|
|
1669
|
+
return errorResult('specship_jira_regression_record requires `case`, `criterion`, and `result`.');
|
|
1670
|
+
}
|
|
1671
|
+
const validStatuses = ['passed', 'failed', 'unexecuted'];
|
|
1672
|
+
if (!validStatuses.includes(status)) {
|
|
1673
|
+
return errorResult(`Unsupported result "${status}" — expected one of: ${validStatuses.join(', ')}.`);
|
|
1674
|
+
}
|
|
1675
|
+
const result = {
|
|
1676
|
+
caseKey,
|
|
1677
|
+
criterionId,
|
|
1678
|
+
status: status,
|
|
1679
|
+
executor: args.executor === 'agent' ? 'agent' : 'human',
|
|
1680
|
+
...(typeof args.harness === 'string' && args.harness.trim()
|
|
1681
|
+
? { harness: args.harness.trim() }
|
|
1682
|
+
: {}),
|
|
1683
|
+
...(typeof args.run === 'string' && args.run.trim() ? { runId: args.run.trim() } : {}),
|
|
1684
|
+
...(typeof args.log_ref === 'string' || typeof args.error === 'string'
|
|
1685
|
+
? {
|
|
1686
|
+
evidence: {
|
|
1687
|
+
...(typeof args.log_ref === 'string' ? { logRef: args.log_ref } : {}),
|
|
1688
|
+
...(typeof args.error === 'string' ? { error: args.error } : {}),
|
|
1689
|
+
},
|
|
1690
|
+
}
|
|
1691
|
+
: {}),
|
|
1692
|
+
};
|
|
1693
|
+
try {
|
|
1694
|
+
const creds = (0, config_1.resolveJiraCredentials)();
|
|
1695
|
+
const client = deps.makeJiraClient
|
|
1696
|
+
? deps.makeJiraClient()
|
|
1697
|
+
: new client_1.JiraClient(creds);
|
|
1698
|
+
const outcome = await (0, regression_pack_1.recordRunResult)({ client, specQueries: deps.specQueries }, result);
|
|
1699
|
+
const lines = [];
|
|
1700
|
+
lines.push(`${caseKey} recorded ${outcome.status} — comment ${outcome.comment}, transition ${outcome.transition}, validates-link ${outcome.linked ? 'updated' : 'untouched'}.`);
|
|
1701
|
+
if (outcome.triageHint)
|
|
1702
|
+
lines.push(outcome.triageHint);
|
|
1703
|
+
return textResult(lines.join('\n'));
|
|
1704
|
+
}
|
|
1705
|
+
catch (err) {
|
|
1706
|
+
if (err instanceof types_1.JiraError)
|
|
1707
|
+
return errorResult(err.message);
|
|
1708
|
+
return errorResult(`Regression record failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1709
|
+
}
|
|
1710
|
+
}
|
|
1711
|
+
/**
|
|
1712
|
+
* Handle `specship_jira_coverage` (REQ-JIRATEAM-004). Builds the sprint
|
|
1713
|
+
* coverage report, renders markdown, and optionally upserts a single
|
|
1714
|
+
* watermarked JIRA comment (A3) — never transitions or edits any issue (A4).
|
|
1715
|
+
*
|
|
1716
|
+
* SECURITY: no credential is echoed (REQ-JIRA-009); only public issue keys,
|
|
1717
|
+
* titles, and statuses appear in output.
|
|
1718
|
+
*/
|
|
1719
|
+
async function handleSpecshipJiraCoverage(args, deps) {
|
|
1720
|
+
const notConfigured = notConfiguredResult();
|
|
1721
|
+
if (notConfigured)
|
|
1722
|
+
return notConfigured;
|
|
1723
|
+
const { buildCoverageReport, formatCoverageMarkdown, COVERAGE_COMMENT_WATERMARK } = await Promise.resolve().then(() => __importStar(require('../jira/coverage')));
|
|
1724
|
+
const { upsertWatermarkedComment } = await Promise.resolve().then(() => __importStar(require('../jira/publish')));
|
|
1725
|
+
const post = args.post === true;
|
|
1726
|
+
const issueKeyArg = typeof args.issue_key === 'string' && args.issue_key.trim()
|
|
1727
|
+
? args.issue_key.trim()
|
|
1728
|
+
: undefined;
|
|
1729
|
+
if (post && !issueKeyArg) {
|
|
1730
|
+
return textResult('Posting the coverage report needs an anchor issue — pass "issue_key" (e.g., the epic key) alongside "post": true.');
|
|
1731
|
+
}
|
|
1732
|
+
const sprint = typeof args.sprint === 'string' && args.sprint.trim()
|
|
1733
|
+
? args.sprint.trim()
|
|
1734
|
+
: undefined;
|
|
1735
|
+
let creds;
|
|
1736
|
+
try {
|
|
1737
|
+
creds = (0, config_1.resolveJiraCredentials)();
|
|
1738
|
+
}
|
|
1739
|
+
catch (err) {
|
|
1740
|
+
return errorResult(err instanceof Error ? err.message : String(err));
|
|
1741
|
+
}
|
|
1742
|
+
const project = (typeof args.project === 'string' && args.project.trim()
|
|
1743
|
+
? args.project.trim()
|
|
1744
|
+
: undefined) ?? creds.project;
|
|
1745
|
+
if (!project) {
|
|
1746
|
+
return textResult('No JIRA project configured. Pass "project", or configure a default with `specship jira configure --project <KEY>`.');
|
|
1747
|
+
}
|
|
1748
|
+
const client = deps.makeJiraClient
|
|
1749
|
+
? deps.makeJiraClient()
|
|
1750
|
+
: new client_1.JiraClient(creds);
|
|
1751
|
+
try {
|
|
1752
|
+
const report = await buildCoverageReport({
|
|
1753
|
+
client,
|
|
1754
|
+
projectRoot: deps.projectRoot,
|
|
1755
|
+
specQueries: deps.specQueries,
|
|
1756
|
+
project,
|
|
1757
|
+
sprint,
|
|
1758
|
+
});
|
|
1759
|
+
const markdown = formatCoverageMarkdown(report);
|
|
1760
|
+
if (!post)
|
|
1761
|
+
return textResult(markdown);
|
|
1762
|
+
// Post path (A3): the only write this handler makes — a single watermarked
|
|
1763
|
+
// comment, upserted in place on re-post. Never a transition/edit (A4).
|
|
1764
|
+
const commentClient = client;
|
|
1765
|
+
const result = await upsertWatermarkedComment(commentClient, issueKeyArg, COVERAGE_COMMENT_WATERMARK, markdown);
|
|
1766
|
+
const verb = result.action === 'updated' ? 'Updated existing' : 'Posted new';
|
|
1767
|
+
return textResult(`${markdown}\n\n> ${verb} coverage comment on ${issueKeyArg}.`);
|
|
1768
|
+
}
|
|
1769
|
+
catch (err) {
|
|
1770
|
+
if (err instanceof types_1.JiraError)
|
|
1771
|
+
return errorResult(err.message);
|
|
1772
|
+
return errorResult(`Failed to build the coverage report: ${err instanceof Error ? err.message : String(err)}`);
|
|
1773
|
+
}
|
|
1774
|
+
}
|
|
1775
|
+
/**
|
|
1776
|
+
* Load the requirement view a diff needs — id, title, body, acceptance — from
|
|
1777
|
+
* the SpecQueries handle. `requirementId` is the frontmatter/heading id (which
|
|
1778
|
+
* for a published spec is `reqIdForIssue(issueKey)`).
|
|
1779
|
+
*/
|
|
1780
|
+
function loadSpecView(sq, requirementId, specRelPath) {
|
|
1781
|
+
const spec = sq.getSpecById?.(requirementId);
|
|
1782
|
+
if (!spec || spec.kind !== 'requirement')
|
|
1783
|
+
return null;
|
|
1784
|
+
const acceptance = (sq.getSpecsByParent?.(requirementId) ?? [])
|
|
1785
|
+
.filter((c) => c.kind === 'acceptance')
|
|
1786
|
+
.map((c) => ({ id: c.id, text: (c.title || c.body || '').trim() }))
|
|
1787
|
+
.filter((c) => c.text.length > 0);
|
|
1788
|
+
return {
|
|
1789
|
+
specRelPath,
|
|
1790
|
+
requirementId,
|
|
1791
|
+
title: spec.title,
|
|
1792
|
+
body: spec.body ?? '',
|
|
1793
|
+
acceptance,
|
|
1794
|
+
};
|
|
1795
|
+
}
|
|
1796
|
+
/** Format one report as a human-readable diff block. */
|
|
1797
|
+
function formatReconcileReport(report) {
|
|
1798
|
+
const lines = [];
|
|
1799
|
+
lines.push(`### ${report.issueKey} — ${report.specRelPath}`);
|
|
1800
|
+
if (report.content) {
|
|
1801
|
+
lines.push('');
|
|
1802
|
+
lines.push('**Content divergence (edited in JIRA after publish)**');
|
|
1803
|
+
lines.push('');
|
|
1804
|
+
lines.push(`- Live summary: ${report.content.liveSummary}`);
|
|
1805
|
+
lines.push(`- Live fingerprint: \`${report.content.liveFingerprint}\``);
|
|
1806
|
+
lines.push(`- Stored fingerprint: \`${report.content.storedFingerprint}\``);
|
|
1807
|
+
lines.push('');
|
|
1808
|
+
lines.push('_Live description:_');
|
|
1809
|
+
lines.push('');
|
|
1810
|
+
lines.push(report.content.liveDescription || '_(empty)_');
|
|
1811
|
+
}
|
|
1812
|
+
if (report.subtasks.length > 0) {
|
|
1813
|
+
lines.push('');
|
|
1814
|
+
lines.push('**Sub-tasks added in JIRA (proposed new acceptance criteria)**');
|
|
1815
|
+
lines.push('');
|
|
1816
|
+
lines.push('| Sub-task | Proposed id | Proposed criterion |');
|
|
1817
|
+
lines.push('| --- | --- | --- |');
|
|
1818
|
+
for (const d of report.subtasks) {
|
|
1819
|
+
lines.push(`| ${cell(d.subtaskKey)} | ${cell(d.proposedCriterionId)} | ${cell(d.proposedCriterionText)} |`);
|
|
1820
|
+
}
|
|
1821
|
+
}
|
|
1822
|
+
return lines.join('\n');
|
|
1823
|
+
}
|
|
1824
|
+
/**
|
|
1825
|
+
* Handle `specship_jira_reconcile` (REQ-JIRATEAM-005).
|
|
1826
|
+
*
|
|
1827
|
+
* Two modes:
|
|
1828
|
+
* - preview (default, A3): enumerate every published spec, live-read the
|
|
1829
|
+
* issue, diff, and return the proposed spec amendment. NO writes.
|
|
1830
|
+
* - apply (A4): fold the previewed diff into the spec file and re-publish so
|
|
1831
|
+
* the fingerprint refreshes. Preview-gated: apply refuses unless
|
|
1832
|
+
* `expected_live_fingerprint` matches the issue's current live fingerprint,
|
|
1833
|
+
* which the caller MUST have just received from a preview call. This
|
|
1834
|
+
* encodes the "preview first, then explicit user confirmation" discipline.
|
|
1835
|
+
*/
|
|
1836
|
+
async function handleSpecshipJiraReconcile(args, deps) {
|
|
1837
|
+
const notConfigured = notConfiguredResult();
|
|
1838
|
+
if (notConfigured)
|
|
1839
|
+
return notConfigured;
|
|
1840
|
+
const { diffIssueVsSpec, reportHasDivergence } = await Promise.resolve().then(() => __importStar(require('../jira/reconcile')));
|
|
1841
|
+
const { appendAcceptanceCriterion, applyContentAmendment, amendSpecFile, } = await Promise.resolve().then(() => __importStar(require('../jira/spec-amend')));
|
|
1842
|
+
const mode = args.mode === 'apply' ? 'apply' : 'preview';
|
|
1843
|
+
const issueKeyArg = typeof args.issue_key === 'string' && args.issue_key.trim()
|
|
1844
|
+
? args.issue_key.trim()
|
|
1845
|
+
: undefined;
|
|
1846
|
+
const published = (0, published_specs_1.enumeratePublishedSpecs)(deps.projectRoot);
|
|
1847
|
+
const scope = issueKeyArg
|
|
1848
|
+
? published.filter((p) => p.issueKey === issueKeyArg)
|
|
1849
|
+
: published;
|
|
1850
|
+
if (scope.length === 0) {
|
|
1851
|
+
return textResult(issueKeyArg
|
|
1852
|
+
? `No published spec found for ${issueKeyArg} — reconcile only applies to specs with a jira_issue: key.`
|
|
1853
|
+
: 'No published specs found — nothing to reconcile.');
|
|
1854
|
+
}
|
|
1855
|
+
let client;
|
|
1856
|
+
try {
|
|
1857
|
+
client = deps.makeJiraClient
|
|
1858
|
+
? deps.makeJiraClient()
|
|
1859
|
+
: new client_1.JiraClient((0, config_1.resolveJiraCredentials)());
|
|
1860
|
+
}
|
|
1861
|
+
catch (err) {
|
|
1862
|
+
return errorResult(err instanceof Error ? err.message : String(err));
|
|
1863
|
+
}
|
|
1864
|
+
const sq = deps.specQueries;
|
|
1865
|
+
if (mode === 'apply') {
|
|
1866
|
+
if (!issueKeyArg) {
|
|
1867
|
+
return errorResult('apply mode needs "issue_key". Run preview first, then apply the confirmed diff for one issue.');
|
|
1868
|
+
}
|
|
1869
|
+
const pub = scope[0];
|
|
1870
|
+
let live;
|
|
1871
|
+
try {
|
|
1872
|
+
live = (await client.getIssue(issueKeyArg)).issue;
|
|
1873
|
+
}
|
|
1874
|
+
catch (err) {
|
|
1875
|
+
return errorResult(err instanceof Error ? err.message : String(err));
|
|
1876
|
+
}
|
|
1877
|
+
// Preview-first gate (A3): apply refuses unless the caller-supplied
|
|
1878
|
+
// `expected_live_fingerprint` matches the issue's current live
|
|
1879
|
+
// fingerprint. That value only exists if the caller just saw it in a
|
|
1880
|
+
// preview and the user confirmed it — no preview → no apply.
|
|
1881
|
+
const expectedFp = typeof args.expected_live_fingerprint === 'string'
|
|
1882
|
+
? args.expected_live_fingerprint.trim()
|
|
1883
|
+
: '';
|
|
1884
|
+
const liveFp = (0, publish_1.issueContentFingerprint)(live.summary ?? '', live.description ?? '');
|
|
1885
|
+
if (!expectedFp || expectedFp !== liveFp) {
|
|
1886
|
+
return errorResult('apply refused: no matching preview. Run mode:"preview" for ' +
|
|
1887
|
+
`${issueKeyArg}, show the user the diff, and re-call apply with the ` +
|
|
1888
|
+
`expected_live_fingerprint from that preview (live fingerprint: ${liveFp}).`);
|
|
1889
|
+
}
|
|
1890
|
+
const requirementId = (0, spec_generator_1.reqIdForIssue)(issueKeyArg);
|
|
1891
|
+
const specView = loadSpecView(sq, requirementId, pub.specRelPath);
|
|
1892
|
+
if (!specView) {
|
|
1893
|
+
return errorResult(`Spec ${requirementId} not found in the index — run specship sync first.`);
|
|
1894
|
+
}
|
|
1895
|
+
const report = diffIssueVsSpec(live, specView, pub.fingerprint);
|
|
1896
|
+
const acceptContent = args.accept_content === true;
|
|
1897
|
+
const acceptSubtasks = Array.isArray(args.accept_subtasks)
|
|
1898
|
+
? args.accept_subtasks.filter((k) => typeof k === 'string')
|
|
1899
|
+
: [];
|
|
1900
|
+
if (!acceptContent && acceptSubtasks.length === 0) {
|
|
1901
|
+
return errorResult('apply refused: nothing to accept. Pass accept_content: true and/or ' +
|
|
1902
|
+
'accept_subtasks: [<Sub-task keys>] with the confirmed proposals.');
|
|
1903
|
+
}
|
|
1904
|
+
const notes = [];
|
|
1905
|
+
if (acceptContent) {
|
|
1906
|
+
if (!report.content) {
|
|
1907
|
+
notes.push('accept_content: true but no content divergence — skipped.');
|
|
1908
|
+
}
|
|
1909
|
+
else {
|
|
1910
|
+
const out = amendSpecFile(deps.projectRoot, pub.absPath, (src) => applyContentAmendment(src, requirementId, live.summary ?? '', live.description ?? ''));
|
|
1911
|
+
notes.push(out.detail);
|
|
1912
|
+
}
|
|
1913
|
+
}
|
|
1914
|
+
for (const stKey of acceptSubtasks) {
|
|
1915
|
+
const div = report.subtasks.find((d) => d.subtaskKey === stKey);
|
|
1916
|
+
if (!div) {
|
|
1917
|
+
notes.push(`${stKey}: no matching Sub-task divergence — skipped.`);
|
|
1918
|
+
continue;
|
|
1919
|
+
}
|
|
1920
|
+
const out = amendSpecFile(deps.projectRoot, pub.absPath, (src) => appendAcceptanceCriterion(src, requirementId, div.proposedCriterionId, div.proposedCriterionText));
|
|
1921
|
+
notes.push(`${stKey} → ${div.proposedCriterionId}: ${out.detail}`);
|
|
1922
|
+
}
|
|
1923
|
+
// Re-publish so the fingerprint refreshes and the next preview reports
|
|
1924
|
+
// no divergence. The publish source is built from what we JUST ACCEPTED
|
|
1925
|
+
// — the live summary/description (when accepted) and the appended
|
|
1926
|
+
// criteria — NOT from the SpecQueries index, which lags the file
|
|
1927
|
+
// write until the caller runs `specship sync`.
|
|
1928
|
+
try {
|
|
1929
|
+
const publishClient = deps.makePublishClient
|
|
1930
|
+
? deps.makePublishClient()
|
|
1931
|
+
: client;
|
|
1932
|
+
const effectiveTitle = acceptContent ? live.summary ?? '' : specView.title;
|
|
1933
|
+
const effectiveBody = acceptContent ? live.description ?? '' : specView.body;
|
|
1934
|
+
const effectiveAcceptance = [
|
|
1935
|
+
...specView.acceptance,
|
|
1936
|
+
...report.subtasks
|
|
1937
|
+
.filter((d) => acceptSubtasks.includes(d.subtaskKey))
|
|
1938
|
+
.map((d) => ({ id: d.proposedCriterionId, text: d.proposedCriterionText })),
|
|
1939
|
+
];
|
|
1940
|
+
const creds = (0, config_1.resolveJiraCredentials)();
|
|
1941
|
+
const projectKey = creds.project;
|
|
1942
|
+
if (projectKey) {
|
|
1943
|
+
const result = await (0, publish_1.publishSpecToJira)(publishClient, {
|
|
1944
|
+
specId: requirementId,
|
|
1945
|
+
title: effectiveTitle,
|
|
1946
|
+
body: effectiveBody,
|
|
1947
|
+
specRelPath: pub.specRelPath,
|
|
1948
|
+
acceptance: effectiveAcceptance,
|
|
1949
|
+
}, { projectKey }, issueKeyArg);
|
|
1950
|
+
(0, publish_1.writeBackJiraIdentity)(pub.absPath, issueKeyArg, {
|
|
1951
|
+
fingerprint: result.fingerprint,
|
|
1952
|
+
reId: null,
|
|
1953
|
+
renameTo: null,
|
|
1954
|
+
});
|
|
1955
|
+
notes.push(`re-published ${issueKeyArg} and refreshed the fingerprint.`);
|
|
1956
|
+
}
|
|
1957
|
+
else {
|
|
1958
|
+
notes.push('skipped re-publish: no default JIRA project configured — pass project via specship_jira_publish to refresh the fingerprint.');
|
|
1959
|
+
}
|
|
1960
|
+
}
|
|
1961
|
+
catch (err) {
|
|
1962
|
+
notes.push(`re-publish failed (spec amended on disk): ${err instanceof Error ? err.message : String(err)}`);
|
|
1963
|
+
}
|
|
1964
|
+
return textResult([
|
|
1965
|
+
`Applied ${issueKeyArg} → ${pub.specRelPath}:`,
|
|
1966
|
+
...notes.map((n) => `- ${n}`),
|
|
1967
|
+
'',
|
|
1968
|
+
'Run `specship sync` so the index reflects the spec edits.',
|
|
1969
|
+
].join('\n'));
|
|
1970
|
+
}
|
|
1971
|
+
// preview mode (default): read + diff every published spec in scope.
|
|
1972
|
+
const blocks = [];
|
|
1973
|
+
const clean = [];
|
|
1974
|
+
for (const pub of scope) {
|
|
1975
|
+
let live;
|
|
1976
|
+
try {
|
|
1977
|
+
live = (await client.getIssue(pub.issueKey)).issue;
|
|
1978
|
+
}
|
|
1979
|
+
catch (err) {
|
|
1980
|
+
blocks.push(`### ${pub.issueKey} — ${pub.specRelPath}\n\n_JIRA read failed: ${err instanceof Error ? err.message : String(err)}_`);
|
|
1981
|
+
continue;
|
|
1982
|
+
}
|
|
1983
|
+
const requirementId = (0, spec_generator_1.reqIdForIssue)(pub.issueKey);
|
|
1984
|
+
const specView = loadSpecView(sq, requirementId, pub.specRelPath);
|
|
1985
|
+
if (!specView) {
|
|
1986
|
+
blocks.push(`### ${pub.issueKey} — ${pub.specRelPath}\n\n_Spec ${requirementId} not in the index — run specship sync._`);
|
|
1987
|
+
continue;
|
|
1988
|
+
}
|
|
1989
|
+
const report = diffIssueVsSpec(live, specView, pub.fingerprint);
|
|
1990
|
+
if (!reportHasDivergence(report)) {
|
|
1991
|
+
clean.push(pub.issueKey);
|
|
1992
|
+
continue;
|
|
1993
|
+
}
|
|
1994
|
+
const liveFp = (0, publish_1.issueContentFingerprint)(live.summary ?? '', live.description ?? '');
|
|
1995
|
+
const machine = '```json\n' +
|
|
1996
|
+
JSON.stringify({
|
|
1997
|
+
issue_key: pub.issueKey,
|
|
1998
|
+
spec_rel_path: pub.specRelPath,
|
|
1999
|
+
expected_live_fingerprint: liveFp,
|
|
2000
|
+
content: report.content
|
|
2001
|
+
? { live_summary: report.content.liveSummary }
|
|
2002
|
+
: null,
|
|
2003
|
+
subtasks: report.subtasks.map((d) => ({
|
|
2004
|
+
subtask_key: d.subtaskKey,
|
|
2005
|
+
proposed_criterion_id: d.proposedCriterionId,
|
|
2006
|
+
proposed_criterion_text: d.proposedCriterionText,
|
|
2007
|
+
})),
|
|
2008
|
+
}, null, 2) +
|
|
2009
|
+
'\n```';
|
|
2010
|
+
blocks.push(`${formatReconcileReport(report)}\n\n${machine}`);
|
|
2011
|
+
}
|
|
2012
|
+
const header = blocks.length === 0
|
|
2013
|
+
? 'No divergences — every published spec matches its JIRA issue.'
|
|
2014
|
+
: `${blocks.length} issue${blocks.length === 1 ? '' : 's'} diverged.`;
|
|
2015
|
+
const footer = blocks.length > 0
|
|
2016
|
+
? '\n\n> Preview only — no file was modified. Show the diff to the user; only ' +
|
|
2017
|
+
'after they explicitly confirm, call again with mode:"apply", issue_key, the ' +
|
|
2018
|
+
'confirmed accept_content / accept_subtasks, and expected_live_fingerprint ' +
|
|
2019
|
+
'from the JSON block above.'
|
|
2020
|
+
: '';
|
|
2021
|
+
const cleanNote = clean.length > 0 ? `\n\nIn sync: ${clean.join(', ')}.` : '';
|
|
2022
|
+
return textResult(`${header}\n\n${blocks.join('\n\n')}${cleanNote}${footer}`);
|
|
2023
|
+
}
|
|
1267
2024
|
//# sourceMappingURL=jira-tools.js.map
|