@girardmedia/bootspring 2.7.0 → 3.1.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.
@@ -33,9 +33,9 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
33
33
  ));
34
34
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
35
35
 
36
- // ../../node_modules/.pnpm/tsup@8.5.1_jiti@1.21.7_postcss@8.5.8_tsx@4.21.0_typescript@5.9.3_yaml@2.8.3/node_modules/tsup/assets/cjs_shims.js
36
+ // ../../node_modules/.pnpm/tsup@8.5.1_jiti@1.21.7_postcss@8.5.14_tsx@4.21.0_typescript@5.9.3_yaml@2.8.3/node_modules/tsup/assets/cjs_shims.js
37
37
  var init_cjs_shims = __esm({
38
- "../../node_modules/.pnpm/tsup@8.5.1_jiti@1.21.7_postcss@8.5.8_tsx@4.21.0_typescript@5.9.3_yaml@2.8.3/node_modules/tsup/assets/cjs_shims.js"() {
38
+ "../../node_modules/.pnpm/tsup@8.5.1_jiti@1.21.7_postcss@8.5.14_tsx@4.21.0_typescript@5.9.3_yaml@2.8.3/node_modules/tsup/assets/cjs_shims.js"() {
39
39
  "use strict";
40
40
  }
41
41
  });
@@ -26434,13 +26434,16 @@ var require_data = __commonJS({
26434
26434
  }
26435
26435
  });
26436
26436
 
26437
- // ../../node_modules/.pnpm/fast-uri@3.1.0/node_modules/fast-uri/lib/utils.js
26437
+ // ../../node_modules/.pnpm/fast-uri@3.1.2/node_modules/fast-uri/lib/utils.js
26438
26438
  var require_utils = __commonJS({
26439
- "../../node_modules/.pnpm/fast-uri@3.1.0/node_modules/fast-uri/lib/utils.js"(exports2, module2) {
26439
+ "../../node_modules/.pnpm/fast-uri@3.1.2/node_modules/fast-uri/lib/utils.js"(exports2, module2) {
26440
26440
  "use strict";
26441
26441
  init_cjs_shims();
26442
26442
  var isUUID = RegExp.prototype.test.bind(/^[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12}$/iu);
26443
26443
  var isIPv4 = RegExp.prototype.test.bind(/^(?:(?:25[0-5]|2[0-4]\d|1\d{2}|[1-9]\d|\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d{2}|[1-9]\d|\d)$/u);
26444
+ var isHexPair = RegExp.prototype.test.bind(/^[\da-f]{2}$/iu);
26445
+ var isUnreserved = RegExp.prototype.test.bind(/^[\da-z\-._~]$/iu);
26446
+ var isPathCharacter = RegExp.prototype.test.bind(/^[\da-z\-._~!$&'()*+,;=:@/]$/iu);
26444
26447
  function stringArrayToHexStripped(input) {
26445
26448
  let acc = "";
26446
26449
  let code = 0;
@@ -26633,27 +26636,77 @@ var require_utils = __commonJS({
26633
26636
  }
26634
26637
  return output.join("");
26635
26638
  }
26636
- function normalizeComponentEncoding(component, esc) {
26637
- const func = esc !== true ? escape : unescape;
26638
- if (component.scheme !== void 0) {
26639
- component.scheme = func(component.scheme);
26640
- }
26641
- if (component.userinfo !== void 0) {
26642
- component.userinfo = func(component.userinfo);
26643
- }
26644
- if (component.host !== void 0) {
26645
- component.host = func(component.host);
26639
+ var HOST_DELIMS = { "@": "%40", "/": "%2F", "?": "%3F", "#": "%23", ":": "%3A" };
26640
+ var HOST_DELIM_RE = /[@/?#:]/g;
26641
+ var HOST_DELIM_NO_COLON_RE = /[@/?#]/g;
26642
+ function reescapeHostDelimiters(host, isIP) {
26643
+ const re = isIP ? HOST_DELIM_NO_COLON_RE : HOST_DELIM_RE;
26644
+ re.lastIndex = 0;
26645
+ return host.replace(re, (ch) => HOST_DELIMS[ch]);
26646
+ }
26647
+ function normalizePercentEncoding(input, decodeUnreserved = false) {
26648
+ if (input.indexOf("%") === -1) {
26649
+ return input;
26646
26650
  }
26647
- if (component.path !== void 0) {
26648
- component.path = func(component.path);
26651
+ let output = "";
26652
+ for (let i = 0; i < input.length; i++) {
26653
+ if (input[i] === "%" && i + 2 < input.length) {
26654
+ const hex = input.slice(i + 1, i + 3);
26655
+ if (isHexPair(hex)) {
26656
+ const normalizedHex = hex.toUpperCase();
26657
+ const decoded = String.fromCharCode(parseInt(normalizedHex, 16));
26658
+ if (decodeUnreserved && isUnreserved(decoded)) {
26659
+ output += decoded;
26660
+ } else {
26661
+ output += "%" + normalizedHex;
26662
+ }
26663
+ i += 2;
26664
+ continue;
26665
+ }
26666
+ }
26667
+ output += input[i];
26649
26668
  }
26650
- if (component.query !== void 0) {
26651
- component.query = func(component.query);
26669
+ return output;
26670
+ }
26671
+ function normalizePathEncoding(input) {
26672
+ let output = "";
26673
+ for (let i = 0; i < input.length; i++) {
26674
+ if (input[i] === "%" && i + 2 < input.length) {
26675
+ const hex = input.slice(i + 1, i + 3);
26676
+ if (isHexPair(hex)) {
26677
+ const normalizedHex = hex.toUpperCase();
26678
+ const decoded = String.fromCharCode(parseInt(normalizedHex, 16));
26679
+ if (decoded !== "." && isUnreserved(decoded)) {
26680
+ output += decoded;
26681
+ } else {
26682
+ output += "%" + normalizedHex;
26683
+ }
26684
+ i += 2;
26685
+ continue;
26686
+ }
26687
+ }
26688
+ if (isPathCharacter(input[i])) {
26689
+ output += input[i];
26690
+ } else {
26691
+ output += escape(input[i]);
26692
+ }
26652
26693
  }
26653
- if (component.fragment !== void 0) {
26654
- component.fragment = func(component.fragment);
26694
+ return output;
26695
+ }
26696
+ function escapePreservingEscapes(input) {
26697
+ let output = "";
26698
+ for (let i = 0; i < input.length; i++) {
26699
+ if (input[i] === "%" && i + 2 < input.length) {
26700
+ const hex = input.slice(i + 1, i + 3);
26701
+ if (isHexPair(hex)) {
26702
+ output += "%" + hex.toUpperCase();
26703
+ i += 2;
26704
+ continue;
26705
+ }
26706
+ }
26707
+ output += escape(input[i]);
26655
26708
  }
26656
- return component;
26709
+ return output;
26657
26710
  }
26658
26711
  function recomposeAuthority(component) {
26659
26712
  const uriTokens = [];
@@ -26668,7 +26721,7 @@ var require_utils = __commonJS({
26668
26721
  if (ipV6res.isIPV6 === true) {
26669
26722
  host = `[${ipV6res.escapedHost}]`;
26670
26723
  } else {
26671
- host = component.host;
26724
+ host = reescapeHostDelimiters(host, false);
26672
26725
  }
26673
26726
  }
26674
26727
  uriTokens.push(host);
@@ -26682,7 +26735,10 @@ var require_utils = __commonJS({
26682
26735
  module2.exports = {
26683
26736
  nonSimpleDomain,
26684
26737
  recomposeAuthority,
26685
- normalizeComponentEncoding,
26738
+ reescapeHostDelimiters,
26739
+ normalizePercentEncoding,
26740
+ normalizePathEncoding,
26741
+ escapePreservingEscapes,
26686
26742
  removeDotSegments,
26687
26743
  isIPv4,
26688
26744
  isUUID,
@@ -26692,9 +26748,9 @@ var require_utils = __commonJS({
26692
26748
  }
26693
26749
  });
26694
26750
 
26695
- // ../../node_modules/.pnpm/fast-uri@3.1.0/node_modules/fast-uri/lib/schemes.js
26751
+ // ../../node_modules/.pnpm/fast-uri@3.1.2/node_modules/fast-uri/lib/schemes.js
26696
26752
  var require_schemes = __commonJS({
26697
- "../../node_modules/.pnpm/fast-uri@3.1.0/node_modules/fast-uri/lib/schemes.js"(exports2, module2) {
26753
+ "../../node_modules/.pnpm/fast-uri@3.1.2/node_modules/fast-uri/lib/schemes.js"(exports2, module2) {
26698
26754
  "use strict";
26699
26755
  init_cjs_shims();
26700
26756
  var { isUUID } = require_utils();
@@ -26903,17 +26959,17 @@ var require_schemes = __commonJS({
26903
26959
  }
26904
26960
  });
26905
26961
 
26906
- // ../../node_modules/.pnpm/fast-uri@3.1.0/node_modules/fast-uri/index.js
26962
+ // ../../node_modules/.pnpm/fast-uri@3.1.2/node_modules/fast-uri/index.js
26907
26963
  var require_fast_uri = __commonJS({
26908
- "../../node_modules/.pnpm/fast-uri@3.1.0/node_modules/fast-uri/index.js"(exports2, module2) {
26964
+ "../../node_modules/.pnpm/fast-uri@3.1.2/node_modules/fast-uri/index.js"(exports2, module2) {
26909
26965
  "use strict";
26910
26966
  init_cjs_shims();
26911
- var { normalizeIPv6, removeDotSegments, recomposeAuthority, normalizeComponentEncoding, isIPv4, nonSimpleDomain } = require_utils();
26967
+ var { normalizeIPv6, removeDotSegments, recomposeAuthority, normalizePercentEncoding, normalizePathEncoding, escapePreservingEscapes, reescapeHostDelimiters, isIPv4, nonSimpleDomain } = require_utils();
26912
26968
  var { SCHEMES, getSchemeHandler } = require_schemes();
26913
26969
  function normalize(uri, options) {
26914
26970
  if (typeof uri === "string") {
26915
26971
  uri = /** @type {T} */
26916
- serialize(parse(uri, options), options);
26972
+ normalizeString(uri, options);
26917
26973
  } else if (typeof uri === "object") {
26918
26974
  uri = /** @type {T} */
26919
26975
  parse(serialize(uri, options), options);
@@ -26980,19 +27036,9 @@ var require_fast_uri = __commonJS({
26980
27036
  return target;
26981
27037
  }
26982
27038
  function equal(uriA, uriB, options) {
26983
- if (typeof uriA === "string") {
26984
- uriA = unescape(uriA);
26985
- uriA = serialize(normalizeComponentEncoding(parse(uriA, options), true), { ...options, skipEscape: true });
26986
- } else if (typeof uriA === "object") {
26987
- uriA = serialize(normalizeComponentEncoding(uriA, true), { ...options, skipEscape: true });
26988
- }
26989
- if (typeof uriB === "string") {
26990
- uriB = unescape(uriB);
26991
- uriB = serialize(normalizeComponentEncoding(parse(uriB, options), true), { ...options, skipEscape: true });
26992
- } else if (typeof uriB === "object") {
26993
- uriB = serialize(normalizeComponentEncoding(uriB, true), { ...options, skipEscape: true });
26994
- }
26995
- return uriA.toLowerCase() === uriB.toLowerCase();
27039
+ const normalizedA = normalizeComparableURI(uriA, options);
27040
+ const normalizedB = normalizeComparableURI(uriB, options);
27041
+ return normalizedA !== void 0 && normalizedB !== void 0 && normalizedA.toLowerCase() === normalizedB.toLowerCase();
26996
27042
  }
26997
27043
  function serialize(cmpts, opts) {
26998
27044
  const component = {
@@ -27017,12 +27063,12 @@ var require_fast_uri = __commonJS({
27017
27063
  if (schemeHandler && schemeHandler.serialize) schemeHandler.serialize(component, options);
27018
27064
  if (component.path !== void 0) {
27019
27065
  if (!options.skipEscape) {
27020
- component.path = escape(component.path);
27066
+ component.path = escapePreservingEscapes(component.path);
27021
27067
  if (component.scheme !== void 0) {
27022
27068
  component.path = component.path.split("%3A").join(":");
27023
27069
  }
27024
27070
  } else {
27025
- component.path = unescape(component.path);
27071
+ component.path = normalizePercentEncoding(component.path);
27026
27072
  }
27027
27073
  }
27028
27074
  if (options.reference !== "suffix" && component.scheme) {
@@ -27057,7 +27103,16 @@ var require_fast_uri = __commonJS({
27057
27103
  return uriTokens.join("");
27058
27104
  }
27059
27105
  var URI_PARSE = /^(?:([^#/:?]+):)?(?:\/\/((?:([^#/?@]*)@)?(\[[^#/?\]]+\]|[^#/:?]*)(?::(\d*))?))?([^#?]*)(?:\?([^#]*))?(?:#((?:.|[\n\r])*))?/u;
27060
- function parse(uri, opts) {
27106
+ function getParseError(parsed, matches) {
27107
+ if (matches[2] !== void 0 && parsed.path && parsed.path[0] !== "/") {
27108
+ return 'URI path must start with "/" when authority is present.';
27109
+ }
27110
+ if (typeof parsed.port === "number" && (parsed.port < 0 || parsed.port > 65535)) {
27111
+ return "URI port is malformed.";
27112
+ }
27113
+ return void 0;
27114
+ }
27115
+ function parseWithStatus(uri, opts) {
27061
27116
  const options = Object.assign({}, opts);
27062
27117
  const parsed = {
27063
27118
  scheme: void 0,
@@ -27068,6 +27123,7 @@ var require_fast_uri = __commonJS({
27068
27123
  query: void 0,
27069
27124
  fragment: void 0
27070
27125
  };
27126
+ let malformedAuthorityOrPort = false;
27071
27127
  let isIP = false;
27072
27128
  if (options.reference === "suffix") {
27073
27129
  if (options.scheme) {
@@ -27088,6 +27144,11 @@ var require_fast_uri = __commonJS({
27088
27144
  if (isNaN(parsed.port)) {
27089
27145
  parsed.port = matches[5];
27090
27146
  }
27147
+ const parseError = getParseError(parsed, matches);
27148
+ if (parseError !== void 0) {
27149
+ parsed.error = parsed.error || parseError;
27150
+ malformedAuthorityOrPort = true;
27151
+ }
27091
27152
  if (parsed.host) {
27092
27153
  const ipv4result = isIPv4(parsed.host);
27093
27154
  if (ipv4result === false) {
@@ -27126,14 +27187,18 @@ var require_fast_uri = __commonJS({
27126
27187
  parsed.scheme = unescape(parsed.scheme);
27127
27188
  }
27128
27189
  if (parsed.host !== void 0) {
27129
- parsed.host = unescape(parsed.host);
27190
+ parsed.host = reescapeHostDelimiters(unescape(parsed.host), isIP);
27130
27191
  }
27131
27192
  }
27132
27193
  if (parsed.path) {
27133
- parsed.path = escape(unescape(parsed.path));
27194
+ parsed.path = normalizePathEncoding(parsed.path);
27134
27195
  }
27135
27196
  if (parsed.fragment) {
27136
- parsed.fragment = encodeURI(decodeURIComponent(parsed.fragment));
27197
+ try {
27198
+ parsed.fragment = encodeURI(decodeURIComponent(parsed.fragment));
27199
+ } catch {
27200
+ parsed.error = parsed.error || "URI malformed";
27201
+ }
27137
27202
  }
27138
27203
  }
27139
27204
  if (schemeHandler && schemeHandler.parse) {
@@ -27142,7 +27207,29 @@ var require_fast_uri = __commonJS({
27142
27207
  } else {
27143
27208
  parsed.error = parsed.error || "URI can not be parsed.";
27144
27209
  }
27145
- return parsed;
27210
+ return { parsed, malformedAuthorityOrPort };
27211
+ }
27212
+ function parse(uri, opts) {
27213
+ return parseWithStatus(uri, opts).parsed;
27214
+ }
27215
+ function normalizeString(uri, opts) {
27216
+ return normalizeStringWithStatus(uri, opts).normalized;
27217
+ }
27218
+ function normalizeStringWithStatus(uri, opts) {
27219
+ const { parsed, malformedAuthorityOrPort } = parseWithStatus(uri, opts);
27220
+ return {
27221
+ normalized: malformedAuthorityOrPort ? uri : serialize(parsed, opts),
27222
+ malformedAuthorityOrPort
27223
+ };
27224
+ }
27225
+ function normalizeComparableURI(uri, opts) {
27226
+ if (typeof uri === "string") {
27227
+ const { normalized, malformedAuthorityOrPort } = normalizeStringWithStatus(uri, opts);
27228
+ return malformedAuthorityOrPort ? void 0 : normalized;
27229
+ }
27230
+ if (typeof uri === "object") {
27231
+ return serialize(uri, opts);
27232
+ }
27146
27233
  }
27147
27234
  var fastUri = {
27148
27235
  SCHEMES,
@@ -31377,7 +31464,7 @@ var init_release = __esm({
31377
31464
  "../../packages/shared/src/release.ts"() {
31378
31465
  "use strict";
31379
31466
  init_cjs_shims();
31380
- BOOTSPRING_VERSION = "2.7.0";
31467
+ BOOTSPRING_VERSION = "3.1.0";
31381
31468
  BOOTSPRING_PACKAGE_NAME = "@girardmedia/bootspring";
31382
31469
  }
31383
31470
  });
@@ -47601,7 +47688,7 @@ var require_dist2 = __commonJS({
47601
47688
  ));
47602
47689
  var __toCommonJS2 = (mod) => __copyProps2(__defProp2({}, "__esModule", { value: true }), mod);
47603
47690
  var init_cjs_shims2 = __esm2({
47604
- "../../node_modules/.pnpm/tsup@8.5.1_jiti@1.21.7_postcss@8.5.8_tsx@4.21.0_typescript@5.9.3_yaml@2.8.3/node_modules/tsup/assets/cjs_shims.js"() {
47691
+ "../../node_modules/.pnpm/tsup@8.5.1_jiti@1.21.7_postcss@8.5.14_tsx@4.21.0_typescript@5.9.3_yaml@2.8.3/node_modules/tsup/assets/cjs_shims.js"() {
47605
47692
  "use strict";
47606
47693
  }
47607
47694
  });
@@ -52278,7 +52365,7 @@ var require_package = __commonJS({
52278
52365
  "../../../package.json"(exports2, module2) {
52279
52366
  module2.exports = {
52280
52367
  name: "bootspring-workspace",
52281
- version: "2.7.0",
52368
+ version: "3.1.0",
52282
52369
  private: true,
52283
52370
  description: "Workspace tooling for the Bootspring monorepo",
52284
52371
  keywords: [
@@ -52353,6 +52440,7 @@ var require_package = __commonJS({
52353
52440
  "planning:sync:check": "node scripts/sync-planning-state.js --check",
52354
52441
  "planning:realign": "node scripts/sync-planning-state.js --sync-runtime",
52355
52442
  "verify:package": "node scripts/check-package-boundaries.js",
52443
+ "verify:security": "node scripts/security-scan.js",
52356
52444
  "db:sync": "node monorepo/packages/shared/db/sync.js",
52357
52445
  "db:sync:check": "node monorepo/packages/shared/db/sync.js --check"
52358
52446
  },
@@ -52363,12 +52451,15 @@ var require_package = __commonJS({
52363
52451
  "@typescript-eslint/eslint-plugin": "^8.57.0",
52364
52452
  "@typescript-eslint/parser": "^8.57.0",
52365
52453
  "@vitest/coverage-v8": "^4.0.18",
52454
+ ajv: "^8.18.0",
52366
52455
  eslint: "^9.39.2",
52367
52456
  globals: "^17.3.0",
52368
52457
  tsup: "^8.5.1",
52369
52458
  tsx: "^4.21.0",
52370
52459
  typescript: "^5.9.3",
52371
- vitest: "^4.0.18"
52460
+ vitest: "^4.0.18",
52461
+ yaml: "^2.8.3",
52462
+ zod: "^4.3.6"
52372
52463
  },
52373
52464
  engines: {
52374
52465
  node: ">=18.0.0"
@@ -52378,8 +52469,10 @@ var require_package = __commonJS({
52378
52469
  ajv: "^8.12.0"
52379
52470
  },
52380
52471
  minimatch: "^10.2.1",
52381
- hono: "4.12.4",
52382
- "@hono/node-server": "1.19.10",
52472
+ hono: "4.12.18",
52473
+ "@hono/node-server": "1.19.13",
52474
+ axios: ">=1.16.0",
52475
+ "simple-git": ">=3.36.0",
52383
52476
  "express-rate-limit": "^8.2.2",
52384
52477
  "path-to-regexp@<0.1.13": "0.1.13",
52385
52478
  "path-to-regexp@>=8.0.0 <8.4.0": "8.4.0",
@@ -52482,9 +52575,100 @@ function formatBuildTask(task) {
52482
52575
  phase: task.phase,
52483
52576
  source: task.source,
52484
52577
  sourceSection: task.sourceSection,
52485
- acceptanceCriteria: task.acceptanceCriteria || []
52578
+ acceptanceCriteria: task.acceptanceCriteria || [],
52579
+ dependencies: task.dependencies || []
52486
52580
  };
52487
52581
  }
52582
+ function autoCommitTask(taskId, taskTitle) {
52583
+ try {
52584
+ const { execSync } = require("child_process");
52585
+ const cwd = getProjectRoot();
52586
+ const status = execSync("git status --porcelain", { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
52587
+ if (!status) return { committed: false, reason: "no_changes" };
52588
+ execSync("git add -A", { cwd, stdio: ["ignore", "ignore", "ignore"] });
52589
+ const msg = `feat(${taskId}): ${taskTitle}`;
52590
+ execSync(`git commit -m ${JSON.stringify(msg)}`, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
52591
+ return { committed: true, message: msg };
52592
+ } catch (e) {
52593
+ return { committed: false, reason: "git_error", error: String(e?.message || e).slice(0, 200) };
52594
+ }
52595
+ }
52596
+ function verifyQuality() {
52597
+ try {
52598
+ const { execSync } = require("child_process");
52599
+ const cwd = getProjectRoot();
52600
+ const fs3 = require("fs");
52601
+ const path3 = require("path");
52602
+ const pkgPath = path3.join(cwd, "package.json");
52603
+ if (!fs3.existsSync(pkgPath)) return { verified: true, skipped: true };
52604
+ const pkg = JSON.parse(fs3.readFileSync(pkgPath, "utf-8"));
52605
+ const hasTest = pkg.scripts?.test;
52606
+ const hasTypecheck = pkg.scripts?.typecheck;
52607
+ const results = { verified: true, checks: [] };
52608
+ if (hasTypecheck) {
52609
+ try {
52610
+ execSync("npm run typecheck 2>&1", { cwd, encoding: "utf8", timeout: 6e4, stdio: ["ignore", "pipe", "pipe"] });
52611
+ results.checks.push({ name: "typecheck", passed: true });
52612
+ } catch (e) {
52613
+ results.checks.push({ name: "typecheck", passed: false, error: String(e?.stdout || e?.message || "").slice(0, 500) });
52614
+ results.verified = false;
52615
+ }
52616
+ }
52617
+ if (hasTest && pkg.devDependencies?.vitest) {
52618
+ try {
52619
+ const changed = execSync("git diff --name-only HEAD 2>/dev/null || true", { cwd, encoding: "utf8" }).trim();
52620
+ const testFiles = changed.split("\n").filter((f) => f.match(/\.(test|spec)\.(ts|js|tsx|jsx)$/));
52621
+ if (testFiles.length > 0) {
52622
+ execSync(`npx vitest run ${testFiles.join(" ")} 2>&1`, { cwd, encoding: "utf8", timeout: 12e4, stdio: ["ignore", "pipe", "pipe"] });
52623
+ results.checks.push({ name: "tests", passed: true, files: testFiles.length });
52624
+ } else {
52625
+ results.checks.push({ name: "tests", passed: true, skipped: true, reason: "no_changed_test_files" });
52626
+ }
52627
+ } catch (e) {
52628
+ results.checks.push({ name: "tests", passed: false, error: String(e?.stdout || e?.message || "").slice(0, 500) });
52629
+ results.verified = false;
52630
+ }
52631
+ }
52632
+ return results;
52633
+ } catch {
52634
+ return { verified: true, skipped: true };
52635
+ }
52636
+ }
52637
+ function loadHandoff() {
52638
+ const fs3 = require("fs");
52639
+ const path3 = require("path");
52640
+ const handoffPath = path3.join(getProjectRoot(), "planning", ".handoff.json");
52641
+ if (!fs3.existsSync(handoffPath)) return null;
52642
+ try {
52643
+ return JSON.parse(fs3.readFileSync(handoffPath, "utf-8"));
52644
+ } catch {
52645
+ return null;
52646
+ }
52647
+ }
52648
+ function saveHandoff(data) {
52649
+ const fs3 = require("fs");
52650
+ const path3 = require("path");
52651
+ const planDir = path3.join(getProjectRoot(), "planning");
52652
+ if (!fs3.existsSync(planDir)) fs3.mkdirSync(planDir, { recursive: true });
52653
+ fs3.writeFileSync(path3.join(planDir, ".handoff.json"), JSON.stringify(data, null, 2));
52654
+ }
52655
+ function getBatchTasks(state, count) {
52656
+ const q = state?.implementationQueue || [];
52657
+ const pending = [];
52658
+ for (const task of q) {
52659
+ if (task.status !== "pending") continue;
52660
+ if (task.dependencies?.length > 0) {
52661
+ const allDone = task.dependencies.every((depId) => {
52662
+ const dep = q.find((t) => t.id === depId);
52663
+ return dep && dep.status === "completed";
52664
+ });
52665
+ if (!allDone) continue;
52666
+ }
52667
+ pending.push(task);
52668
+ if (pending.length >= count) break;
52669
+ }
52670
+ return pending;
52671
+ }
52488
52672
  async function executeLocalBuild(args) {
52489
52673
  const action = args?.action || "status";
52490
52674
  const state = loadBuildState();
@@ -52561,6 +52745,8 @@ async function executeLocalBuild(args) {
52561
52745
  if (!inProgress) {
52562
52746
  return { content: [{ type: "text", text: JSON.stringify({ error: "No task currently in progress", hint: "Use action=next to get a task first" }, null, 2) }] };
52563
52747
  }
52748
+ const quality = args?.verify !== false ? verifyQuality() : { verified: true, skipped: true };
52749
+ const commitResult = args?.autoCommit !== false ? autoCommitTask(inProgress.id, inProgress.title) : { committed: false, reason: "disabled" };
52564
52750
  buildUpdateTaskStatus(state, inProgress.id, "completed");
52565
52751
  const nextTask = buildGetNextTask(state);
52566
52752
  if (nextTask) {
@@ -52570,10 +52756,13 @@ async function executeLocalBuild(args) {
52570
52756
  const stats = buildGetStats(state);
52571
52757
  return { content: [{ type: "text", text: JSON.stringify({
52572
52758
  completed: { id: inProgress.id, title: inProgress.title },
52759
+ commit: commitResult,
52760
+ quality,
52573
52761
  progress: { completed: stats.completed, total: stats.total, percent: stats.percent },
52574
52762
  nextTask: nextTask ? { ...formatBuildTask(nextTask), file: "planning/TODO.md" } : null,
52575
52763
  allComplete: !nextTask,
52576
- message: nextTask ? `Complete! Next: implement ${nextTask.id} \u2014 ${nextTask.title}` : "Build complete! All tasks finished."
52764
+ state: nextTask ? "advanced" : "all_complete",
52765
+ message: nextTask ? `Done! Committed ${inProgress.id}. Next: ${nextTask.id} \u2014 ${nextTask.title}` : "Build complete! All tasks finished."
52577
52766
  }, null, 2) }] };
52578
52767
  }
52579
52768
  case "skip": {
@@ -52795,8 +52984,66 @@ async function executeLocalBuild(args) {
52795
52984
  message: marked.length > 0 ? `Marked ${marked.length} task${marked.length === 1 ? "" : "s"} as completed from git history` : `Scanned ${lines.length} commits, found ${foundIds.size} bs-IDs, but none matched pending/in-progress tasks`
52796
52985
  }, null, 2) }] };
52797
52986
  }
52987
+ case "handoff": {
52988
+ if (args?.save) {
52989
+ const handoffData = {
52990
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
52991
+ lastTaskId: args.lastTaskId || null,
52992
+ inProgressTask: state ? (state.implementationQueue || []).find((t) => t.status === "in_progress")?.id || null : null,
52993
+ filesModified: args.filesModified || [],
52994
+ notes: args.notes || "",
52995
+ blockers: args.blockers || [],
52996
+ progress: state ? buildGetStats(state) : null
52997
+ };
52998
+ saveHandoff(handoffData);
52999
+ return { content: [{ type: "text", text: JSON.stringify({
53000
+ saved: true,
53001
+ handoff: handoffData,
53002
+ message: "Session handoff saved. Next session will pick up from here."
53003
+ }, null, 2) }] };
53004
+ }
53005
+ const handoff = loadHandoff();
53006
+ if (!handoff) {
53007
+ return { content: [{ type: "text", text: JSON.stringify({
53008
+ hasHandoff: false,
53009
+ message: "No previous session handoff found. Start fresh with action=next."
53010
+ }, null, 2) }] };
53011
+ }
53012
+ const currentState = state ? buildGetStats(state) : null;
53013
+ return { content: [{ type: "text", text: JSON.stringify({
53014
+ hasHandoff: true,
53015
+ handoff,
53016
+ currentProgress: currentState,
53017
+ message: `Previous session left off at ${handoff.inProgressTask || handoff.lastTaskId || "unknown"}. ${handoff.notes || "No notes."}`,
53018
+ suggestedAction: handoff.inProgressTask ? "action=current to see the in-progress task" : "action=next to continue"
53019
+ }, null, 2) }] };
53020
+ }
53021
+ case "batch": {
53022
+ if (!state) {
53023
+ return { content: [{ type: "text", text: JSON.stringify({ error: "No build state found" }, null, 2) }] };
53024
+ }
53025
+ const count = Math.min(Math.max(args?.count || 5, 1), 10);
53026
+ const batchTasks = getBatchTasks(state, count);
53027
+ if (batchTasks.length === 0) {
53028
+ const stats = buildGetStats(state);
53029
+ return { content: [{ type: "text", text: JSON.stringify({
53030
+ message: "No eligible pending tasks",
53031
+ progress: stats
53032
+ }, null, 2) }] };
53033
+ }
53034
+ return { content: [{ type: "text", text: JSON.stringify({
53035
+ batch: batchTasks.map(formatBuildTask),
53036
+ count: batchTasks.length,
53037
+ instructions: [
53038
+ "Implement all tasks in this batch",
53039
+ "For each completed task, call action=done",
53040
+ "Tasks are ordered by dependency \u2014 complete them in order",
53041
+ "If stuck on one, call action=skip and continue with the next"
53042
+ ]
53043
+ }, null, 2) }] };
53044
+ }
52798
53045
  default:
52799
- return { content: [{ type: "text", text: JSON.stringify({ error: `Unknown action: ${action}`, validActions: ["next", "current", "done", "skip", "status", "list", "init", "sync", "advance", "scan"] }, null, 2) }] };
53046
+ return { content: [{ type: "text", text: JSON.stringify({ error: `Unknown action: ${action}`, validActions: ["next", "current", "done", "skip", "status", "list", "init", "sync", "advance", "scan", "handoff", "batch"] }, null, 2) }] };
52800
53047
  }
52801
53048
  }
52802
53049
  async function executeLocalSeed(args) {
@@ -53054,13 +53301,31 @@ var FALLBACK_TOOLS = [
53054
53301
  },
53055
53302
  {
53056
53303
  name: "bootspring_build",
53057
- description: "Autonomous build system. Get tasks, mark complete, check progress. Use action=next to get next task, action=done to mark complete, action=status for progress.",
53304
+ description: `Autonomous build loop. Manages task queue with auto-commit and quality verification.
53305
+
53306
+ Core loop: next -> implement -> done -> (auto-advances to next task)
53307
+ - action=done auto-commits with conventional message and returns next task
53308
+ - action=batch returns multiple tasks for efficient batch execution
53309
+ - action=handoff saves/loads session state for cross-session continuity
53310
+ - Dependencies are checked automatically \u2014 blocked tasks are skipped`,
53058
53311
  inputSchema: {
53059
53312
  type: "object",
53060
53313
  properties: {
53061
- action: { type: "string", enum: ["next", "current", "done", "skip", "status", "list", "init", "sync", "advance", "scan"] },
53062
- reason: { type: "string" },
53063
- autoDone: { type: "boolean" }
53314
+ action: {
53315
+ type: "string",
53316
+ enum: ["next", "current", "done", "skip", "status", "list", "init", "sync", "advance", "scan", "handoff", "batch"],
53317
+ description: "next: get next task | done: mark complete + auto-commit + return next | batch: get N tasks | handoff: save/load session state | advance: autonomous state machine"
53318
+ },
53319
+ reason: { type: "string", description: "Reason for skipping (action=skip)" },
53320
+ autoDone: { type: "boolean", description: "Auto-complete if git is clean (action=advance)" },
53321
+ autoCommit: { type: "boolean", description: "Auto-commit on done (default: true)" },
53322
+ verify: { type: "boolean", description: "Run quality checks before done (default: true)" },
53323
+ count: { type: "number", description: "Number of tasks to return (action=batch, max 10)" },
53324
+ save: { type: "boolean", description: "Save handoff state (action=handoff)" },
53325
+ notes: { type: "string", description: "Handoff notes for next session (action=handoff save)" },
53326
+ filesModified: { type: "array", items: { type: "string" }, description: "Files modified this session (action=handoff save)" },
53327
+ blockers: { type: "array", items: { type: "string" }, description: "Unresolved blockers (action=handoff save)" },
53328
+ lastTaskId: { type: "string", description: "Last completed task ID (action=handoff save)" }
53064
53329
  },
53065
53330
  required: ["action"]
53066
53331
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@girardmedia/bootspring",
3
- "version": "2.7.0",
3
+ "version": "3.1.0",
4
4
  "description": "Thin client for Bootspring cloud MCP, hosted agents, and paywalled workflow intelligence",
5
5
  "keywords": [
6
6
  "ai",
@@ -33,7 +33,13 @@ function resolveCommandsSource() {
33
33
 
34
34
  const COMMANDS_SOURCE = resolveCommandsSource();
35
35
 
36
- const BOOTSPRING_SKILL_CONTENT = `# Bootspring MCP Operating Skill
36
+ const BOOTSPRING_SKILL_CONTENT = `---
37
+ name: bootspring
38
+ description: Bootspring MCP Operating Skill
39
+ version: 1.0.0
40
+ ---
41
+
42
+ # Bootspring MCP Operating Skill
37
43
 
38
44
  Use Bootspring MCP tools as the primary workflow for any project with Bootspring configured.
39
45
  These tools work via MCP protocol with any assistant: Claude Code, Codex, Gemini CLI, or others.