@specship/specship-win32-arm64 0.13.0 → 0.14.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 +180 -1
- package/lib/dist/bin/specship.js.map +1 -1
- package/lib/dist/jira/auth.d.ts +17 -0
- package/lib/dist/jira/auth.d.ts.map +1 -0
- package/lib/dist/jira/auth.js +33 -0
- package/lib/dist/jira/auth.js.map +1 -0
- package/lib/dist/jira/client.d.ts +120 -0
- package/lib/dist/jira/client.d.ts.map +1 -0
- package/lib/dist/jira/client.js +316 -0
- package/lib/dist/jira/client.js.map +1 -0
- package/lib/dist/jira/config.d.ts +45 -0
- package/lib/dist/jira/config.d.ts.map +1 -0
- package/lib/dist/jira/config.js +178 -0
- package/lib/dist/jira/config.js.map +1 -0
- package/lib/dist/jira/index.d.ts +8 -0
- package/lib/dist/jira/index.d.ts.map +1 -0
- package/lib/dist/jira/index.js +24 -0
- package/lib/dist/jira/index.js.map +1 -0
- package/lib/dist/jira/pull-request.d.ts +72 -0
- package/lib/dist/jira/pull-request.d.ts.map +1 -0
- package/lib/dist/jira/pull-request.js +127 -0
- package/lib/dist/jira/pull-request.js.map +1 -0
- package/lib/dist/jira/spec-generator.d.ts +34 -0
- package/lib/dist/jira/spec-generator.d.ts.map +1 -0
- package/lib/dist/jira/spec-generator.js +138 -0
- package/lib/dist/jira/spec-generator.js.map +1 -0
- package/lib/dist/jira/spec-writer.d.ts +36 -0
- package/lib/dist/jira/spec-writer.d.ts.map +1 -0
- package/lib/dist/jira/spec-writer.js +143 -0
- package/lib/dist/jira/spec-writer.js.map +1 -0
- package/lib/dist/jira/types.d.ts +174 -0
- package/lib/dist/jira/types.d.ts.map +1 -0
- package/lib/dist/jira/types.js +60 -0
- package/lib/dist/jira/types.js.map +1 -0
- package/lib/dist/mcp/jira-tools.d.ts +234 -0
- package/lib/dist/mcp/jira-tools.d.ts.map +1 -0
- package/lib/dist/mcp/jira-tools.js +787 -0
- package/lib/dist/mcp/jira-tools.js.map +1 -0
- 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 +10 -0
- package/lib/dist/mcp/server-instructions.js.map +1 -1
- package/lib/dist/mcp/tools.d.ts.map +1 -1
- package/lib/dist/mcp/tools.js +32 -0
- package/lib/dist/mcp/tools.js.map +1 -1
- package/lib/dist/server/cli.js +1 -1
- package/lib/dist/server/routes/sse.js +1 -1
- package/lib/dist/server/server.js +1 -1
- package/lib/dist/statusline/index.d.ts +19 -0
- package/lib/dist/statusline/index.d.ts.map +1 -1
- package/lib/dist/statusline/index.js +96 -3
- package/lib/dist/statusline/index.js.map +1 -1
- package/lib/dist/statusline/render.d.ts +24 -0
- package/lib/dist/statusline/render.d.ts.map +1 -1
- package/lib/dist/statusline/render.js +26 -5
- package/lib/dist/statusline/render.js.map +1 -1
- package/lib/dist/workflows/executor.d.ts +25 -1
- package/lib/dist/workflows/executor.d.ts.map +1 -1
- package/lib/dist/workflows/executor.js +28 -3
- package/lib/dist/workflows/executor.js.map +1 -1
- package/lib/node_modules/.package-lock.json +11 -20
- package/lib/node_modules/ajv/node_modules/fast-uri/index.js +1 -1
- package/lib/node_modules/ajv/node_modules/fast-uri/package.json +1 -1
- package/lib/node_modules/ajv/node_modules/fast-uri/test/security.test.js +28 -0
- package/lib/node_modules/toad-cache/README.md +10 -9
- package/lib/node_modules/toad-cache/dist/toad-cache.cjs +139 -139
- package/lib/node_modules/toad-cache/dist/toad-cache.mjs +136 -140
- package/lib/node_modules/toad-cache/package.json +8 -8
- package/lib/node_modules/toad-cache/toad-cache.d.cts +20 -14
- package/lib/node_modules/toad-cache/toad-cache.d.ts +18 -14
- package/lib/node_modules/yaml/README.md +14 -31
- package/lib/node_modules/yaml/browser/dist/compose/compose-collection.js +3 -3
- package/lib/node_modules/yaml/browser/dist/compose/compose-doc.js +0 -1
- package/lib/node_modules/yaml/browser/dist/compose/compose-node.js +4 -21
- package/lib/node_modules/yaml/browser/dist/compose/compose-scalar.js +8 -14
- package/lib/node_modules/yaml/browser/dist/compose/composer.js +2 -4
- package/lib/node_modules/yaml/browser/dist/compose/resolve-block-map.js +0 -2
- package/lib/node_modules/yaml/browser/dist/compose/resolve-block-seq.js +1 -3
- package/lib/node_modules/yaml/browser/dist/compose/resolve-flow-collection.js +2 -8
- package/lib/node_modules/yaml/browser/dist/compose/resolve-flow-scalar.js +4 -6
- package/lib/node_modules/yaml/browser/dist/compose/resolve-props.js +5 -3
- package/lib/node_modules/yaml/browser/dist/compose/util-empty-scalar-position.js +2 -1
- package/lib/node_modules/yaml/browser/dist/compose/util-map-includes.js +5 -1
- package/lib/node_modules/yaml/browser/dist/doc/Document.js +2 -3
- package/lib/node_modules/yaml/browser/dist/doc/anchors.js +2 -1
- package/lib/node_modules/yaml/browser/dist/doc/applyReviver.js +0 -1
- package/lib/node_modules/yaml/browser/dist/doc/createNode.js +2 -1
- package/lib/node_modules/yaml/browser/dist/errors.js +1 -1
- package/lib/node_modules/yaml/browser/dist/log.js +6 -1
- package/lib/node_modules/yaml/browser/dist/node_modules/tslib/tslib.es6.js +21 -0
- package/lib/node_modules/yaml/browser/dist/nodes/Alias.js +12 -27
- package/lib/node_modules/yaml/browser/dist/nodes/YAMLMap.js +1 -1
- package/lib/node_modules/yaml/browser/dist/nodes/addPairToJSMap.js +49 -8
- package/lib/node_modules/yaml/browser/dist/parse/lexer.js +24 -28
- package/lib/node_modules/yaml/browser/dist/parse/parser.js +8 -31
- package/lib/node_modules/yaml/browser/dist/public-api.js +0 -3
- package/lib/node_modules/yaml/browser/dist/schema/Schema.js +2 -1
- package/lib/node_modules/yaml/browser/dist/schema/json/schema.js +1 -1
- package/lib/node_modules/yaml/browser/dist/schema/tags.js +16 -29
- package/lib/node_modules/yaml/browser/dist/schema/yaml-1.1/binary.js +13 -5
- package/lib/node_modules/yaml/browser/dist/schema/yaml-1.1/pairs.js +1 -1
- package/lib/node_modules/yaml/browser/dist/schema/yaml-1.1/schema.js +0 -2
- package/lib/node_modules/yaml/browser/dist/schema/yaml-1.1/timestamp.js +1 -1
- package/lib/node_modules/yaml/browser/dist/stringify/stringify.js +5 -10
- package/lib/node_modules/yaml/browser/dist/stringify/stringifyCollection.js +3 -13
- package/lib/node_modules/yaml/browser/dist/stringify/stringifyNumber.js +2 -3
- package/lib/node_modules/yaml/browser/dist/stringify/stringifyPair.js +1 -1
- package/lib/node_modules/yaml/browser/dist/stringify/stringifyString.js +16 -24
- package/lib/node_modules/yaml/browser/dist/util.js +1 -1
- package/lib/node_modules/yaml/dist/cli.d.ts +1 -1
- package/lib/node_modules/yaml/dist/cli.mjs +3 -5
- package/lib/node_modules/yaml/dist/compose/compose-collection.d.ts +4 -4
- package/lib/node_modules/yaml/dist/compose/compose-collection.js +3 -3
- package/lib/node_modules/yaml/dist/compose/compose-doc.d.ts +6 -6
- package/lib/node_modules/yaml/dist/compose/compose-doc.js +0 -1
- package/lib/node_modules/yaml/dist/compose/compose-node.d.ts +7 -8
- package/lib/node_modules/yaml/dist/compose/compose-node.js +4 -21
- package/lib/node_modules/yaml/dist/compose/compose-scalar.d.ts +4 -4
- package/lib/node_modules/yaml/dist/compose/compose-scalar.js +7 -13
- package/lib/node_modules/yaml/dist/compose/composer.d.ts +6 -7
- package/lib/node_modules/yaml/dist/compose/composer.js +3 -6
- package/lib/node_modules/yaml/dist/compose/resolve-block-map.d.ts +5 -5
- package/lib/node_modules/yaml/dist/compose/resolve-block-map.js +0 -2
- package/lib/node_modules/yaml/dist/compose/resolve-block-scalar.d.ts +5 -5
- package/lib/node_modules/yaml/dist/compose/resolve-block-seq.d.ts +5 -5
- package/lib/node_modules/yaml/dist/compose/resolve-block-seq.js +1 -3
- package/lib/node_modules/yaml/dist/compose/resolve-end.d.ts +2 -2
- package/lib/node_modules/yaml/dist/compose/resolve-flow-collection.d.ts +7 -7
- package/lib/node_modules/yaml/dist/compose/resolve-flow-collection.js +2 -8
- package/lib/node_modules/yaml/dist/compose/resolve-flow-scalar.d.ts +4 -4
- package/lib/node_modules/yaml/dist/compose/resolve-flow-scalar.js +4 -6
- package/lib/node_modules/yaml/dist/compose/resolve-props.d.ts +2 -2
- package/lib/node_modules/yaml/dist/compose/resolve-props.js +5 -3
- package/lib/node_modules/yaml/dist/compose/util-contains-newline.d.ts +1 -1
- package/lib/node_modules/yaml/dist/compose/util-empty-scalar-position.d.ts +1 -1
- package/lib/node_modules/yaml/dist/compose/util-empty-scalar-position.js +2 -1
- package/lib/node_modules/yaml/dist/compose/util-flow-indent-check.d.ts +2 -2
- package/lib/node_modules/yaml/dist/compose/util-map-includes.d.ts +3 -3
- package/lib/node_modules/yaml/dist/compose/util-map-includes.js +5 -1
- package/lib/node_modules/yaml/dist/doc/Document.d.ts +11 -11
- package/lib/node_modules/yaml/dist/doc/Document.js +2 -3
- package/lib/node_modules/yaml/dist/doc/anchors.d.ts +2 -2
- package/lib/node_modules/yaml/dist/doc/anchors.js +2 -1
- package/lib/node_modules/yaml/dist/doc/applyReviver.js +0 -1
- package/lib/node_modules/yaml/dist/doc/createNode.d.ts +4 -4
- package/lib/node_modules/yaml/dist/doc/createNode.js +2 -1
- package/lib/node_modules/yaml/dist/doc/directives.d.ts +1 -1
- package/lib/node_modules/yaml/dist/errors.d.ts +1 -1
- package/lib/node_modules/yaml/dist/errors.js +1 -1
- package/lib/node_modules/yaml/dist/index.d.ts +18 -21
- package/lib/node_modules/yaml/dist/log.js +4 -4
- package/lib/node_modules/yaml/dist/nodes/Alias.d.ts +9 -10
- package/lib/node_modules/yaml/dist/nodes/Alias.js +11 -26
- package/lib/node_modules/yaml/dist/nodes/Collection.d.ts +4 -4
- package/lib/node_modules/yaml/dist/nodes/Node.d.ts +9 -16
- package/lib/node_modules/yaml/dist/nodes/Pair.d.ts +8 -9
- package/lib/node_modules/yaml/dist/nodes/Scalar.d.ts +4 -9
- package/lib/node_modules/yaml/dist/nodes/YAMLMap.d.ts +10 -10
- package/lib/node_modules/yaml/dist/nodes/YAMLSeq.d.ts +10 -10
- package/lib/node_modules/yaml/dist/nodes/addPairToJSMap.d.ts +3 -3
- package/lib/node_modules/yaml/dist/nodes/addPairToJSMap.js +48 -7
- package/lib/node_modules/yaml/dist/nodes/identity.d.ts +7 -7
- package/lib/node_modules/yaml/dist/nodes/toJS.d.ts +2 -4
- package/lib/node_modules/yaml/dist/options.d.ts +10 -22
- package/lib/node_modules/yaml/dist/parse/cst-scalar.d.ts +4 -4
- package/lib/node_modules/yaml/dist/parse/cst-stringify.d.ts +1 -1
- package/lib/node_modules/yaml/dist/parse/cst-visit.d.ts +2 -2
- package/lib/node_modules/yaml/dist/parse/cst.d.ts +3 -4
- package/lib/node_modules/yaml/dist/parse/lexer.d.ts +1 -1
- package/lib/node_modules/yaml/dist/parse/lexer.js +24 -28
- package/lib/node_modules/yaml/dist/parse/parser.d.ts +4 -4
- package/lib/node_modules/yaml/dist/parse/parser.js +9 -33
- package/lib/node_modules/yaml/dist/public-api.d.ts +5 -6
- package/lib/node_modules/yaml/dist/public-api.js +0 -3
- package/lib/node_modules/yaml/dist/schema/Schema.d.ts +5 -4
- package/lib/node_modules/yaml/dist/schema/Schema.js +2 -1
- package/lib/node_modules/yaml/dist/schema/common/map.d.ts +1 -1
- package/lib/node_modules/yaml/dist/schema/common/null.d.ts +1 -1
- package/lib/node_modules/yaml/dist/schema/common/seq.d.ts +1 -1
- package/lib/node_modules/yaml/dist/schema/common/string.d.ts +1 -1
- package/lib/node_modules/yaml/dist/schema/core/bool.d.ts +1 -1
- package/lib/node_modules/yaml/dist/schema/core/float.d.ts +1 -1
- package/lib/node_modules/yaml/dist/schema/core/int.d.ts +1 -1
- package/lib/node_modules/yaml/dist/schema/core/schema.d.ts +1 -1
- package/lib/node_modules/yaml/dist/schema/json/schema.d.ts +1 -1
- package/lib/node_modules/yaml/dist/schema/json/schema.js +1 -1
- package/lib/node_modules/yaml/dist/schema/tags.d.ts +3 -11
- package/lib/node_modules/yaml/dist/schema/tags.js +13 -26
- package/lib/node_modules/yaml/dist/schema/types.d.ts +12 -14
- package/lib/node_modules/yaml/dist/schema/yaml-1.1/binary.d.ts +1 -1
- package/lib/node_modules/yaml/dist/schema/yaml-1.1/binary.js +7 -9
- package/lib/node_modules/yaml/dist/schema/yaml-1.1/bool.d.ts +1 -1
- package/lib/node_modules/yaml/dist/schema/yaml-1.1/float.d.ts +1 -1
- package/lib/node_modules/yaml/dist/schema/yaml-1.1/int.d.ts +1 -1
- package/lib/node_modules/yaml/dist/schema/yaml-1.1/omap.d.ts +17 -11
- package/lib/node_modules/yaml/dist/schema/yaml-1.1/pairs.d.ts +7 -7
- package/lib/node_modules/yaml/dist/schema/yaml-1.1/schema.d.ts +1 -1
- package/lib/node_modules/yaml/dist/schema/yaml-1.1/schema.js +0 -2
- package/lib/node_modules/yaml/dist/schema/yaml-1.1/set.d.ts +9 -9
- package/lib/node_modules/yaml/dist/schema/yaml-1.1/timestamp.d.ts +1 -1
- package/lib/node_modules/yaml/dist/schema/yaml-1.1/timestamp.js +1 -1
- package/lib/node_modules/yaml/dist/stringify/stringify.d.ts +3 -3
- package/lib/node_modules/yaml/dist/stringify/stringify.js +5 -10
- package/lib/node_modules/yaml/dist/stringify/stringifyCollection.d.ts +2 -2
- package/lib/node_modules/yaml/dist/stringify/stringifyCollection.js +3 -13
- package/lib/node_modules/yaml/dist/stringify/stringifyDocument.d.ts +3 -3
- package/lib/node_modules/yaml/dist/stringify/stringifyNumber.d.ts +1 -1
- package/lib/node_modules/yaml/dist/stringify/stringifyNumber.js +2 -3
- package/lib/node_modules/yaml/dist/stringify/stringifyPair.d.ts +2 -2
- package/lib/node_modules/yaml/dist/stringify/stringifyPair.js +1 -1
- package/lib/node_modules/yaml/dist/stringify/stringifyString.d.ts +2 -2
- package/lib/node_modules/yaml/dist/stringify/stringifyString.js +15 -23
- package/lib/node_modules/yaml/dist/util.d.ts +12 -16
- package/lib/node_modules/yaml/dist/util.js +2 -2
- package/lib/node_modules/yaml/dist/visit.d.ts +7 -7
- package/lib/node_modules/yaml/package.json +14 -13
- package/lib/package.json +5 -6
- package/package.json +1 -1
- package/lib/node_modules/jsonc-parser/CHANGELOG.md +0 -76
- package/lib/node_modules/jsonc-parser/LICENSE.md +0 -21
- package/lib/node_modules/jsonc-parser/README.md +0 -364
- package/lib/node_modules/jsonc-parser/SECURITY.md +0 -41
- package/lib/node_modules/jsonc-parser/lib/esm/impl/edit.js +0 -185
- package/lib/node_modules/jsonc-parser/lib/esm/impl/format.js +0 -261
- package/lib/node_modules/jsonc-parser/lib/esm/impl/parser.js +0 -659
- package/lib/node_modules/jsonc-parser/lib/esm/impl/scanner.js +0 -443
- package/lib/node_modules/jsonc-parser/lib/esm/impl/string-intern.js +0 -29
- package/lib/node_modules/jsonc-parser/lib/esm/main.d.ts +0 -351
- package/lib/node_modules/jsonc-parser/lib/esm/main.js +0 -178
- package/lib/node_modules/jsonc-parser/lib/umd/impl/edit.js +0 -201
- package/lib/node_modules/jsonc-parser/lib/umd/impl/format.js +0 -275
- package/lib/node_modules/jsonc-parser/lib/umd/impl/parser.js +0 -682
- package/lib/node_modules/jsonc-parser/lib/umd/impl/scanner.js +0 -456
- package/lib/node_modules/jsonc-parser/lib/umd/impl/string-intern.js +0 -42
- package/lib/node_modules/jsonc-parser/lib/umd/main.d.ts +0 -351
- package/lib/node_modules/jsonc-parser/lib/umd/main.js +0 -194
- package/lib/node_modules/jsonc-parser/package.json +0 -37
- package/lib/node_modules/yaml/browser/dist/schema/yaml-1.1/merge.js +0 -67
- package/lib/node_modules/yaml/dist/schema/yaml-1.1/merge.d.ts +0 -9
- package/lib/node_modules/yaml/dist/schema/yaml-1.1/merge.js +0 -71
|
@@ -0,0 +1,787 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* JIRA MCP tools (REQ-JIRA-002, REQ-JIRA-003, REQ-JIRA-004).
|
|
4
|
+
*
|
|
5
|
+
* `specship_jira_issues` lists the issues assigned to the authenticated user
|
|
6
|
+
* (identity resolved from the token, never a typed name), optionally narrowed
|
|
7
|
+
* to a project. `specship_jira_issue` fetches a single issue by key with its
|
|
8
|
+
* full detail (description + subtasks). `specship_jira_pick` fetches an issue
|
|
9
|
+
* and authors a well-formed SpecShip spec from it under `specs/` (idempotent on
|
|
10
|
+
* the issue key). All read the user-level config, resolve credentials, and
|
|
11
|
+
* drive `JiraClient`.
|
|
12
|
+
*
|
|
13
|
+
* SECURITY (REQ-JIRA-009): the token is never surfaced. Every error path
|
|
14
|
+
* returns only the JiraError's own message — which by construction contains
|
|
15
|
+
* no credential — and the "not configured" path returns a plain pointer to
|
|
16
|
+
* `specship jira configure`, never a partial or fabricated result.
|
|
17
|
+
*/
|
|
18
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
19
|
+
if (k2 === undefined) k2 = k;
|
|
20
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
21
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
22
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
23
|
+
}
|
|
24
|
+
Object.defineProperty(o, k2, desc);
|
|
25
|
+
}) : (function(o, m, k, k2) {
|
|
26
|
+
if (k2 === undefined) k2 = k;
|
|
27
|
+
o[k2] = m[k];
|
|
28
|
+
}));
|
|
29
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
30
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
31
|
+
}) : function(o, v) {
|
|
32
|
+
o["default"] = v;
|
|
33
|
+
});
|
|
34
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
35
|
+
var ownKeys = function(o) {
|
|
36
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
37
|
+
var ar = [];
|
|
38
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
39
|
+
return ar;
|
|
40
|
+
};
|
|
41
|
+
return ownKeys(o);
|
|
42
|
+
};
|
|
43
|
+
return function (mod) {
|
|
44
|
+
if (mod && mod.__esModule) return mod;
|
|
45
|
+
var result = {};
|
|
46
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
47
|
+
__setModuleDefault(result, mod);
|
|
48
|
+
return result;
|
|
49
|
+
};
|
|
50
|
+
})();
|
|
51
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
52
|
+
exports.jiraToolDefinitions = void 0;
|
|
53
|
+
exports.handleSpecshipJiraIssues = handleSpecshipJiraIssues;
|
|
54
|
+
exports.handleSpecshipJiraIssue = handleSpecshipJiraIssue;
|
|
55
|
+
exports.handleSpecshipJiraPick = handleSpecshipJiraPick;
|
|
56
|
+
exports.handleJiraRunCompletion = handleJiraRunCompletion;
|
|
57
|
+
exports.handleSpecshipJiraStart = handleSpecshipJiraStart;
|
|
58
|
+
exports.deriveWorkState = deriveWorkState;
|
|
59
|
+
exports.handleSpecshipJiraTrack = handleSpecshipJiraTrack;
|
|
60
|
+
const fs = __importStar(require("fs"));
|
|
61
|
+
const path = __importStar(require("path"));
|
|
62
|
+
const crypto_1 = require("crypto");
|
|
63
|
+
const config_1 = require("../jira/config");
|
|
64
|
+
const client_1 = require("../jira/client");
|
|
65
|
+
const types_1 = require("../jira/types");
|
|
66
|
+
const spec_writer_1 = require("../jira/spec-writer");
|
|
67
|
+
const spec_generator_1 = require("../jira/spec-generator");
|
|
68
|
+
const pull_request_1 = require("../jira/pull-request");
|
|
69
|
+
function textResult(text) {
|
|
70
|
+
return { content: [{ type: 'text', text }] };
|
|
71
|
+
}
|
|
72
|
+
function errorResult(message) {
|
|
73
|
+
return { content: [{ type: 'text', text: message }], isError: true };
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Factory seam that resolves credentials and builds a live `JiraClient` for the
|
|
77
|
+
* status-push paths. The default resolves the user-level config; tests pass a
|
|
78
|
+
* `makeJiraClient` dep returning a fake so no host is contacted. Throws only if
|
|
79
|
+
* credentials are unresolvable — callers wrap it and degrade to a note, never a
|
|
80
|
+
* blocked workflow (REQ-JIRA-007, degrade-gracefully).
|
|
81
|
+
*/
|
|
82
|
+
function defaultMakeJiraClient() {
|
|
83
|
+
const creds = (0, config_1.resolveJiraCredentials)();
|
|
84
|
+
return {
|
|
85
|
+
client: new client_1.JiraClient(creds),
|
|
86
|
+
transitions: creds.transitions ?? {},
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Push the "start" status to JIRA (REQ-JIRA-007.A1): assign the issue to the
|
|
91
|
+
* authenticated user and transition it toward "in progress". Returns a short
|
|
92
|
+
* human note describing what happened (assigned, moved, or skipped) — or `null`
|
|
93
|
+
* when there's nothing to say. NEVER throws: a JIRA hiccup (auth, network, a
|
|
94
|
+
* missing transition) must not block the local workflow from starting — the
|
|
95
|
+
* note simply reports the skip (reinforcement 4).
|
|
96
|
+
*/
|
|
97
|
+
async function pushJiraStartStatus(key, make) {
|
|
98
|
+
let ctx;
|
|
99
|
+
try {
|
|
100
|
+
ctx = make();
|
|
101
|
+
}
|
|
102
|
+
catch (err) {
|
|
103
|
+
return `Note: couldn't push start status to JIRA — ${errMsg(err)}`;
|
|
104
|
+
}
|
|
105
|
+
const notes = [];
|
|
106
|
+
// Assign to the authenticated user (identity resolved from the token).
|
|
107
|
+
try {
|
|
108
|
+
const conn = await ctx.client.testConnection();
|
|
109
|
+
if (conn.accountId) {
|
|
110
|
+
await ctx.client.assignIssue(key, conn.accountId);
|
|
111
|
+
notes.push(`assigned ${key} to you`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
catch (err) {
|
|
115
|
+
notes.push(`couldn't assign ${key} (${errMsg(err)})`);
|
|
116
|
+
}
|
|
117
|
+
// Transition toward "in progress" — a missing transition is a skip, not a
|
|
118
|
+
// failure (A3), and never blocks the run.
|
|
119
|
+
try {
|
|
120
|
+
const target = ctx.transitions.inProgress ?? 'In Progress';
|
|
121
|
+
const res = await ctx.client.transitionIssue(key, target);
|
|
122
|
+
if ('transitioned' in res) {
|
|
123
|
+
notes.push(`moved ${key} to "${res.transitioned}"`);
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
notes.push(`didn't transition ${key} — ${res.reason}`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
catch (err) {
|
|
130
|
+
notes.push(`couldn't transition ${key} (${errMsg(err)})`);
|
|
131
|
+
}
|
|
132
|
+
return notes.length ? `JIRA: ${notes.join('; ')}.` : null;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Push the "PR raised" status to JIRA (REQ-JIRA-007.A2): transition the issue
|
|
136
|
+
* toward "in review" and comment the PR link on it. When the configured
|
|
137
|
+
* transition doesn't exist for this workflow, it still comments the PR link and
|
|
138
|
+
* reports the skip (A3) rather than erroring. Returns a human note. NEVER
|
|
139
|
+
* throws — a JIRA-side failure must not undo the raised PR.
|
|
140
|
+
*
|
|
141
|
+
* SECURITY: only the public PR URL + issue key are sent (REQ-JIRA-009).
|
|
142
|
+
*/
|
|
143
|
+
async function pushJiraReviewStatus(key, prUrl, make) {
|
|
144
|
+
let ctx;
|
|
145
|
+
try {
|
|
146
|
+
ctx = make();
|
|
147
|
+
}
|
|
148
|
+
catch (err) {
|
|
149
|
+
return `Note: couldn't push review status to JIRA — ${errMsg(err)}`;
|
|
150
|
+
}
|
|
151
|
+
const notes = [];
|
|
152
|
+
try {
|
|
153
|
+
const target = ctx.transitions.inReview ?? 'In Review';
|
|
154
|
+
const res = await ctx.client.transitionIssue(key, target);
|
|
155
|
+
if ('transitioned' in res) {
|
|
156
|
+
notes.push(`moved ${key} to "${res.transitioned}"`);
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
// A3: the transition is unavailable — still comment, report the skip.
|
|
160
|
+
notes.push(`didn't transition ${key} — ${res.reason}`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
catch (err) {
|
|
164
|
+
notes.push(`couldn't transition ${key} (${errMsg(err)})`);
|
|
165
|
+
}
|
|
166
|
+
// The PR-link comment is posted regardless of the transition outcome (A3).
|
|
167
|
+
try {
|
|
168
|
+
await ctx.client.addComment(key, `SpecShip raised a pull request: ${prUrl}`);
|
|
169
|
+
notes.push(`commented the PR link on ${key}`);
|
|
170
|
+
}
|
|
171
|
+
catch (err) {
|
|
172
|
+
notes.push(`couldn't comment the PR link on ${key} (${errMsg(err)})`);
|
|
173
|
+
}
|
|
174
|
+
return notes.length ? `JIRA: ${notes.join('; ')}.` : null;
|
|
175
|
+
}
|
|
176
|
+
/** Extract a credential-free message from an unknown error (REQ-JIRA-009). */
|
|
177
|
+
function errMsg(err) {
|
|
178
|
+
return err instanceof Error ? err.message : String(err);
|
|
179
|
+
}
|
|
180
|
+
exports.jiraToolDefinitions = [
|
|
181
|
+
{
|
|
182
|
+
name: 'specship_jira_issues',
|
|
183
|
+
description: 'List the JIRA issues assigned to you (resolved from your configured ' +
|
|
184
|
+
'token — you never type your own name). Optionally narrow to a project.',
|
|
185
|
+
inputSchema: {
|
|
186
|
+
type: 'object',
|
|
187
|
+
properties: {
|
|
188
|
+
project: {
|
|
189
|
+
type: 'string',
|
|
190
|
+
description: 'Optional project key to narrow the list (e.g., "PROJ"). Omit to list all your assigned issues.',
|
|
191
|
+
},
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
name: 'specship_jira_issue',
|
|
197
|
+
description: 'Fetch a single JIRA issue by key (e.g., "PROJ-123") with its full ' +
|
|
198
|
+
'detail — summary, status, type, description, and any subtasks.',
|
|
199
|
+
inputSchema: {
|
|
200
|
+
type: 'object',
|
|
201
|
+
properties: {
|
|
202
|
+
key: {
|
|
203
|
+
type: 'string',
|
|
204
|
+
description: 'The issue key to fetch, e.g., "PROJ-123". Required.',
|
|
205
|
+
},
|
|
206
|
+
},
|
|
207
|
+
required: ['key'],
|
|
208
|
+
},
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
name: 'specship_jira_pick',
|
|
212
|
+
description: 'Pick a JIRA issue by key (e.g., "PROJ-123") and author a SpecShip spec ' +
|
|
213
|
+
'from it under specs/ — summary becomes the title, description the ' +
|
|
214
|
+
'requirement body, subtasks the acceptance criteria. Re-picking the same ' +
|
|
215
|
+
'issue updates its spec in place rather than creating a duplicate.',
|
|
216
|
+
inputSchema: {
|
|
217
|
+
type: 'object',
|
|
218
|
+
properties: {
|
|
219
|
+
key: {
|
|
220
|
+
type: 'string',
|
|
221
|
+
description: 'The issue key to pick, e.g., "PROJ-123". Required.',
|
|
222
|
+
},
|
|
223
|
+
},
|
|
224
|
+
required: ['key'],
|
|
225
|
+
},
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
name: 'specship_jira_start',
|
|
229
|
+
description: 'Start implementing a picked JIRA issue: run the bundled spec-implement ' +
|
|
230
|
+
'workflow on the spec that specship_jira_pick authored for the key, in an ' +
|
|
231
|
+
'isolated worktree. Runs to the plan/approve gate and stops there — review ' +
|
|
232
|
+
'the plan, then approve to proceed. Pick the issue first if no spec exists.',
|
|
233
|
+
inputSchema: {
|
|
234
|
+
type: 'object',
|
|
235
|
+
properties: {
|
|
236
|
+
key: {
|
|
237
|
+
type: 'string',
|
|
238
|
+
description: 'The issue key to start, e.g., "PROJ-123". Required. A spec for it ' +
|
|
239
|
+
'must already exist (run specship_jira_pick first).',
|
|
240
|
+
},
|
|
241
|
+
},
|
|
242
|
+
required: ['key'],
|
|
243
|
+
},
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
name: 'specship_jira_track',
|
|
247
|
+
description: 'Show a read-only tracking view of every JIRA issue you have brought into ' +
|
|
248
|
+
"SpecShip: each row joins the issue's SpecShip work-state (spec authored → " +
|
|
249
|
+
'implementing → PR raised → verified) with its LIVE JIRA status (a fresh ' +
|
|
250
|
+
'read at track time, so an issue moved outside SpecShip reflects its current ' +
|
|
251
|
+
'status). Read-only — it never re-picks or re-starts anything. Optionally ' +
|
|
252
|
+
'narrow the JIRA read to a project.',
|
|
253
|
+
inputSchema: {
|
|
254
|
+
type: 'object',
|
|
255
|
+
properties: {
|
|
256
|
+
project: {
|
|
257
|
+
type: 'string',
|
|
258
|
+
description: 'Optional project key to narrow the live JIRA read (e.g., "PROJ"). Omit to read all your assigned issues.',
|
|
259
|
+
},
|
|
260
|
+
},
|
|
261
|
+
},
|
|
262
|
+
},
|
|
263
|
+
];
|
|
264
|
+
/** Render the issue list as compact markdown, or an explicit empty line. */
|
|
265
|
+
function formatIssues(issues) {
|
|
266
|
+
if (issues.length === 0) {
|
|
267
|
+
return 'No issues assigned to you.';
|
|
268
|
+
}
|
|
269
|
+
const lines = issues.map(i => `- **${i.key}** — ${i.summary} — ${i.status} — ${i.issueType}`);
|
|
270
|
+
return `Issues assigned to you (${issues.length}):\n\n${lines.join('\n')}`;
|
|
271
|
+
}
|
|
272
|
+
/** Render a single issue's full detail as markdown. */
|
|
273
|
+
function formatIssue(issue) {
|
|
274
|
+
const parts = [
|
|
275
|
+
`## ${issue.key} — ${issue.summary}`,
|
|
276
|
+
'',
|
|
277
|
+
`- **Status:** ${issue.status}`,
|
|
278
|
+
`- **Type:** ${issue.issueType}`,
|
|
279
|
+
];
|
|
280
|
+
const description = issue.description?.trim();
|
|
281
|
+
parts.push('', '### Description', '', description || '_No description._');
|
|
282
|
+
if (issue.subtasks && issue.subtasks.length > 0) {
|
|
283
|
+
parts.push('', `### Subtasks (${issue.subtasks.length})`, '');
|
|
284
|
+
for (const st of issue.subtasks) {
|
|
285
|
+
parts.push(`- **${st.key}** — ${st.summary} — ${st.status}`);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
return parts.join('\n');
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* The shared "not configured → clear pointer" gate for the JIRA tools. Returns
|
|
292
|
+
* a pointer `ToolResult` when neither a config file nor env credentials are
|
|
293
|
+
* present, or `null` when configured (the caller proceeds). Never an error
|
|
294
|
+
* stack or a fabricated result.
|
|
295
|
+
*/
|
|
296
|
+
function notConfiguredResult() {
|
|
297
|
+
let hasConfig = false;
|
|
298
|
+
try {
|
|
299
|
+
hasConfig = (0, config_1.loadJiraConfig)() !== null;
|
|
300
|
+
}
|
|
301
|
+
catch {
|
|
302
|
+
// A malformed config file still means "configured but broken" — fall
|
|
303
|
+
// through to resolveJiraCredentials, which surfaces the actionable message.
|
|
304
|
+
hasConfig = true;
|
|
305
|
+
}
|
|
306
|
+
const hasEnv = Boolean(process.env.SPECSHIP_JIRA_BASE_URL);
|
|
307
|
+
if (!hasConfig && !hasEnv) {
|
|
308
|
+
return textResult('JIRA is not configured. Run "specship jira configure" to connect a ' +
|
|
309
|
+
'JIRA Cloud or Data Center instance, then try again.');
|
|
310
|
+
}
|
|
311
|
+
return null;
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Handle `specship_jira_issues`. Independent of the code graph — talks only
|
|
315
|
+
* to the configured JIRA host through the stored credentials.
|
|
316
|
+
*/
|
|
317
|
+
async function handleSpecshipJiraIssues(args) {
|
|
318
|
+
// Not configured → a clear pointer, never an error stack or a fabricated list.
|
|
319
|
+
const notConfigured = notConfiguredResult();
|
|
320
|
+
if (notConfigured)
|
|
321
|
+
return notConfigured;
|
|
322
|
+
const project = typeof args.project === 'string' && args.project.trim()
|
|
323
|
+
? args.project.trim()
|
|
324
|
+
: undefined;
|
|
325
|
+
try {
|
|
326
|
+
const creds = (0, config_1.resolveJiraCredentials)();
|
|
327
|
+
const client = new client_1.JiraClient(creds);
|
|
328
|
+
const result = await client.listMyIssues({ project });
|
|
329
|
+
return textResult(formatIssues(result.issues));
|
|
330
|
+
}
|
|
331
|
+
catch (err) {
|
|
332
|
+
// JiraError messages are credential-free by construction (REQ-JIRA-009).
|
|
333
|
+
if (err instanceof types_1.JiraError) {
|
|
334
|
+
return errorResult(err.message);
|
|
335
|
+
}
|
|
336
|
+
// Defensive: never let an unexpected error leak internals.
|
|
337
|
+
return errorResult('Failed to list JIRA issues.');
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* Handle `specship_jira_issue` (REQ-JIRA-003). Fetches a single issue by key
|
|
342
|
+
* and renders its full detail. Independent of the code graph — talks only to
|
|
343
|
+
* the configured JIRA host. A missing/no-access key surfaces as an explicit
|
|
344
|
+
* tool error (A2), never a silent empty result.
|
|
345
|
+
*/
|
|
346
|
+
async function handleSpecshipJiraIssue(args) {
|
|
347
|
+
// Not configured → a clear pointer, never an error stack or a fabricated issue.
|
|
348
|
+
const notConfigured = notConfiguredResult();
|
|
349
|
+
if (notConfigured)
|
|
350
|
+
return notConfigured;
|
|
351
|
+
// A missing/blank key is a user mistake, not an internal fault — a clear
|
|
352
|
+
// message, not a stack.
|
|
353
|
+
const key = typeof args.key === 'string' && args.key.trim() ? args.key.trim() : undefined;
|
|
354
|
+
if (!key) {
|
|
355
|
+
return textResult('An issue key is required (e.g., "PROJ-123"). Pass it as the "key" argument.');
|
|
356
|
+
}
|
|
357
|
+
try {
|
|
358
|
+
const creds = (0, config_1.resolveJiraCredentials)();
|
|
359
|
+
const client = new client_1.JiraClient(creds);
|
|
360
|
+
const result = await client.getIssue(key);
|
|
361
|
+
return textResult(formatIssue(result.issue));
|
|
362
|
+
}
|
|
363
|
+
catch (err) {
|
|
364
|
+
// JiraError messages (incl. JiraNotFoundError for a missing/no-access key)
|
|
365
|
+
// are credential-free by construction (REQ-JIRA-009).
|
|
366
|
+
if (err instanceof types_1.JiraError) {
|
|
367
|
+
return errorResult(err.message);
|
|
368
|
+
}
|
|
369
|
+
// Defensive: never let an unexpected error leak internals.
|
|
370
|
+
return errorResult('Failed to fetch the JIRA issue.');
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* Handle `specship_jira_pick` (REQ-JIRA-004). Fetches a single issue by key
|
|
375
|
+
* (reusing the REQ-JIRA-003 path) and authors a well-formed SpecShip spec from
|
|
376
|
+
* it under `specs/`, idempotent on the issue key (A3). Independent of the code
|
|
377
|
+
* graph — talks only to the configured JIRA host. A missing/no-access key
|
|
378
|
+
* surfaces as a `JiraNotFoundError` → tool error, never a written empty spec.
|
|
379
|
+
*
|
|
380
|
+
* SECURITY: the issue summary/description/subtasks are untrusted content; the
|
|
381
|
+
* generator neutralizes any injected spec structure before embedding. No token
|
|
382
|
+
* appears in any output or error (REQ-JIRA-009).
|
|
383
|
+
*/
|
|
384
|
+
async function handleSpecshipJiraPick(args) {
|
|
385
|
+
// Not configured → a clear pointer, never an error stack or a written spec.
|
|
386
|
+
const notConfigured = notConfiguredResult();
|
|
387
|
+
if (notConfigured)
|
|
388
|
+
return notConfigured;
|
|
389
|
+
// A missing/blank key is a user mistake, not an internal fault.
|
|
390
|
+
const key = typeof args.key === 'string' && args.key.trim() ? args.key.trim() : undefined;
|
|
391
|
+
if (!key) {
|
|
392
|
+
return textResult('An issue key is required (e.g., "PROJ-123"). Pass it as the "key" argument.');
|
|
393
|
+
}
|
|
394
|
+
// The project whose specs/ directory receives the spec. Threaded through the
|
|
395
|
+
// handler args like the other tools' context, falling back to the cwd.
|
|
396
|
+
const projectRoot = typeof args.projectPath === 'string' && args.projectPath.trim()
|
|
397
|
+
? args.projectPath.trim()
|
|
398
|
+
: process.cwd();
|
|
399
|
+
try {
|
|
400
|
+
const creds = (0, config_1.resolveJiraCredentials)();
|
|
401
|
+
const client = new client_1.JiraClient(creds);
|
|
402
|
+
const result = await client.getIssue(key);
|
|
403
|
+
const written = (0, spec_writer_1.writeSpecFromIssue)(result.issue, projectRoot);
|
|
404
|
+
const verb = written.created ? 'Created' : 'Updated';
|
|
405
|
+
return textResult(`${verb} spec for ${result.issue.key} at ${written.path}. ` +
|
|
406
|
+
'Run "specship sync" (or let the watcher pick it up) to index it, then ' +
|
|
407
|
+
`run specship_jira_start for ${result.issue.key} to implement it.`);
|
|
408
|
+
}
|
|
409
|
+
catch (err) {
|
|
410
|
+
// JiraError messages (incl. JiraNotFoundError for a missing/no-access key)
|
|
411
|
+
// are credential-free by construction (REQ-JIRA-009).
|
|
412
|
+
if (err instanceof types_1.JiraError) {
|
|
413
|
+
return errorResult(err.message);
|
|
414
|
+
}
|
|
415
|
+
// Defensive: never let an unexpected error leak internals.
|
|
416
|
+
return errorResult('Failed to pick the JIRA issue.');
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
/** Read `metadata.approval.message` off a run, tolerating any metadata shape. */
|
|
420
|
+
function approvalMessageOf(run) {
|
|
421
|
+
const approval = run.metadata
|
|
422
|
+
?.approval;
|
|
423
|
+
return typeof approval?.message === 'string' ? approval.message : undefined;
|
|
424
|
+
}
|
|
425
|
+
/**
|
|
426
|
+
* Re-read the persisted run so we see the settled status + approval metadata.
|
|
427
|
+
* `executor.start` returns the pre-finalize in-memory run whose status still
|
|
428
|
+
* reads "running" even after it paused at the approval gate; the DB row carries
|
|
429
|
+
* the real "paused" status + the approval message. Falls back to the returned
|
|
430
|
+
* run when the handle can't look it up (e.g. a stubbed executor in tests).
|
|
431
|
+
*/
|
|
432
|
+
function reloadRun(specQueries, run) {
|
|
433
|
+
const getById = specQueries?.getWorkflowRunById;
|
|
434
|
+
if (typeof getById !== 'function')
|
|
435
|
+
return run;
|
|
436
|
+
try {
|
|
437
|
+
return getById.call(specQueries, run.id) ?? run;
|
|
438
|
+
}
|
|
439
|
+
catch {
|
|
440
|
+
return run;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
/** Read the H1 title out of a spec file, falling back to `fallback`. */
|
|
444
|
+
function readSpecTitle(specPath, fallback) {
|
|
445
|
+
if (!specPath)
|
|
446
|
+
return fallback;
|
|
447
|
+
try {
|
|
448
|
+
const content = fs.readFileSync(specPath, 'utf8');
|
|
449
|
+
for (const raw of content.split(/\r?\n/)) {
|
|
450
|
+
const line = raw.trim();
|
|
451
|
+
const m = line.match(/^#\s+(.+)$/);
|
|
452
|
+
if (m)
|
|
453
|
+
return m[1].trim() || fallback;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
catch {
|
|
457
|
+
/* fall through to the fallback */
|
|
458
|
+
}
|
|
459
|
+
return fallback;
|
|
460
|
+
}
|
|
461
|
+
/** Recover the `jira` metadata block off a run, tolerating any metadata shape. */
|
|
462
|
+
function jiraMetaOf(run) {
|
|
463
|
+
const jira = run.metadata?.jira;
|
|
464
|
+
return jira && typeof jira === 'object' ? jira : undefined;
|
|
465
|
+
}
|
|
466
|
+
/**
|
|
467
|
+
* True when the run's verify leg RAN AND PASSED — the only state that raises a
|
|
468
|
+
* PR (REQ-JIRA-006.A1; aligns with REQ-JIRA-005.A2). A verify that FAILED halts
|
|
469
|
+
* the run before completion (status `failed`), and a SKIPPED verify
|
|
470
|
+
* (`VERIFY_RESULT=skipped`) completes the run but must NOT raise a PR — tests
|
|
471
|
+
* that did not run are never treated as a pass.
|
|
472
|
+
*/
|
|
473
|
+
function verifyPassed(run) {
|
|
474
|
+
const meta = run.metadata ?? {};
|
|
475
|
+
const states = meta.nodeStates;
|
|
476
|
+
if (states && states.verify && states.verify !== 'completed')
|
|
477
|
+
return false;
|
|
478
|
+
const outputs = meta.outputs;
|
|
479
|
+
const verifyText = outputs?.verify?.text ?? '';
|
|
480
|
+
return /VERIFY_RESULT=ran-and-passed/.test(verifyText);
|
|
481
|
+
}
|
|
482
|
+
/**
|
|
483
|
+
* Completion hook for a JIRA-started run (REQ-JIRA-006). Fires only once the
|
|
484
|
+
* run has settled to `completed`. Raises a PR — traceable to the issue via the
|
|
485
|
+
* key in the branch name, PR title, AND body — ONLY when the run's verify leg
|
|
486
|
+
* ran and passed. On any PR-tooling failure it surfaces the reason and leaves
|
|
487
|
+
* the branch + worktree fully intact for a manual PR (A3); it NEVER auto-merges
|
|
488
|
+
* or closes the PR (the human owns "done"). A non-JIRA run is a silent no-op.
|
|
489
|
+
*
|
|
490
|
+
* SECURITY: no JIRA token/secret appears in the PR title/body/logs — only the
|
|
491
|
+
* public issue key (REQ-JIRA-009).
|
|
492
|
+
*/
|
|
493
|
+
async function handleJiraRunCompletion(run, deps) {
|
|
494
|
+
const jira = jiraMetaOf(run);
|
|
495
|
+
const issueKey = jira?.issueKey;
|
|
496
|
+
if (!issueKey)
|
|
497
|
+
return null; // not a JIRA-started run — nothing to do.
|
|
498
|
+
const log = deps.log ?? ((m) => {
|
|
499
|
+
// eslint-disable-next-line no-console
|
|
500
|
+
console.log(m);
|
|
501
|
+
});
|
|
502
|
+
// A1: a completed-but-unverified run (verify skipped) raises NO PR.
|
|
503
|
+
if (!verifyPassed(run)) {
|
|
504
|
+
log(`Run for ${issueKey} completed but its verification did not pass — no ` +
|
|
505
|
+
'pull request was raised.');
|
|
506
|
+
return null;
|
|
507
|
+
}
|
|
508
|
+
if (!run.isolationEnvId) {
|
|
509
|
+
log(`Run for ${issueKey} completed but has no worktree on record — no pull ` +
|
|
510
|
+
'request was raised.');
|
|
511
|
+
return null;
|
|
512
|
+
}
|
|
513
|
+
const env = deps.getIsolationEnvById(run.isolationEnvId);
|
|
514
|
+
if (!env) {
|
|
515
|
+
log(`Run for ${issueKey} completed but its worktree could not be resolved — ` +
|
|
516
|
+
'no pull request was raised.');
|
|
517
|
+
return null;
|
|
518
|
+
}
|
|
519
|
+
const meta = env.metadata ?? {};
|
|
520
|
+
const repoRoot = (typeof meta.repoRoot === 'string' ? meta.repoRoot : undefined) ??
|
|
521
|
+
deps.projectRoot ??
|
|
522
|
+
process.cwd();
|
|
523
|
+
const specTitle = jira.title ?? jira.specId ?? issueKey;
|
|
524
|
+
const raise = deps.raisePullRequest ?? pull_request_1.raisePullRequest;
|
|
525
|
+
const outcome = await raise({
|
|
526
|
+
repoRoot,
|
|
527
|
+
branchName: env.branchName,
|
|
528
|
+
worktreePath: env.workingPath,
|
|
529
|
+
issueKey,
|
|
530
|
+
title: (0, pull_request_1.buildPrTitle)(issueKey, specTitle),
|
|
531
|
+
body: (0, pull_request_1.buildPrBody)(issueKey, specTitle),
|
|
532
|
+
});
|
|
533
|
+
if (outcome.ok) {
|
|
534
|
+
log(`Raised pull request for ${issueKey}: ${outcome.url}`);
|
|
535
|
+
// A2: PR raised → transition toward "in review" and comment the PR link.
|
|
536
|
+
// Only on a raised PR — a failed raise never advances the issue (below),
|
|
537
|
+
// matching REQ-JIRA-005.A2 (leave it in-progress). Never throws.
|
|
538
|
+
const make = deps.makeJiraClient ?? defaultMakeJiraClient;
|
|
539
|
+
const note = await pushJiraReviewStatus(issueKey, outcome.url, make);
|
|
540
|
+
if (note)
|
|
541
|
+
log(note);
|
|
542
|
+
}
|
|
543
|
+
else {
|
|
544
|
+
// A3 (of REQ-JIRA-006) / A2 (of REQ-JIRA-005): report the reason; the
|
|
545
|
+
// branch + worktree are left intact upstream and the issue is NOT
|
|
546
|
+
// transitioned (it stays in-progress).
|
|
547
|
+
log(outcome.message);
|
|
548
|
+
}
|
|
549
|
+
return outcome;
|
|
550
|
+
}
|
|
551
|
+
/**
|
|
552
|
+
* Handle `specship_jira_start` (REQ-JIRA-005). Drives the bundled
|
|
553
|
+
* `spec-implement` workflow on the spec `specship_jira_pick` authored for the
|
|
554
|
+
* issue key, in an isolated worktree, and runs it to the first pause — the
|
|
555
|
+
* plan/approve gate (A1). It does NOT block for the full implementation.
|
|
556
|
+
*
|
|
557
|
+
* On the non-failed path it pushes the "start" status back to JIRA
|
|
558
|
+
* (REQ-JIRA-007.A1): assigns the issue and transitions it toward "in progress".
|
|
559
|
+
* That push NEVER blocks the workflow — a JIRA hiccup (auth, network, a missing
|
|
560
|
+
* transition) is surfaced as a note, not an error. A failed/rejected run
|
|
561
|
+
* (REQ-JIRA-005.A2) raises no PR and is never advanced past "in progress"; the
|
|
562
|
+
* "in review" transition + PR comment (REQ-JIRA-007.A2) fire only once the run
|
|
563
|
+
* reaches `completed` with a verified, raised PR.
|
|
564
|
+
*
|
|
565
|
+
* SECURITY: the credential is handled only inside `JiraClient` and never
|
|
566
|
+
* echoed (REQ-JIRA-009); only the public issue key / PR URL are surfaced.
|
|
567
|
+
*/
|
|
568
|
+
async function handleSpecshipJiraStart(args, deps) {
|
|
569
|
+
// Not configured → a clear pointer, never an error stack or a started run.
|
|
570
|
+
const notConfigured = notConfiguredResult();
|
|
571
|
+
if (notConfigured)
|
|
572
|
+
return notConfigured;
|
|
573
|
+
// A missing/blank key is a user mistake, not an internal fault.
|
|
574
|
+
const key = typeof args.key === 'string' && args.key.trim() ? args.key.trim() : undefined;
|
|
575
|
+
if (!key) {
|
|
576
|
+
return textResult('An issue key is required (e.g., "PROJ-123"). Pass it as the "key" argument.');
|
|
577
|
+
}
|
|
578
|
+
const projectRoot = deps.projectRoot;
|
|
579
|
+
const specId = (0, spec_generator_1.reqIdForIssue)(key);
|
|
580
|
+
// The spec must already exist — start never silently authors one. If it's
|
|
581
|
+
// absent, point the caller at specship_jira_pick (which writes the spec keyed
|
|
582
|
+
// on this exact issue key, so start then targets the same SPEC_ID).
|
|
583
|
+
const specPath = (0, spec_writer_1.findSpecForIssueKey)(key, projectRoot);
|
|
584
|
+
if (!specPath) {
|
|
585
|
+
return textResult(`No spec for ${key} was found under specs/. Run specship_jira_pick for ` +
|
|
586
|
+
`${key} first to author it, then run specship_jira_start again.`);
|
|
587
|
+
}
|
|
588
|
+
const specTitle = readSpecTitle(specPath, specId);
|
|
589
|
+
try {
|
|
590
|
+
const loadWorkflowByName = deps.loadWorkflowByName ??
|
|
591
|
+
(await Promise.resolve().then(() => __importStar(require('../workflows/discovery')))).loadWorkflowByName;
|
|
592
|
+
const WorktreeProvider = deps.WorktreeProvider ??
|
|
593
|
+
(await Promise.resolve().then(() => __importStar(require('../isolation/worktree')))).WorktreeProvider;
|
|
594
|
+
const WorkflowExecutor = deps.WorkflowExecutor ??
|
|
595
|
+
(await Promise.resolve().then(() => __importStar(require('../workflows/executor')))).WorkflowExecutor;
|
|
596
|
+
const getSpecShipDir = deps.getSpecShipDir ?? (await Promise.resolve().then(() => __importStar(require('../directory')))).getSpecShipDir;
|
|
597
|
+
const loaded = loadWorkflowByName(projectRoot, 'spec-implement');
|
|
598
|
+
if (!loaded) {
|
|
599
|
+
return errorResult('The bundled "spec-implement" workflow was not found. Reinstall ' +
|
|
600
|
+
'SpecShip or check `specship workflow list`.');
|
|
601
|
+
}
|
|
602
|
+
// Carry the issue key in the branch name too (specship/<key>-<shortRunId>)
|
|
603
|
+
// so JIRA's dev panel matches on branch + PR title + PR body (REQ-JIRA-006).
|
|
604
|
+
const runId = (0, crypto_1.randomUUID)();
|
|
605
|
+
const shortRunId = runId.substring(0, 8);
|
|
606
|
+
const branchSlug = key.replace(/[^A-Za-z0-9._/-]/g, '-').replace(/-+/g, '-');
|
|
607
|
+
const branchName = `specship/${branchSlug}-${shortRunId}`;
|
|
608
|
+
// Completion deps: the PR is raised only when the run later resumes to
|
|
609
|
+
// `completed` with a passing verify. Non-JIRA runs no-op this hook.
|
|
610
|
+
const completionDeps = {
|
|
611
|
+
getIsolationEnvById: deps.getIsolationEnvById ??
|
|
612
|
+
((id) => deps.specQueries.getIsolationEnvById?.(id)),
|
|
613
|
+
raisePullRequest: deps.raisePullRequest,
|
|
614
|
+
projectRoot,
|
|
615
|
+
makeJiraClient: deps.makeJiraClient,
|
|
616
|
+
};
|
|
617
|
+
const worktrees = new WorktreeProvider(deps.specQueries);
|
|
618
|
+
const executor = new WorkflowExecutor(deps.specQueries, worktrees, projectRoot, async (completedRun) => {
|
|
619
|
+
await handleJiraRunCompletion(completedRun, completionDeps);
|
|
620
|
+
});
|
|
621
|
+
const started = await executor.start(loaded.workflow, {
|
|
622
|
+
runId,
|
|
623
|
+
branchName,
|
|
624
|
+
projectRoot,
|
|
625
|
+
inputs: { SPEC_ID: specId },
|
|
626
|
+
variables: {
|
|
627
|
+
ARTIFACTS_DIR: path.join(getSpecShipDir(projectRoot), 'artifacts'),
|
|
628
|
+
CONTEXT: projectRoot,
|
|
629
|
+
},
|
|
630
|
+
runMetadata: { jira: { issueKey: key, specId, title: specTitle } },
|
|
631
|
+
});
|
|
632
|
+
const run = reloadRun(deps.specQueries, started.run);
|
|
633
|
+
if (run.status === 'failed') {
|
|
634
|
+
// A2: a failed run raises no PR and never advances the issue past
|
|
635
|
+
// "in progress" — the issue is not transitioned by this handler.
|
|
636
|
+
const why = run.errorMessage ? ` ${run.errorMessage}` : '';
|
|
637
|
+
return errorResult(`Implementation run for ${key} (${specId}) failed and no pull request ` +
|
|
638
|
+
`was raised.${why}`);
|
|
639
|
+
}
|
|
640
|
+
// A1 (REQ-JIRA-007): the run started, so push the "start" status —
|
|
641
|
+
// assign the issue + transition it toward "in progress". This runs only on
|
|
642
|
+
// the non-failed path (the run reached the gate); a JIRA hiccup here NEVER
|
|
643
|
+
// blocks the workflow — pushJiraStartStatus never throws and its note is
|
|
644
|
+
// surfaced in the returned text (reinforcement 4).
|
|
645
|
+
const startNote = await pushJiraStartStatus(key, deps.makeJiraClient ?? defaultMakeJiraClient);
|
|
646
|
+
// A1: the run reached the plan/approve gate and paused. Surface the
|
|
647
|
+
// approval message + runId; the caller approves (or rejects) to continue.
|
|
648
|
+
const gate = approvalMessageOf(run);
|
|
649
|
+
return textResult(`Started spec-implement for ${key} (${specId}). Run ${run.id} is paused ` +
|
|
650
|
+
`at the plan/approve gate — review the plan and approve to proceed.` +
|
|
651
|
+
(startNote ? `\n\n${startNote}` : '') +
|
|
652
|
+
(gate ? `\n\n${gate}` : '') +
|
|
653
|
+
`\n\nApprove with: specship workflow approve ${run.id}` +
|
|
654
|
+
`\nOr reject with feedback: specship workflow reject ${run.id} --comment "…"` +
|
|
655
|
+
`\n\nOnce approved and the implementation completes and verifies, a pull ` +
|
|
656
|
+
`request for ${key} is raised automatically (its key is carried on the ` +
|
|
657
|
+
`branch, PR title, and body so JIRA links it back).`);
|
|
658
|
+
}
|
|
659
|
+
catch (err) {
|
|
660
|
+
// Defensive: never let an unexpected error leak internals.
|
|
661
|
+
return errorResult(`Failed to start implementation for ${key}: ${err instanceof Error ? err.message : String(err)}`);
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
/**
|
|
665
|
+
* Map a workflow run to its SpecShip lifecycle work-state (REQ-JIRA-008). Pure
|
|
666
|
+
* — reads only the run's status + metadata, most-advanced stage first:
|
|
667
|
+
* - verify ran and passed → `verified` (the terminal success),
|
|
668
|
+
* - a raised-PR marker on the run → `PR raised`,
|
|
669
|
+
* - running/paused → `implementing`,
|
|
670
|
+
* - anything else (pending/failed/cancelled/completed-without-markers) → the
|
|
671
|
+
* spec exists but no implementation has landed → `spec authored`.
|
|
672
|
+
*/
|
|
673
|
+
function deriveWorkState(run) {
|
|
674
|
+
if (verifyPassed(run))
|
|
675
|
+
return 'verified';
|
|
676
|
+
const jira = jiraMetaOf(run);
|
|
677
|
+
if (jira && typeof jira.prUrl === 'string' && jira.prUrl.trim()) {
|
|
678
|
+
return 'PR raised';
|
|
679
|
+
}
|
|
680
|
+
const status = run.status;
|
|
681
|
+
if (status === 'running' || status === 'paused')
|
|
682
|
+
return 'implementing';
|
|
683
|
+
return 'spec authored';
|
|
684
|
+
}
|
|
685
|
+
/** Factory seam that builds a live track client from the resolved credentials. */
|
|
686
|
+
function defaultMakeTrackClient() {
|
|
687
|
+
const creds = (0, config_1.resolveJiraCredentials)();
|
|
688
|
+
return new client_1.JiraClient(creds);
|
|
689
|
+
}
|
|
690
|
+
const JIRA_UNREACHABLE = '— (JIRA unreachable)';
|
|
691
|
+
/** Render the tracking rows as a compact markdown table (REQ-JIRA-008). */
|
|
692
|
+
function formatTrack(rows) {
|
|
693
|
+
if (rows.length === 0) {
|
|
694
|
+
return 'No picked issues yet — run specship_jira_pick first.';
|
|
695
|
+
}
|
|
696
|
+
const lines = [
|
|
697
|
+
'| Issue | SpecShip | JIRA |',
|
|
698
|
+
'| --- | --- | --- |',
|
|
699
|
+
...rows.map((r) => `| **${r.issueKey}** — ${r.title} | ${r.workState} | ${r.jiraStatus} |`),
|
|
700
|
+
];
|
|
701
|
+
return `Tracking ${rows.length} picked issue${rows.length === 1 ? '' : 's'}:\n\n${lines.join('\n')}`;
|
|
702
|
+
}
|
|
703
|
+
/**
|
|
704
|
+
* Handle `specship_jira_track` (REQ-JIRA-008). A read-only view joining each
|
|
705
|
+
* picked issue's SpecShip work-state (from its workflow run) with its LIVE JIRA
|
|
706
|
+
* status (a fresh read at track time — never the pick-time cached metadata, so
|
|
707
|
+
* an issue moved outside SpecShip shows its current status). It never re-picks
|
|
708
|
+
* or re-starts anything.
|
|
709
|
+
*
|
|
710
|
+
* Runs are enumerated newest-first and deduped by issue key keeping the
|
|
711
|
+
* most-recent run. A JIRA read failure degrades PER ROW to a clear "unreachable"
|
|
712
|
+
* marker and never fails the whole view (degrade-gracefully, matching the
|
|
713
|
+
* sibling status tools).
|
|
714
|
+
*
|
|
715
|
+
* SECURITY: no token appears in any output or error (REQ-JIRA-009) — only public
|
|
716
|
+
* issue keys, titles, and statuses.
|
|
717
|
+
*/
|
|
718
|
+
async function handleSpecshipJiraTrack(args, deps) {
|
|
719
|
+
// Not configured → a clear pointer, never an error stack or a fabricated view.
|
|
720
|
+
const notConfigured = notConfiguredResult();
|
|
721
|
+
if (notConfigured)
|
|
722
|
+
return notConfigured;
|
|
723
|
+
const project = typeof args.project === 'string' && args.project.trim()
|
|
724
|
+
? args.project.trim()
|
|
725
|
+
: undefined;
|
|
726
|
+
// Enumerate the runs that record which issues entered the SpecShip pipeline,
|
|
727
|
+
// keeping only JIRA-started runs and deduping by key (newest-first, so the
|
|
728
|
+
// first occurrence is the most-recent run — reinforcement 3).
|
|
729
|
+
const limit = typeof deps.limit === 'number' && deps.limit > 0 ? deps.limit : 200;
|
|
730
|
+
const getAll = deps.specQueries.getAllWorkflowRuns;
|
|
731
|
+
const runs = typeof getAll === 'function' ? getAll.call(deps.specQueries, limit) ?? [] : [];
|
|
732
|
+
const picked = [];
|
|
733
|
+
const seen = new Set();
|
|
734
|
+
for (const run of runs) {
|
|
735
|
+
const jira = jiraMetaOf(run);
|
|
736
|
+
const issueKey = jira?.issueKey;
|
|
737
|
+
if (!issueKey || seen.has(issueKey))
|
|
738
|
+
continue;
|
|
739
|
+
seen.add(issueKey);
|
|
740
|
+
picked.push({
|
|
741
|
+
issueKey,
|
|
742
|
+
title: jira?.title?.trim() || issueKey,
|
|
743
|
+
workState: deriveWorkState(run),
|
|
744
|
+
});
|
|
745
|
+
}
|
|
746
|
+
if (picked.length === 0) {
|
|
747
|
+
return textResult(formatTrack([]));
|
|
748
|
+
}
|
|
749
|
+
// Fresh JIRA read (reinforcement 1: the JIRA column is ALWAYS from this live
|
|
750
|
+
// read, never the pick-time cached metadata). A whole-read failure degrades
|
|
751
|
+
// every row; a per-key fallback failure degrades only that row.
|
|
752
|
+
const make = deps.makeJiraClient ?? defaultMakeTrackClient;
|
|
753
|
+
let client;
|
|
754
|
+
let listStatuses;
|
|
755
|
+
try {
|
|
756
|
+
client = make();
|
|
757
|
+
const listed = await client.listMyIssues({ project });
|
|
758
|
+
listStatuses = new Map(listed.issues.map((i) => [i.key, i.status]));
|
|
759
|
+
}
|
|
760
|
+
catch {
|
|
761
|
+
// The assigned-issues read failed entirely — every row degrades (per-row).
|
|
762
|
+
listStatuses = undefined;
|
|
763
|
+
}
|
|
764
|
+
const rows = [];
|
|
765
|
+
for (const p of picked) {
|
|
766
|
+
let jiraStatus = JIRA_UNREACHABLE;
|
|
767
|
+
if (listStatuses) {
|
|
768
|
+
const live = listStatuses.get(p.issueKey);
|
|
769
|
+
if (live !== undefined) {
|
|
770
|
+
jiraStatus = live;
|
|
771
|
+
}
|
|
772
|
+
else if (client) {
|
|
773
|
+
// No longer in the assigned list — a single live read still tracks it.
|
|
774
|
+
try {
|
|
775
|
+
const single = await client.getIssue(p.issueKey);
|
|
776
|
+
jiraStatus = single.issue.status;
|
|
777
|
+
}
|
|
778
|
+
catch {
|
|
779
|
+
jiraStatus = JIRA_UNREACHABLE;
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
rows.push({ ...p, jiraStatus });
|
|
784
|
+
}
|
|
785
|
+
return textResult(formatTrack(rows));
|
|
786
|
+
}
|
|
787
|
+
//# sourceMappingURL=jira-tools.js.map
|