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.
- package/.agents/agents/acceptance-critic.md +5 -5
- package/.agents/agents/auditor.md +17 -18
- package/.agents/agents/plan-critic.md +5 -5
- package/.agents/agents/story-worker.md +5 -5
- package/.agents/docs/execution-reference.md +27 -5
- package/.agents/instructions.md +10 -12
- package/.agents/rules/ci-remediation.md +3 -3
- package/.agents/rules/gherkin-standards.md +3 -2
- package/.agents/rules/git-conventions-reference.md +12 -3
- package/.agents/rules/git-conventions.md +9 -7
- package/.agents/rules/testing-standards.md +8 -7
- package/.agents/runtime-deps.json +1 -1
- package/.agents/scripts/bootstrap.js +94 -89
- package/.agents/scripts/lib/baselines/duplication-scanner.js +17 -7
- package/.agents/scripts/lib/bootstrap/project-bootstrap.js +78 -78
- package/.agents/scripts/lib/cli/standard-args.js +60 -76
- package/.agents/scripts/lib/cli-args.js +26 -0
- package/.agents/scripts/lib/config/gates/shared.js +3 -3
- package/.agents/scripts/lib/feedback-loop/graduate-steps.js +205 -0
- package/.agents/scripts/lib/feedback-loop/graduator-core.js +47 -782
- package/.agents/scripts/lib/feedback-loop/graduator-gh.js +449 -0
- package/.agents/scripts/lib/observability/close-telemetry.js +330 -0
- package/.agents/scripts/lib/observability/runtime-friction.js +2 -0
- package/.agents/scripts/lib/observability/signal-validator.js +17 -5
- package/.agents/scripts/lib/orchestration/code-review.js +22 -0
- package/.agents/scripts/lib/orchestration/plan-metrics.js +76 -63
- package/.agents/scripts/lib/orchestration/review-providers/review-provider-factory.js +23 -0
- package/.agents/scripts/lib/orchestration/run-epilogue.js +6 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/code-review.js +2 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/confirm-merge.js +349 -263
- package/.agents/scripts/lib/orchestration/single-story-close/phases/options.js +21 -7
- package/.agents/scripts/lib/orchestration/single-story-close/phases/review-override.js +4 -0
- package/.agents/scripts/lib/orchestration/single-story-close/runner.js +327 -314
- package/.agents/scripts/lib/orchestration/ticket-validator.js +19 -36
- package/.agents/scripts/lib/signals/detectors/common.js +63 -51
- package/.agents/scripts/lib/transpile.js +28 -3
- package/.agents/scripts/single-story-close.js +10 -2
- package/.agents/scripts/single-story-confirm-merge.js +267 -238
- package/.agents/skills/core/idea-refinement/SKILL.md +6 -6
- package/.agents/skills/stack/qa/qa-harness/SKILL.md +1 -2
- package/.agents/workflows/audit-architecture.md +5 -4
- package/.agents/workflows/audit-documentation.md +5 -5
- package/.agents/workflows/audit-performance.md +10 -10
- package/.agents/workflows/helpers/acceptance-self-eval.md +8 -8
- package/.agents/workflows/helpers/audit-lens-core.md +30 -57
- package/.agents/workflows/helpers/deliver-digest.md +2 -2
- package/.agents/workflows/helpers/deliver-reference.md +3 -1
- package/.agents/workflows/helpers/deliver-story.md +6 -1
- package/.agents/workflows/helpers/parallel-tooling.md +16 -18
- package/.agents/workflows/mandrel-deliver.md +1 -1
- package/.agents/workflows/mandrel-plan.md +6 -5
- package/docs/CHANGELOG.md +26 -0
- package/lib/cli/guarded-sync.js +87 -0
- package/lib/cli/sync-agents.js +9 -92
- package/lib/cli/sync-commands.js +9 -101
- 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
|
|
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
|
-
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
|
55
|
-
|
|
56
|
-
|
|
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 {
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
|
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)',
|