mandrel 2.64.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 +8 -7
- package/.agents/agents/auditor.md +20 -20
- package/.agents/agents/plan-critic.md +8 -7
- package/.agents/agents/story-worker.md +7 -7
- package/.agents/audit-checklists/quality.md +3 -0
- package/.agents/docs/agentrc-reference.json +1 -9
- package/.agents/docs/configuration.md +8 -7
- 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/schemas/agentrc.schema.json +6 -13
- package/.agents/schemas/audit-rules.schema.json +1 -1
- package/.agents/schemas/story-deliver-terminal.schema.json +5 -0
- package/.agents/scripts/bootstrap.js +102 -91
- package/.agents/scripts/check-context-budget.js +1 -1
- package/.agents/scripts/lib/ITicketingProvider.js +1 -3
- package/.agents/scripts/lib/audit-suite/findings.js +1 -17
- package/.agents/scripts/lib/audit-suite/frontmatter.js +0 -28
- package/.agents/scripts/lib/audit-suite/index.js +0 -6
- package/.agents/scripts/lib/audit-suite/selector.js +0 -31
- package/.agents/scripts/lib/baselines/duplication-scanner.js +17 -7
- package/.agents/scripts/lib/bootstrap/agents-md-fold.js +156 -0
- package/.agents/scripts/lib/bootstrap/commit-push.js +1 -1
- package/.agents/scripts/lib/bootstrap/manifest.js +2 -2
- package/.agents/scripts/lib/bootstrap/project-bootstrap.js +91 -107
- 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/config/review-chain-default.js +13 -0
- package/.agents/scripts/lib/config-settings-schema-delivery.js +2 -2
- package/.agents/scripts/lib/config-settings-schema-quality.js +11 -13
- package/.agents/scripts/lib/doc-tiers.js +25 -6
- 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/generated/agentrc-validator.js +1 -1
- package/.agents/scripts/lib/observability/close-telemetry.js +330 -0
- package/.agents/scripts/lib/observability/metrics-ledger.js +0 -72
- 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 +33 -6
- package/.agents/scripts/lib/orchestration/epic-rollup.js +29 -12
- package/.agents/scripts/lib/orchestration/merge-block-class.js +20 -4
- package/.agents/scripts/lib/orchestration/merge-poll.js +41 -22
- package/.agents/scripts/lib/orchestration/plan-metrics.js +76 -63
- package/.agents/scripts/lib/orchestration/required-checks.js +147 -0
- package/.agents/scripts/lib/orchestration/review-providers/code-review.js +203 -0
- package/.agents/scripts/lib/orchestration/review-providers/review-provider-factory.js +29 -4
- package/.agents/scripts/lib/orchestration/review-providers/security-review.js +3 -2
- package/.agents/scripts/lib/orchestration/run-epilogue.js +6 -0
- package/.agents/scripts/lib/orchestration/single-story-close/failed-terminal.js +1 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/code-review.js +2 -12
- package/.agents/scripts/lib/orchestration/single-story-close/phases/confirm-merge.js +370 -268
- package/.agents/scripts/lib/orchestration/single-story-close/phases/options.js +21 -7
- package/.agents/scripts/lib/orchestration/single-story-close/phases/post-land.js +112 -82
- package/.agents/scripts/lib/orchestration/single-story-close/phases/review-override.js +4 -0
- package/.agents/scripts/lib/orchestration/single-story-close/runner.js +393 -313
- package/.agents/scripts/lib/orchestration/story-close/phases/review-core.js +12 -87
- package/.agents/scripts/lib/orchestration/story-deliver-terminal.js +3 -0
- package/.agents/scripts/lib/orchestration/ticket-validator.js +19 -36
- package/.agents/scripts/lib/signals/detectors/common.js +63 -51
- package/.agents/scripts/lib/templates/decomposer-prompts.js +5 -24
- package/.agents/scripts/lib/transpile.js +28 -3
- package/.agents/scripts/providers/github/issues.js +14 -23
- package/.agents/scripts/single-story-close.js +10 -2
- package/.agents/scripts/single-story-confirm-merge.js +267 -238
- package/.agents/scripts/sync-claude-agents.js +1 -1
- 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/audit-quality.md +42 -7
- package/.agents/workflows/helpers/acceptance-self-eval.md +9 -9
- package/.agents/workflows/helpers/audit-lens-core.md +30 -57
- package/.agents/workflows/helpers/code-review.md +15 -38
- package/.agents/workflows/helpers/deliver-digest.md +2 -2
- package/.agents/workflows/helpers/deliver-reference.md +7 -3
- package/.agents/workflows/helpers/deliver-story.md +9 -1
- package/.agents/workflows/helpers/parallel-tooling.md +16 -18
- package/.agents/workflows/helpers/plan-reference.md +9 -8
- package/.agents/workflows/mandrel-deliver.md +3 -2
- package/.agents/workflows/mandrel-plan.md +11 -7
- package/.agents/workflows/mandrel-update.md +5 -3
- package/docs/CHANGELOG.md +57 -0
- package/lib/cli/claude-code-version.js +73 -0
- package/lib/cli/doctor.js +2 -2
- package/lib/cli/guarded-sync.js +87 -0
- package/lib/cli/registry.js +9 -0
- package/lib/cli/sync-agents.js +9 -92
- package/lib/cli/sync-commands.js +9 -101
- package/lib/cli/uninstall.js +37 -9
- package/lib/migrations/index.js +2 -0
- package/lib/migrations/steps/2.65.0-fold-claude-md-into-agents-md.js +38 -0
- package/package.json +3 -2
- package/.agents/scripts/lib/audit-suite/lens-diff-floor.js +0 -99
- package/.agents/scripts/lib/audit-suite/runner.js +0 -205
- package/.agents/scripts/lib/audit-suite/substitutions.js +0 -96
- package/.agents/scripts/lib/audit-suite/workflow-loader.js +0 -37
- package/.agents/scripts/lib/orchestration/story-close/phases/local-lens-review.js +0 -234
|
@@ -10,7 +10,7 @@ import fs from 'node:fs';
|
|
|
10
10
|
import path from 'node:path';
|
|
11
11
|
import readline from 'node:readline/promises';
|
|
12
12
|
import { fileURLToPath } from 'node:url';
|
|
13
|
-
|
|
13
|
+
import { stageLegacyEntryDocRemoval } from './lib/bootstrap/agents-md-fold.js';
|
|
14
14
|
import {
|
|
15
15
|
buildManualInstructions,
|
|
16
16
|
COMMIT_SUBJECT,
|
|
@@ -716,6 +716,13 @@ async function detectCreation(answers, skipGithub) {
|
|
|
716
716
|
return creation;
|
|
717
717
|
}
|
|
718
718
|
|
|
719
|
+
function projectNameFor(pn, projects) {
|
|
720
|
+
const match = projects.find((p) => p.value === pn);
|
|
721
|
+
if (!match) return '(unknown)';
|
|
722
|
+
const m = /^(.*)\s+\(#\d+\)$/.exec(match.label);
|
|
723
|
+
return m ? m[1] : match.label;
|
|
724
|
+
}
|
|
725
|
+
|
|
719
726
|
/**
|
|
720
727
|
* `{ name, number }` for the summary; the picker stores only the number, so
|
|
721
728
|
* an existing project's name is looked up.
|
|
@@ -723,20 +730,78 @@ async function detectCreation(answers, skipGithub) {
|
|
|
723
730
|
function resolveProjectDisplay(answers, skipGithub, projectsList) {
|
|
724
731
|
const pn = answers.projectNumber;
|
|
725
732
|
if (!pn) return { name: '(skip)', number: '(skip)' };
|
|
726
|
-
if (
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
733
|
+
if (!/^\d+$/.test(pn)) return { name: pn, number: '(new)' };
|
|
734
|
+
if (skipGithub) return { name: '(unknown)', number: pn };
|
|
735
|
+
const projects =
|
|
736
|
+
projectsList ?? safeList(() => listProjects({ owner: answers.owner }));
|
|
737
|
+
return { name: projectNameFor(pn, projects), number: pn };
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
/** Opt-ins default off; dry-run resolves them without prompting. */
|
|
741
|
+
const OPT_INS = Object.freeze([
|
|
742
|
+
{
|
|
743
|
+
key: 'withProjectBoard',
|
|
744
|
+
flag: 'with-project-board',
|
|
745
|
+
prompt: 'Set up project board fields (Status, custom)?',
|
|
746
|
+
},
|
|
747
|
+
{
|
|
748
|
+
key: 'withIssueForms',
|
|
749
|
+
flag: 'with-issue-forms',
|
|
750
|
+
prompt: 'Generate GitHub Issue Form templates?',
|
|
751
|
+
},
|
|
752
|
+
{
|
|
753
|
+
key: 'withQuality',
|
|
754
|
+
flag: 'with-quality',
|
|
755
|
+
prompt:
|
|
756
|
+
'Install local quality gates (pre-commit hook + quality:preview/watch scripts)?',
|
|
757
|
+
},
|
|
758
|
+
]);
|
|
759
|
+
|
|
760
|
+
async function resolveOptIns(state) {
|
|
761
|
+
const optIns = {};
|
|
762
|
+
for (const { key, flag, prompt } of OPT_INS) {
|
|
763
|
+
let on = Boolean(state.flags[flag]);
|
|
764
|
+
if (!state.flags['dry-run'] && !on) {
|
|
765
|
+
on = await confirmYesNo(prompt, state.interactive, false);
|
|
736
766
|
}
|
|
737
|
-
|
|
767
|
+
optIns[key] = on;
|
|
768
|
+
}
|
|
769
|
+
return optIns;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
async function approveCreation(state, creation) {
|
|
773
|
+
if (state.flags['dry-run'] || !(creation.newRepo || creation.newProject)) {
|
|
774
|
+
return true;
|
|
738
775
|
}
|
|
739
|
-
return
|
|
776
|
+
return confirmYesNo(
|
|
777
|
+
'Create the new GitHub repo/project listed above?',
|
|
778
|
+
state.interactive,
|
|
779
|
+
);
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
async function confirmSummary(state, answers, creation, projectsList) {
|
|
783
|
+
const skipGithub = Boolean(state.flags['skip-github']);
|
|
784
|
+
const project = resolveProjectDisplay(answers, skipGithub, projectsList);
|
|
785
|
+
Logger.info(
|
|
786
|
+
renderAnswerSummary(
|
|
787
|
+
answers,
|
|
788
|
+
creation,
|
|
789
|
+
project,
|
|
790
|
+
state.gitInitialized,
|
|
791
|
+
resolveRepoVisibility(state.flags),
|
|
792
|
+
),
|
|
793
|
+
);
|
|
794
|
+
return confirmYesNo('Is this correct?', state.interactive);
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
/** Owner repo/project lists, fetched once for pickers and summary. */
|
|
798
|
+
function fetchPickerLists(state, skipGithub) {
|
|
799
|
+
const owner = resolveOwnerForPicker(state.defaults, state.flags);
|
|
800
|
+
if (skipGithub || !owner) return { reposList: [], projectsList: [] };
|
|
801
|
+
return {
|
|
802
|
+
reposList: safeList(() => listRepos({ owner }).map(bareRepoName)),
|
|
803
|
+
projectsList: safeList(() => listProjects({ owner })),
|
|
804
|
+
};
|
|
740
805
|
}
|
|
741
806
|
|
|
742
807
|
/**
|
|
@@ -745,22 +810,16 @@ function resolveProjectDisplay(answers, skipGithub, projectsList) {
|
|
|
745
810
|
*/
|
|
746
811
|
export async function collectAndConfirm(state) {
|
|
747
812
|
const skipGithub = Boolean(state.flags['skip-github']);
|
|
748
|
-
const
|
|
749
|
-
// Fetched once for pickers and summary, so the name never needs a second call.
|
|
750
|
-
const reposList =
|
|
751
|
-
!skipGithub && owner
|
|
752
|
-
? safeList(() => listRepos({ owner }).map(bareRepoName))
|
|
753
|
-
: [];
|
|
754
|
-
const projectsList =
|
|
755
|
-
!skipGithub && owner ? safeList(() => listProjects({ owner })) : [];
|
|
756
|
-
|
|
813
|
+
const lists = fetchPickerLists(state, skipGithub);
|
|
757
814
|
let silentAccept = state.silentAccept;
|
|
758
815
|
for (;;) {
|
|
759
816
|
const { answers, missing } = await collectAnswers({
|
|
760
|
-
questions: buildQuestions(
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
817
|
+
questions: buildQuestions(
|
|
818
|
+
state.defaults,
|
|
819
|
+
state.flags,
|
|
820
|
+
process.env,
|
|
821
|
+
lists,
|
|
822
|
+
),
|
|
764
823
|
flags: state.flags,
|
|
765
824
|
interactive: state.interactive,
|
|
766
825
|
assumeYes: state.assumeYes,
|
|
@@ -775,78 +834,24 @@ export async function collectAndConfirm(state) {
|
|
|
775
834
|
);
|
|
776
835
|
return { ok: false, exit: 1 };
|
|
777
836
|
}
|
|
778
|
-
|
|
779
|
-
|
|
837
|
+
answers.operatorHandle = normalizeHandleAnswer(
|
|
838
|
+
answers.operatorHandle || answers.owner,
|
|
839
|
+
);
|
|
780
840
|
|
|
781
841
|
const creation = await detectCreation(answers, skipGithub);
|
|
782
|
-
|
|
783
|
-
Logger.info(
|
|
784
|
-
renderAnswerSummary(
|
|
785
|
-
answers,
|
|
786
|
-
creation,
|
|
787
|
-
project,
|
|
788
|
-
state.gitInitialized,
|
|
789
|
-
resolveRepoVisibility(state.flags),
|
|
790
|
-
),
|
|
791
|
-
);
|
|
792
|
-
const correct = await confirmYesNo('Is this correct?', state.interactive);
|
|
793
|
-
if (!correct) {
|
|
842
|
+
if (!(await confirmSummary(state, answers, creation, lists.projectsList))) {
|
|
794
843
|
Logger.info('[Bootstrap] Okay — let’s try again.');
|
|
795
844
|
silentAccept = [];
|
|
796
845
|
continue;
|
|
797
846
|
}
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
'Create the new GitHub repo/project listed above?',
|
|
802
|
-
state.interactive,
|
|
803
|
-
);
|
|
804
|
-
if (!approved) {
|
|
805
|
-
Logger.error(
|
|
806
|
-
'[Bootstrap] Creation declined — cannot continue without the repo/project. Exiting.',
|
|
807
|
-
);
|
|
808
|
-
return { ok: false, exit: 1 };
|
|
809
|
-
}
|
|
810
|
-
}
|
|
811
|
-
|
|
812
|
-
// Opt-ins default off; dry-run resolves them without prompting.
|
|
813
|
-
let withProjectBoard = Boolean(state.flags['with-project-board']);
|
|
814
|
-
if (!state.flags['dry-run'] && !withProjectBoard) {
|
|
815
|
-
withProjectBoard = await confirmYesNo(
|
|
816
|
-
'Set up project board fields (Status, custom)?',
|
|
817
|
-
state.interactive,
|
|
818
|
-
false,
|
|
819
|
-
);
|
|
820
|
-
}
|
|
821
|
-
|
|
822
|
-
let withIssueForms = Boolean(state.flags['with-issue-forms']);
|
|
823
|
-
if (!state.flags['dry-run'] && !withIssueForms) {
|
|
824
|
-
withIssueForms = await confirmYesNo(
|
|
825
|
-
'Generate GitHub Issue Form templates?',
|
|
826
|
-
state.interactive,
|
|
827
|
-
false,
|
|
828
|
-
);
|
|
829
|
-
}
|
|
830
|
-
|
|
831
|
-
let withQuality = Boolean(state.flags['with-quality']);
|
|
832
|
-
if (!state.flags['dry-run'] && !withQuality) {
|
|
833
|
-
withQuality = await confirmYesNo(
|
|
834
|
-
'Install local quality gates (pre-commit hook + quality:preview/watch scripts)?',
|
|
835
|
-
state.interactive,
|
|
836
|
-
false,
|
|
847
|
+
if (!(await approveCreation(state, creation))) {
|
|
848
|
+
Logger.error(
|
|
849
|
+
'[Bootstrap] Creation declined — cannot continue without the repo/project. Exiting.',
|
|
837
850
|
);
|
|
851
|
+
return { ok: false, exit: 1 };
|
|
838
852
|
}
|
|
839
|
-
|
|
840
|
-
return {
|
|
841
|
-
ok: true,
|
|
842
|
-
payload: {
|
|
843
|
-
answers,
|
|
844
|
-
creation,
|
|
845
|
-
withProjectBoard,
|
|
846
|
-
withIssueForms,
|
|
847
|
-
withQuality,
|
|
848
|
-
},
|
|
849
|
-
};
|
|
853
|
+
const optIns = await resolveOptIns(state);
|
|
854
|
+
return { ok: true, payload: { answers, creation, ...optIns } };
|
|
850
855
|
}
|
|
851
856
|
}
|
|
852
857
|
|
|
@@ -1068,7 +1073,13 @@ export async function offerCommitPush(state, deps = {}) {
|
|
|
1068
1073
|
return { ok: true, payload: { commitPush: { action: 'declined' } } };
|
|
1069
1074
|
}
|
|
1070
1075
|
|
|
1071
|
-
const
|
|
1076
|
+
const added = stageBootstrapFiles({ projectRoot: cwd, runGit: runGitImpl });
|
|
1077
|
+
// The folded-away CLAUDE.md deletion rides the same commit.
|
|
1078
|
+
const staged = stageLegacyEntryDocRemoval({
|
|
1079
|
+
projectRoot: cwd,
|
|
1080
|
+
runGit: runGitImpl,
|
|
1081
|
+
after: added,
|
|
1082
|
+
});
|
|
1072
1083
|
if (!staged.ok) {
|
|
1073
1084
|
Logger.warn(`[Bootstrap] Could not stage the wiring: ${staged.error}`);
|
|
1074
1085
|
Logger.info(`\n[Bootstrap] ${instructions}`);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* CLI: ratchet gate for documentation context bytes against
|
|
3
|
-
* `baselines/context-budget.json`. Measures `alwaysLoaded` (the
|
|
3
|
+
* `baselines/context-budget.json`. Measures `alwaysLoaded` (the entry-doc
|
|
4
4
|
* `@`-import closure), `mandatoryRead` (`project.docsContextFiles`) and
|
|
5
5
|
* `workflow` (entry points + `mandatoryReads:` closure).
|
|
6
6
|
*
|
|
@@ -105,9 +105,7 @@ export class ITicketingProvider {
|
|
|
105
105
|
}
|
|
106
106
|
|
|
107
107
|
/**
|
|
108
|
-
*
|
|
109
|
-
* there is no parent or the backend cannot answer, so a degraded lookup
|
|
110
|
-
* never fails a lifecycle edge.
|
|
108
|
+
* `null` means "no parent"; throws when the lookup cannot answer.
|
|
111
109
|
*
|
|
112
110
|
* @param {number} _number
|
|
113
111
|
* @returns {Promise<object|null>}
|
|
@@ -1,27 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* The surviving-Critical halting rule and per-component baseline rollup deltas.
|
|
3
3
|
* Deltas are per rollup, not per row: row churn that moves no rollup is noise.
|
|
4
4
|
* Pure, no I/O.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { groupRows, resolveComponents } from '../baselines/components.js';
|
|
8
8
|
|
|
9
|
-
/**
|
|
10
|
-
* Non-standard severities are ignored.
|
|
11
|
-
*
|
|
12
|
-
* @param {Array<{ severity?: string }>|null|undefined} findings
|
|
13
|
-
* @returns {{ critical: number, high: number, medium: number, low: number }}
|
|
14
|
-
*/
|
|
15
|
-
export function aggregateSummary(findings) {
|
|
16
|
-
const summary = { critical: 0, high: 0, medium: 0, low: 0 };
|
|
17
|
-
for (const finding of findings ?? []) {
|
|
18
|
-
if (Object.hasOwn(summary, finding.severity)) {
|
|
19
|
-
summary[finding.severity] += 1;
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
return summary;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
9
|
/**
|
|
26
10
|
* The single halting rule: a surviving Critical finding halts delivery. Every
|
|
27
11
|
* consumer routes through this rather than re-deriving `critical > 0`.
|
|
@@ -8,8 +8,6 @@
|
|
|
8
8
|
// biome-ignore-start lint/complexity/useRegexLiterals: typhonjs-escomplex MI workaround
|
|
9
9
|
const FRONTMATTER_RE = new RegExp(String.raw`^---\r?\n([\s\S]*?)\r?\n---\r?\n`);
|
|
10
10
|
const NEWLINE_SPLIT_RE = new RegExp(String.raw`\r?\n`);
|
|
11
|
-
const PARAGRAPH_SPLIT_RE = new RegExp(String.raw`\r?\n\s*\r?\n`);
|
|
12
|
-
const COLLAPSE_WS_RE = new RegExp(String.raw`\s+`, 'g');
|
|
13
11
|
const SENTENCE_RE = new RegExp(String.raw`[^.!?\n]+[.!?]+`, 'g');
|
|
14
12
|
// biome-ignore-end lint/complexity/useRegexLiterals: typhonjs-escomplex MI workaround
|
|
15
13
|
|
|
@@ -42,22 +40,6 @@ export function extractFrontmatter(content) {
|
|
|
42
40
|
return fm;
|
|
43
41
|
}
|
|
44
42
|
|
|
45
|
-
/**
|
|
46
|
-
* @param {string} content
|
|
47
|
-
* @returns {string}
|
|
48
|
-
*/
|
|
49
|
-
export function firstProseParagraph(content) {
|
|
50
|
-
const stripped = content.replace(FRONTMATTER_RE, '');
|
|
51
|
-
for (const block of stripped.split(PARAGRAPH_SPLIT_RE)) {
|
|
52
|
-
const trimmed = block.trim();
|
|
53
|
-
if (!trimmed) continue;
|
|
54
|
-
if (trimmed.startsWith('#')) continue;
|
|
55
|
-
if (trimmed.startsWith('---')) continue;
|
|
56
|
-
return trimmed.replace(COLLAPSE_WS_RE, ' ');
|
|
57
|
-
}
|
|
58
|
-
return '';
|
|
59
|
-
}
|
|
60
|
-
|
|
61
43
|
/**
|
|
62
44
|
* @param {string} text
|
|
63
45
|
* @returns {string}
|
|
@@ -74,13 +56,3 @@ export function clampSummary(text) {
|
|
|
74
56
|
}
|
|
75
57
|
return result;
|
|
76
58
|
}
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* @param {string} content
|
|
80
|
-
* @returns {string}
|
|
81
|
-
*/
|
|
82
|
-
export function summarizeWorkflow(content) {
|
|
83
|
-
const fm = extractFrontmatter(content);
|
|
84
|
-
const candidate = fm.description?.trim() || firstProseParagraph(content);
|
|
85
|
-
return clampSummary(candidate);
|
|
86
|
-
}
|
|
@@ -7,16 +7,10 @@ export {
|
|
|
7
7
|
readAuditRules,
|
|
8
8
|
} from './checklist-threading.js';
|
|
9
9
|
export { buildDispatchChecklist } from './dispatch-checklist.js';
|
|
10
|
-
export {
|
|
11
|
-
countChangedLines,
|
|
12
|
-
evaluateLensDiffFloor,
|
|
13
|
-
} from './lens-diff-floor.js';
|
|
14
|
-
export { runAuditSuite } from './runner.js';
|
|
15
10
|
export {
|
|
16
11
|
LENS_TIERS,
|
|
17
12
|
matchesAnyFilePattern,
|
|
18
13
|
matchesFilePattern,
|
|
19
14
|
resolveLensTier,
|
|
20
15
|
selectAudits,
|
|
21
|
-
selectLocalLenses,
|
|
22
16
|
} from './selector.js';
|
|
@@ -45,37 +45,6 @@ export function resolveLensTier(lens) {
|
|
|
45
45
|
return scope;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
/**
|
|
49
|
-
* Local-tier lenses whose `filePatterns` hit the change set (not
|
|
50
|
-
* {@link selectAudits}, which would widen it with keyword matches).
|
|
51
|
-
*
|
|
52
|
-
* @param {{
|
|
53
|
-
* changedFiles?: string[],
|
|
54
|
-
* injectedRules?: { audits?: Record<string, object> },
|
|
55
|
-
* resolveLensTierFn?: typeof resolveLensTier,
|
|
56
|
-
* }} [params]
|
|
57
|
-
* @returns {string[]} The matched local-lens identifiers, in manifest order.
|
|
58
|
-
*/
|
|
59
|
-
export function selectLocalLenses({
|
|
60
|
-
changedFiles,
|
|
61
|
-
injectedRules,
|
|
62
|
-
resolveLensTierFn = resolveLensTier,
|
|
63
|
-
} = {}) {
|
|
64
|
-
const files = Array.isArray(changedFiles) ? changedFiles : [];
|
|
65
|
-
if (files.length === 0) return [];
|
|
66
|
-
|
|
67
|
-
const rules = injectedRules ?? readAuditRulesSync();
|
|
68
|
-
const selected = [];
|
|
69
|
-
for (const [lens, entry] of Object.entries(rules.audits ?? {})) {
|
|
70
|
-
if (resolveLensTierFn(lens) !== 'local') continue;
|
|
71
|
-
const patterns = entry?.triggers?.filePatterns ?? [];
|
|
72
|
-
if (matchesAnyFilePattern(patterns, files)) {
|
|
73
|
-
selected.push(lens);
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
return selected;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
48
|
/**
|
|
80
49
|
* Manifest `sensitivePaths` classes the change set touches (review depth).
|
|
81
50
|
*
|
|
@@ -5,24 +5,33 @@
|
|
|
5
5
|
import { readFileSync } from 'node:fs';
|
|
6
6
|
import { createRequire } from 'node:module';
|
|
7
7
|
import path from 'node:path';
|
|
8
|
+
import { resolveDependencyVersion } from '../dependency-version.js';
|
|
8
9
|
|
|
9
10
|
const DEFAULT_MIN_TOKENS = 50;
|
|
10
11
|
const DEFAULT_FORMATS = Object.freeze(['javascript']);
|
|
11
12
|
|
|
12
13
|
const require = createRequire(import.meta.url);
|
|
13
14
|
|
|
15
|
+
/** jscpd 5 is a Rust rewrite with no Node API; only major 4 exposes one. */
|
|
16
|
+
const SUPPORTED_JSCPD_MAJOR = '^4';
|
|
17
|
+
|
|
14
18
|
/**
|
|
15
19
|
* Lazy CJS load: jscpd's ESM entry has a broken transitive `colors/safe`
|
|
16
20
|
* specifier under strict ESM resolution, and importers that never scan should
|
|
17
21
|
* not pay the load.
|
|
18
22
|
*
|
|
23
|
+
* No `detectClones` means an unsupported major, not a missing install.
|
|
24
|
+
*
|
|
25
|
+
* @param {NodeJS.Require} [requireFn] substitutes the module resolver
|
|
19
26
|
* @returns {(opts: object) => Promise<Array<object>>}
|
|
20
27
|
*/
|
|
21
|
-
export function resolveDetectClones() {
|
|
22
|
-
const jscpd =
|
|
23
|
-
if (typeof jscpd
|
|
28
|
+
export function resolveDetectClones(requireFn = require) {
|
|
29
|
+
const jscpd = requireFn('jscpd');
|
|
30
|
+
if (typeof jscpd?.detectClones !== 'function') {
|
|
31
|
+
const version = resolveDependencyVersion('jscpd', requireFn) ?? 'unknown';
|
|
24
32
|
throw new Error(
|
|
25
|
-
|
|
33
|
+
`[Duplication] jscpd ${version} exposes no detectClones Node API — ` +
|
|
34
|
+
`the duplication gate supports jscpd ${SUPPORTED_JSCPD_MAJOR}; install jscpd@${SUPPORTED_JSCPD_MAJOR}`,
|
|
26
35
|
);
|
|
27
36
|
}
|
|
28
37
|
return jscpd.detectClones;
|
|
@@ -44,6 +53,9 @@ export function relativisePath(sourceId, cwd) {
|
|
|
44
53
|
}
|
|
45
54
|
|
|
46
55
|
/**
|
|
56
|
+
* A side with `end.line < start.line` counts nothing: its real span is
|
|
57
|
+
* unrecoverable, and widening it recorded hundreds of phantom lines.
|
|
58
|
+
*
|
|
47
59
|
* @param {{ start?: { line?: number }, end?: { line?: number } }} dup
|
|
48
60
|
* @returns {Array<number>} the 1-based line numbers the clone covers
|
|
49
61
|
*/
|
|
@@ -51,10 +63,8 @@ function cloneLineNumbers(dup) {
|
|
|
51
63
|
const start = dup?.start?.line;
|
|
52
64
|
const end = dup?.end?.line;
|
|
53
65
|
if (!Number.isInteger(start) || !Number.isInteger(end)) return [];
|
|
54
|
-
const lo = Math.min(start, end);
|
|
55
|
-
const hi = Math.max(start, end);
|
|
56
66
|
const lines = [];
|
|
57
|
-
for (let n =
|
|
67
|
+
for (let n = start; n <= end; n += 1) lines.push(n);
|
|
58
68
|
return lines;
|
|
59
69
|
}
|
|
60
70
|
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLAUDE.md → AGENTS.md fold for bootstrap and the update migration: a
|
|
3
|
+
* surviving CLAUDE.md would shadow AGENTS.md, since the host prefers it.
|
|
4
|
+
*
|
|
5
|
+
* @module bootstrap/agents-md-fold
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import fs from 'node:fs';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
|
|
11
|
+
/** Entry-doc wiring keys idempotence off this exact import path. */
|
|
12
|
+
export const SYSTEM_PROMPT_IMPORT = '@.agents/instructions.md';
|
|
13
|
+
|
|
14
|
+
export const SYSTEM_PROMPT_BLOCK = `## System Prompt
|
|
15
|
+
|
|
16
|
+
${SYSTEM_PROMPT_IMPORT}
|
|
17
|
+
`;
|
|
18
|
+
|
|
19
|
+
/** Install template for a fresh entry doc. */
|
|
20
|
+
export const SYSTEM_PROMPT_ENTRY_DOC = `# Agent Protocols
|
|
21
|
+
|
|
22
|
+
${SYSTEM_PROMPT_BLOCK}`;
|
|
23
|
+
|
|
24
|
+
const ENTRY_DOC = 'AGENTS.md';
|
|
25
|
+
export const LEGACY_ENTRY_DOC = 'CLAUDE.md';
|
|
26
|
+
|
|
27
|
+
const SELF_IMPORT = '@AGENTS.md';
|
|
28
|
+
|
|
29
|
+
/** @type {readonly string[]} */
|
|
30
|
+
const BOOTSTRAP_REMOVED_PATHS = Object.freeze([LEGACY_ENTRY_DOC]);
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @param {string} text
|
|
34
|
+
* @returns {string}
|
|
35
|
+
*/
|
|
36
|
+
function withTrailingNewline(text) {
|
|
37
|
+
return text.length === 0 || text.endsWith('\n') ? text : `${text}\n`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Keep the first system-prompt import line, drop every later one.
|
|
42
|
+
*
|
|
43
|
+
* @param {string[]} lines
|
|
44
|
+
* @returns {string[]}
|
|
45
|
+
*/
|
|
46
|
+
function dedupeImport(lines) {
|
|
47
|
+
let seen = false;
|
|
48
|
+
return lines.filter((line) => {
|
|
49
|
+
if (line.trim() !== SYSTEM_PROMPT_IMPORT) return true;
|
|
50
|
+
if (seen) return false;
|
|
51
|
+
seen = true;
|
|
52
|
+
return true;
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* `agents` null means AGENTS.md is absent.
|
|
58
|
+
*
|
|
59
|
+
* @param {{ claude: string, agents?: string|null }} input
|
|
60
|
+
* @returns {string} the resulting AGENTS.md content
|
|
61
|
+
*/
|
|
62
|
+
function foldEntryDocs({ claude, agents = null }) {
|
|
63
|
+
const claudeBody = claude
|
|
64
|
+
.split('\n')
|
|
65
|
+
.filter((line) => line.trim() !== SELF_IMPORT)
|
|
66
|
+
.join('\n');
|
|
67
|
+
const combined =
|
|
68
|
+
typeof agents === 'string'
|
|
69
|
+
? `${withTrailingNewline(agents)}\n${claudeBody}`
|
|
70
|
+
: claudeBody;
|
|
71
|
+
const lines = dedupeImport(combined.split('\n'));
|
|
72
|
+
let result = withTrailingNewline(lines.join('\n'));
|
|
73
|
+
if (!lines.some((line) => line.trim() === SYSTEM_PROMPT_IMPORT)) {
|
|
74
|
+
result =
|
|
75
|
+
result.trim().length === 0
|
|
76
|
+
? SYSTEM_PROMPT_ENTRY_DOC
|
|
77
|
+
: `${result}\n${SYSTEM_PROMPT_BLOCK}`;
|
|
78
|
+
}
|
|
79
|
+
return result;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Fold a root CLAUDE.md into AGENTS.md on disk and delete CLAUDE.md. No-op
|
|
84
|
+
* when CLAUDE.md is absent, so a second pass mutates nothing.
|
|
85
|
+
*
|
|
86
|
+
* @param {string} projectRoot
|
|
87
|
+
* @param {typeof fs} [fsImpl]
|
|
88
|
+
* @returns {{ action: 'folded'|'none', path: string }}
|
|
89
|
+
*/
|
|
90
|
+
export function foldClaudeMdIntoAgentsMd(projectRoot, fsImpl = fs) {
|
|
91
|
+
const claudePath = path.join(projectRoot, LEGACY_ENTRY_DOC);
|
|
92
|
+
const agentsPath = path.join(projectRoot, ENTRY_DOC);
|
|
93
|
+
if (!fsImpl.existsSync(claudePath)) {
|
|
94
|
+
return { action: 'none', path: agentsPath };
|
|
95
|
+
}
|
|
96
|
+
const claude = fsImpl.readFileSync(claudePath, 'utf8');
|
|
97
|
+
const agents = fsImpl.existsSync(agentsPath)
|
|
98
|
+
? fsImpl.readFileSync(agentsPath, 'utf8')
|
|
99
|
+
: null;
|
|
100
|
+
fsImpl.writeFileSync(agentsPath, foldEntryDocs({ claude, agents }), 'utf8');
|
|
101
|
+
fsImpl.rmSync(claudePath, { force: true });
|
|
102
|
+
return { action: 'folded', path: agentsPath };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Wire the system-prompt import into AGENTS.md, folding a CLAUDE.md in first.
|
|
107
|
+
* Creates, folds, appends, or no-ops.
|
|
108
|
+
*
|
|
109
|
+
* @param {string} projectRoot
|
|
110
|
+
* @param {typeof fs} [fsImpl]
|
|
111
|
+
* @returns {{ action: 'created'|'folded'|'appended'|'already-present', path: string }}
|
|
112
|
+
*/
|
|
113
|
+
export function wireEntryDoc(projectRoot, fsImpl = fs) {
|
|
114
|
+
const folded = foldClaudeMdIntoAgentsMd(projectRoot, fsImpl);
|
|
115
|
+
const target = folded.path;
|
|
116
|
+
if (folded.action === 'folded') return folded;
|
|
117
|
+
if (!fsImpl.existsSync(target)) {
|
|
118
|
+
fsImpl.writeFileSync(target, SYSTEM_PROMPT_ENTRY_DOC, 'utf8');
|
|
119
|
+
return { action: 'created', path: target };
|
|
120
|
+
}
|
|
121
|
+
const existing = fsImpl.readFileSync(target, 'utf8');
|
|
122
|
+
if (existing.includes(SYSTEM_PROMPT_IMPORT)) {
|
|
123
|
+
return { action: 'already-present', path: target };
|
|
124
|
+
}
|
|
125
|
+
const separator = existing.length > 0 && !existing.endsWith('\n') ? '\n' : '';
|
|
126
|
+
fsImpl.writeFileSync(
|
|
127
|
+
target,
|
|
128
|
+
`${existing}${separator}\n${SYSTEM_PROMPT_BLOCK}`,
|
|
129
|
+
'utf8',
|
|
130
|
+
);
|
|
131
|
+
return { action: 'appended', path: target };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Stage removal of folded-away entry docs; a failed `after` passes through.
|
|
136
|
+
*
|
|
137
|
+
* @param {{ after?: { ok: boolean, error?: string }, projectRoot: string, runGit: (args: string[], cwd: string) => { ok: boolean, stderr?: string }, fsImpl?: typeof fs }} args
|
|
138
|
+
* @returns {{ ok: boolean, error?: string, removed: string[] }}
|
|
139
|
+
*/
|
|
140
|
+
export function stageLegacyEntryDocRemoval({
|
|
141
|
+
projectRoot,
|
|
142
|
+
runGit,
|
|
143
|
+
fsImpl = fs,
|
|
144
|
+
after = { ok: true },
|
|
145
|
+
}) {
|
|
146
|
+
if (!after.ok) return after;
|
|
147
|
+
const removed = BOOTSTRAP_REMOVED_PATHS.filter(
|
|
148
|
+
(rel) => !fsImpl.existsSync(path.join(projectRoot, rel)),
|
|
149
|
+
);
|
|
150
|
+
if (removed.length === 0) return { ok: true, removed };
|
|
151
|
+
const result = runGit(
|
|
152
|
+
['rm', '--cached', '--ignore-unmatch', '--quiet', '--', ...removed],
|
|
153
|
+
projectRoot,
|
|
154
|
+
);
|
|
155
|
+
return { ok: result.ok, error: result.stderr || 'git rm failed', removed };
|
|
156
|
+
}
|
|
@@ -62,10 +62,10 @@ export function buildMutationManifest(ctx = {}) {
|
|
|
62
62
|
entries.push(
|
|
63
63
|
{
|
|
64
64
|
phaseGroup: PHASE_GROUPS.IDE_WIRING,
|
|
65
|
-
target: rel('
|
|
65
|
+
target: rel('AGENTS.md'),
|
|
66
66
|
action: 'merge',
|
|
67
67
|
detail:
|
|
68
|
-
'Wire the @.agents/instructions.md system-prompt import so Claude Code hydrates the framework on cold start.',
|
|
68
|
+
'Wire the @.agents/instructions.md system-prompt import into AGENTS.md (folding any CLAUDE.md into it) so Claude Code hydrates the framework on cold start.',
|
|
69
69
|
reversible: true,
|
|
70
70
|
},
|
|
71
71
|
{
|