mandrel 2.65.0 → 2.66.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.
Files changed (56) hide show
  1. package/.agents/agents/acceptance-critic.md +5 -5
  2. package/.agents/agents/auditor.md +17 -18
  3. package/.agents/agents/plan-critic.md +5 -5
  4. package/.agents/agents/story-worker.md +5 -5
  5. package/.agents/docs/execution-reference.md +27 -5
  6. package/.agents/instructions.md +10 -12
  7. package/.agents/rules/ci-remediation.md +3 -3
  8. package/.agents/rules/gherkin-standards.md +3 -2
  9. package/.agents/rules/git-conventions-reference.md +12 -3
  10. package/.agents/rules/git-conventions.md +9 -7
  11. package/.agents/rules/testing-standards.md +8 -7
  12. package/.agents/runtime-deps.json +1 -1
  13. package/.agents/scripts/bootstrap.js +94 -89
  14. package/.agents/scripts/lib/baselines/duplication-scanner.js +17 -7
  15. package/.agents/scripts/lib/bootstrap/project-bootstrap.js +78 -78
  16. package/.agents/scripts/lib/cli/standard-args.js +60 -76
  17. package/.agents/scripts/lib/cli-args.js +26 -0
  18. package/.agents/scripts/lib/config/gates/shared.js +3 -3
  19. package/.agents/scripts/lib/feedback-loop/graduate-steps.js +205 -0
  20. package/.agents/scripts/lib/feedback-loop/graduator-core.js +47 -782
  21. package/.agents/scripts/lib/feedback-loop/graduator-gh.js +449 -0
  22. package/.agents/scripts/lib/observability/close-telemetry.js +330 -0
  23. package/.agents/scripts/lib/observability/runtime-friction.js +2 -0
  24. package/.agents/scripts/lib/observability/signal-validator.js +17 -5
  25. package/.agents/scripts/lib/orchestration/code-review.js +22 -0
  26. package/.agents/scripts/lib/orchestration/plan-metrics.js +76 -63
  27. package/.agents/scripts/lib/orchestration/review-providers/review-provider-factory.js +23 -0
  28. package/.agents/scripts/lib/orchestration/run-epilogue.js +6 -0
  29. package/.agents/scripts/lib/orchestration/single-story-close/phases/code-review.js +2 -0
  30. package/.agents/scripts/lib/orchestration/single-story-close/phases/confirm-merge.js +349 -263
  31. package/.agents/scripts/lib/orchestration/single-story-close/phases/options.js +21 -7
  32. package/.agents/scripts/lib/orchestration/single-story-close/phases/review-override.js +4 -0
  33. package/.agents/scripts/lib/orchestration/single-story-close/runner.js +327 -314
  34. package/.agents/scripts/lib/orchestration/ticket-validator.js +19 -36
  35. package/.agents/scripts/lib/signals/detectors/common.js +63 -51
  36. package/.agents/scripts/lib/transpile.js +28 -3
  37. package/.agents/scripts/single-story-close.js +10 -2
  38. package/.agents/scripts/single-story-confirm-merge.js +267 -238
  39. package/.agents/skills/core/idea-refinement/SKILL.md +6 -6
  40. package/.agents/skills/stack/qa/qa-harness/SKILL.md +1 -2
  41. package/.agents/workflows/audit-architecture.md +5 -4
  42. package/.agents/workflows/audit-documentation.md +5 -5
  43. package/.agents/workflows/audit-performance.md +10 -10
  44. package/.agents/workflows/helpers/acceptance-self-eval.md +8 -8
  45. package/.agents/workflows/helpers/audit-lens-core.md +30 -57
  46. package/.agents/workflows/helpers/deliver-digest.md +2 -2
  47. package/.agents/workflows/helpers/deliver-reference.md +3 -1
  48. package/.agents/workflows/helpers/deliver-story.md +6 -1
  49. package/.agents/workflows/helpers/parallel-tooling.md +16 -18
  50. package/.agents/workflows/mandrel-deliver.md +1 -1
  51. package/.agents/workflows/mandrel-plan.md +6 -5
  52. package/docs/CHANGELOG.md +26 -0
  53. package/lib/cli/guarded-sync.js +87 -0
  54. package/lib/cli/sync-agents.js +9 -92
  55. package/lib/cli/sync-commands.js +9 -101
  56. package/package.json +2 -2
@@ -60,49 +60,32 @@ function collectTaskPathReferences(task) {
60
60
  */
61
61
  function collectTaskChangesPaths(task) {
62
62
  const paths = new Set();
63
- const body = task.body;
63
+ const source = resolveChangesSource(task);
64
+ if (source === null) return paths;
65
+ for (const arrName of ['changes', 'references']) {
66
+ const arr = source[arrName];
67
+ if (!Array.isArray(arr)) continue;
68
+ for (const item of arr) collectChangesItem(item, paths);
69
+ }
70
+ return paths;
71
+ }
64
72
 
73
+ function resolveChangesSource(task) {
74
+ const body = task.body;
65
75
  // A parse failure throws rather than yielding an empty whitelist, which
66
76
  // would misreport every declared path as missing.
67
77
  if (typeof body === 'string' && body.trim().length > 0) {
68
- const parsed = parseStoryBodyOrThrow(task);
69
- for (const arrName of ['changes', 'references']) {
70
- const arr = parsed[arrName];
71
- if (!Array.isArray(arr)) continue;
72
- for (const item of arr) {
73
- if (typeof item === 'string') {
74
- collectPathsFromText(item, paths);
75
- } else if (
76
- item !== null &&
77
- typeof item === 'object' &&
78
- typeof item.path === 'string' &&
79
- item.path.length > 0
80
- ) {
81
- paths.add(item.path);
82
- }
83
- }
84
- }
85
- return paths;
78
+ return parseStoryBodyOrThrow(task);
86
79
  }
80
+ return body !== null && typeof body === 'object' ? body : null;
81
+ }
87
82
 
88
- if (body === null || typeof body !== 'object') return paths;
89
- for (const arrName of ['changes', 'references']) {
90
- const arr = body[arrName];
91
- if (!Array.isArray(arr)) continue;
92
- for (const item of arr) {
93
- if (typeof item === 'string') {
94
- collectPathsFromText(item, paths);
95
- } else if (
96
- item !== null &&
97
- typeof item === 'object' &&
98
- typeof item.path === 'string' &&
99
- item.path.length > 0
100
- ) {
101
- paths.add(item.path);
102
- }
103
- }
83
+ function collectChangesItem(item, paths) {
84
+ if (typeof item === 'string') {
85
+ collectPathsFromText(item, paths);
86
+ } else if (typeof item?.path === 'string' && item.path.length > 0) {
87
+ paths.add(item.path);
104
88
  }
105
- return paths;
106
89
  }
107
90
 
108
91
  function defaultGitRunner({ baseBranchRef, path, cwd }) {
@@ -51,9 +51,12 @@ export function extractTool(rec) {
51
51
  */
52
52
  export function validateDetectorArgs(args, opts) {
53
53
  const { fnName } = opts;
54
- const requireTracesPath = opts.requireTracesPath ?? true;
55
- const requireStoryId = opts.requireStoryId ?? true;
56
- const requireThreshold = opts.requireThreshold ?? true;
54
+ const gates = {
55
+ always: true,
56
+ requireTracesPath: opts.requireTracesPath ?? true,
57
+ requireStoryId: opts.requireStoryId ?? true,
58
+ requireThreshold: opts.requireThreshold ?? true,
59
+ };
57
60
 
58
61
  if (args == null || typeof args !== 'object') {
59
62
  throw new TypeError(
@@ -61,57 +64,66 @@ export function validateDetectorArgs(args, opts) {
61
64
  );
62
65
  }
63
66
 
64
- const { tracesPath, epicId, storyId, threshold } = args;
65
- const taskId = args.taskId ?? null;
66
-
67
- if (args.nowFn != null && typeof args.nowFn !== 'function') {
68
- throw new TypeError(
69
- `${fnName}: nowFn, when provided, must be a function (got ${typeof args.nowFn})`,
70
- );
71
- }
72
- const nowFn = args.nowFn ?? (() => new Date().toISOString());
73
-
74
- if (requireTracesPath) {
75
- if (typeof tracesPath !== 'string' || tracesPath.length === 0) {
76
- throw new TypeError(
77
- `${fnName}: tracesPath must be a non-empty string (got ${tracesPath})`,
78
- );
79
- }
80
- }
81
-
82
- if (!isPositiveInt(epicId)) {
83
- throw new RangeError(
84
- `${fnName}: epicId must be a positive integer (got ${epicId})`,
85
- );
86
- }
87
-
88
- if (requireStoryId) {
89
- if (!isPositiveInt(storyId)) {
90
- throw new RangeError(
91
- `${fnName}: storyId must be a positive integer (got ${storyId})`,
92
- );
93
- }
94
- if (taskId !== null && !isPositiveInt(taskId)) {
95
- throw new RangeError(
96
- `${fnName}: taskId must be a positive integer or null (got ${taskId})`,
97
- );
98
- }
99
- }
100
-
101
- if (requireThreshold) {
102
- if (!Number.isInteger(threshold) || threshold < 0) {
103
- throw new RangeError(
104
- `${fnName}: threshold must be a non-negative integer (got ${threshold})`,
105
- );
67
+ const values = { ...args, taskId: args.taskId ?? null };
68
+ for (const rule of DETECTOR_ARG_RULES) {
69
+ const value = values[rule.field];
70
+ if (gates[rule.gate] && rule.invalid(value)) {
71
+ throw new rule.Error(`${fnName}: ${rule.message(value)}`);
106
72
  }
107
73
  }
108
74
 
109
75
  return {
110
- tracesPath: requireTracesPath ? tracesPath : undefined,
111
- epicId,
112
- storyId: requireStoryId ? storyId : undefined,
113
- taskId: requireStoryId ? taskId : undefined,
114
- threshold: requireThreshold ? threshold : undefined,
115
- nowFn,
76
+ tracesPath: gates.requireTracesPath ? values.tracesPath : undefined,
77
+ epicId: values.epicId,
78
+ storyId: gates.requireStoryId ? values.storyId : undefined,
79
+ taskId: gates.requireStoryId ? values.taskId : undefined,
80
+ threshold: gates.requireThreshold ? values.threshold : undefined,
81
+ nowFn: args.nowFn ?? (() => new Date().toISOString()),
116
82
  };
117
83
  }
84
+
85
+ const DETECTOR_ARG_RULES = [
86
+ {
87
+ field: 'nowFn',
88
+ gate: 'always',
89
+ invalid: (v) => v != null && typeof v !== 'function',
90
+ Error: TypeError,
91
+ message: (v) =>
92
+ `nowFn, when provided, must be a function (got ${typeof v})`,
93
+ },
94
+ {
95
+ field: 'tracesPath',
96
+ gate: 'requireTracesPath',
97
+ invalid: (v) => typeof v !== 'string' || v.length === 0,
98
+ Error: TypeError,
99
+ message: (v) => `tracesPath must be a non-empty string (got ${v})`,
100
+ },
101
+ {
102
+ field: 'epicId',
103
+ gate: 'always',
104
+ invalid: (v) => !isPositiveInt(v),
105
+ Error: RangeError,
106
+ message: (v) => `epicId must be a positive integer (got ${v})`,
107
+ },
108
+ {
109
+ field: 'storyId',
110
+ gate: 'requireStoryId',
111
+ invalid: (v) => !isPositiveInt(v),
112
+ Error: RangeError,
113
+ message: (v) => `storyId must be a positive integer (got ${v})`,
114
+ },
115
+ {
116
+ field: 'taskId',
117
+ gate: 'requireStoryId',
118
+ invalid: (v) => v !== null && !isPositiveInt(v),
119
+ Error: RangeError,
120
+ message: (v) => `taskId must be a positive integer or null (got ${v})`,
121
+ },
122
+ {
123
+ field: 'threshold',
124
+ gate: 'requireThreshold',
125
+ invalid: (v) => !Number.isInteger(v) || v < 0,
126
+ Error: RangeError,
127
+ message: (v) => `threshold must be a non-negative integer (got ${v})`,
128
+ },
129
+ ];
@@ -8,6 +8,9 @@ const require = createRequire(import.meta.url);
8
8
 
9
9
  const TS_EXTS = new Set(['.ts', '.tsx', '.mts', '.cts']);
10
10
 
11
+ /** TS 7 moved `transpileModule` under `unstable/*`; mirrors the peer range. */
12
+ const SUPPORTED_TS_RANGE = '>=5.0.0 <7';
13
+
11
14
  let _ts = null;
12
15
  let _tsLoadFailed = false;
13
16
 
@@ -23,6 +26,27 @@ function loadTypeScript() {
23
26
  }
24
27
  }
25
28
 
29
+ /** Compiler modules already diagnosed as API-less — one warning per module. */
30
+ const _unsupportedDiagnosed = new WeakSet();
31
+
32
+ /**
33
+ * Warn once per API-less module rather than once per file.
34
+ *
35
+ * @param {object} ts
36
+ * @returns {boolean}
37
+ */
38
+ function isUsableCompiler(ts) {
39
+ if (typeof ts.transpileModule === 'function') return true;
40
+ if (!_unsupportedDiagnosed.has(ts)) {
41
+ _unsupportedDiagnosed.add(ts);
42
+ Logger.warn(
43
+ `[Maintainability] ⚠ typescript ${ts.version ?? 'unknown'} exposes no transpileModule API; ` +
44
+ `TypeScript files are not scored. Supported range: ${SUPPORTED_TS_RANGE}.`,
45
+ );
46
+ }
47
+ return false;
48
+ }
49
+
26
50
  let _tsVersion = null;
27
51
 
28
52
  /**
@@ -100,7 +124,7 @@ function buildLineMapper(sourceMapText, code) {
100
124
  *
101
125
  * @param {string} filePath
102
126
  * @param {string} source
103
- * @param {{withLineMap?: boolean}} [opts]
127
+ * @param {{withLineMap?: boolean, typescript?: object}} [opts]
104
128
  * @returns {string|null|{code: string, mapLine: ((line: number) => number|null)|null}}
105
129
  */
106
130
  export function transpileIfNeeded(filePath, source, opts = {}) {
@@ -108,14 +132,15 @@ export function transpileIfNeeded(filePath, source, opts = {}) {
108
132
  if (!isTypeScriptPath(filePath)) {
109
133
  return withLineMap ? { code: source, mapLine: null } : source;
110
134
  }
111
- const ts = loadTypeScript();
135
+ const ts = opts?.typescript ?? loadTypeScript();
112
136
  if (!ts) {
113
137
  Logger.warn(
114
138
  `[Maintainability] ⚠ typescript package not resolvable; cannot score ${filePath}. ` +
115
- "Install with 'npm install --save-dev typescript' (peer dep, >=5.0.0).",
139
+ `Install with 'npm install --save-dev typescript' (peer dep, ${SUPPORTED_TS_RANGE}).`,
116
140
  );
117
141
  return null;
118
142
  }
143
+ if (!isUsableCompiler(ts)) return null;
119
144
  try {
120
145
  const result = ts.transpileModule(source, {
121
146
  compilerOptions: {
@@ -16,7 +16,11 @@
16
16
  * [--merge-watch-mode <sync|async>]
17
17
  * [--rerun-advisory <n>]
18
18
  * [--override-review-block <reason>]
19
+ * [--worker-tokens <n>]
19
20
  *
21
+ * `--worker-tokens <n>` is the story-worker's host-reported token total,
22
+ * recorded best-effort in the close result's `telemetry` (absent or invalid →
23
+ * `null` plus a warning; never a change of status or exit code).
20
24
  * `--override-review-block <reason>` is the audited escape past a code-review
21
25
  * CRITICAL blocker (instead of a hand-merge with no record).
22
26
  * `--merge-watch-mode async` is passed per close by the orchestrator, the only
@@ -35,7 +39,7 @@ import { parseSprintArgsTolerant } from './lib/cli-args.js';
35
39
  import { runAsCli } from './lib/cli-utils.js';
36
40
  import { formatCliError } from './lib/error-redactor.js';
37
41
  import { Logger } from './lib/Logger.js';
38
- import { emitTerminalFriction } from './lib/observability/runtime-friction.js';
42
+ import { emitCloseTerminalSignals } from './lib/observability/close-telemetry.js';
39
43
  import { resolveRunScopedConfig } from './lib/orchestration/run-scoped-config.js';
40
44
  import {
41
45
  failedTerminalFor,
@@ -91,7 +95,7 @@ async function main() {
91
95
  // Mirrors runAsCli's default error line, which this catch pre-empts.
92
96
  Logger.error(`[single-story-close] Fatal error: ${formatCliError(err)}`);
93
97
  emitTerminalEnvelope(terminal);
94
- await emitTerminalFriction({ envelope: terminal });
98
+ await emitCloseTerminalSignals({ envelope: terminal });
95
99
  return exitCodeForTerminal(terminal);
96
100
  }
97
101
  }
@@ -130,6 +134,10 @@ runAsCli(import.meta.url, main, {
130
134
  // forbids that literal outside `phases/auto-merge.js`.
131
135
  'Land despite a Story-scope code-review CRITICAL blocker you have reviewed and judged wrong. The reason is mandatory (≥12 chars) and is recorded on the Story, on the PR, and as a `review-block-overridden` friction signal; the terminal envelope reports `gates.codeReview: "overridden"`. Use this instead of merging the PR by hand with the GitHub CLI — a hand-merge bypasses the gate with no record at all.',
132
136
  ],
137
+ [
138
+ '--worker-tokens <n>',
139
+ 'The story-worker’s host-reported total tokens, recorded in the close result’s `telemetry.workerTokens`. Best-effort: an absent or non-integer value records null with a warning and never changes the status or exit code.',
140
+ ],
133
141
  ],
134
142
  notes: [
135
143
  'Exit codes:\n 0 landed\n 1 blocked or failed\n 3 pending (resumable — run the envelope’s nextCommand)',