session-orchestrator 4.0.1 → 4.2.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/skills/session-plan/SKILL.md +1 -1
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.codex-plugin/skills/session-plan/SKILL.md +1 -1
- package/.cursor/skills/session-plan/SKILL.md +1 -1
- package/.cursor-plugin/plugin.json +1 -1
- package/CHANGELOG.md +57 -0
- package/README.md +55 -51
- package/agents/ux-evaluator.md +1 -1
- package/commands/close.md +3 -3
- package/commands/go.md +2 -0
- package/commands/memory-cleanup.md +4 -3
- package/commands/persona-panel.md +1 -1
- package/commands/release.md +4 -4
- package/commands/session.md +3 -2
- package/docs/README.md +4 -4
- package/docs/USER-GUIDE.md +115 -48
- package/docs/agent-authoring.md +2 -2
- package/docs/baseline.md +55 -1
- package/docs/ci-setup.md +1 -1
- package/docs/codex-setup.md +1 -0
- package/docs/components.md +2 -2
- package/docs/cursor-setup.md +1 -0
- package/docs/events-schema.md +4 -1
- package/docs/instruction-delivery.md +1 -1
- package/docs/memory-proposal-flow.md +3 -3
- package/docs/migration-v4.md +2 -2
- package/docs/owner-config-schema.md +74 -90
- package/docs/persona-panel.md +4 -4
- package/docs/pi-setup.md +1 -0
- package/docs/rule-authoring.md +13 -6
- package/docs/scope-collision-guard.md +2 -0
- package/docs/session-config-reference.md +55 -22
- package/docs/session-config-template.md +9 -5
- package/docs/vault-docs-architecture.md +4 -2
- package/hooks/_lib/hook-import-set.json +28 -3
- package/hooks/_lib/vcs-create-matcher.mjs +214 -16
- package/hooks/hooks-codex.json +1 -1
- package/hooks/hooks.json +1 -1
- package/hooks/pre-bash-issue-budget.mjs +123 -26
- package/hooks/subagent-telemetry.mjs +106 -20
- package/package.json +4 -4
- package/scripts/baseline-archetypes.mjs +28 -0
- package/scripts/ci/assert-coverage-green.mjs +100 -0
- package/scripts/lib/auto-dialectic.mjs +0 -68
- package/scripts/lib/baseline-archetypes.mjs +439 -0
- package/scripts/lib/build-live-signals.mjs +5 -6
- package/scripts/lib/config/issue-budget.mjs +68 -8
- package/scripts/lib/config/private-config-dir.mjs +3 -2
- package/scripts/lib/config/remote-hosts.mjs +2 -2
- package/scripts/lib/config-schema.mjs +79 -0
- package/scripts/lib/events.mjs +3 -3
- package/scripts/lib/file-lock.mjs +47 -5
- package/scripts/lib/issue-budget-reconcile.mjs +392 -0
- package/scripts/lib/issue-budget.mjs +76 -3
- package/scripts/lib/learnings/evolve-telemetry.mjs +1 -2
- package/scripts/lib/maintenance-due-banner.mjs +440 -0
- package/scripts/lib/owner-config.example.yaml +29 -46
- package/scripts/lib/owner-yaml.mjs +14 -13
- package/scripts/lib/project-hygiene.mjs +182 -6
- package/scripts/lib/quality-gate.mjs +13 -6
- package/scripts/lib/resource-probe/evaluate.mjs +19 -21
- package/scripts/lib/rules-sync.mjs +34 -4
- package/scripts/lib/session-close-backfill.mjs +182 -40
- package/scripts/lib/session-end/phase-skip.mjs +85 -86
- package/scripts/lib/session-end/tail-runner.mjs +178 -0
- package/scripts/lib/session-identity/own-session.mjs +24 -13
- package/scripts/lib/session-schema/constants.mjs +6 -0
- package/scripts/lib/session-schema/validator.mjs +20 -0
- package/scripts/lib/session-shape.mjs +558 -0
- package/scripts/lib/session-start-probes.mjs +10 -3
- package/scripts/lib/session-token-rollup.mjs +95 -10
- package/scripts/lib/state-md/frontmatter-mutators.mjs +22 -34
- package/scripts/lib/state-md.mjs +1 -0
- package/scripts/lib/subagents-schema.mjs +77 -9
- package/scripts/lib/telemetry/pricing.mjs +197 -0
- package/scripts/lib/telemetry/sync.mjs +50 -1
- package/scripts/lib/validate/check-owner-leakage.mjs +17 -8
- package/scripts/lib/validate/check-skill-script-paths.mjs +33 -10
- package/scripts/lib/validate/check-unwired-features.mjs +8 -7
- package/scripts/lib/vault-mirror/process.mjs +2 -1
- package/scripts/lib/vault-mirror/render-sessions.mjs +8 -1
- package/scripts/lib/vault-status/narrative-mirror.mjs +4 -4
- package/scripts/lib/wave-resource-gate.mjs +23 -27
- package/scripts/lib/wave-sizing.mjs +10 -3
- package/scripts/materialize-wave-scope.mjs +68 -14
- package/scripts/print-applicable-rules.mjs +7 -6
- package/scripts/print-learnings-index.mjs +3 -2
- package/scripts/release.mjs +32 -11
- package/scripts/session-shape.mjs +266 -0
- package/skills/_shared/config-reading.md +15 -9
- package/skills/_shared/private-capability-context.md +89 -0
- package/skills/bootstrap/SKILL.md +61 -13
- package/skills/bootstrap/_shared-template.md +99 -14
- package/skills/bootstrap/deep-template.md +36 -26
- package/skills/bootstrap/fast-template.md +44 -8
- package/skills/bootstrap/intensity-heuristic.md +10 -4
- package/skills/bootstrap/private-contract.md +119 -0
- package/skills/bootstrap/public-fallback.md +30 -18
- package/skills/bootstrap/standard-template.md +39 -24
- package/skills/discovery/probes-ui.md +1 -1
- package/skills/docs-orchestrator/audience-mapping.md +1 -1
- package/skills/evolve/SKILL.md +2 -2
- package/skills/gitlab-ops/SKILL.md +3 -3
- package/skills/grill/SKILL.md +1 -1
- package/skills/memory-cleanup/SKILL.md +2 -2
- package/skills/plan/mode-new.md +9 -0
- package/skills/reconcile/SKILL.md +1 -1
- package/skills/session-end/SKILL.md +3 -2
- package/skills/session-end/phase-3-2-docs-verification.md +1 -1
- package/skills/session-end/phase-3-6-tail.md +23 -65
- package/skills/session-end/phase-3-7a-recommendations.md +2 -2
- package/skills/session-end/references/phase-3-documentation-updates.md +8 -6
- package/skills/session-end/references/phase-5-issue-cleanup.md +26 -0
- package/skills/session-end/session-metrics-write.md +31 -12
- package/skills/session-plan/SKILL.md +56 -48
- package/skills/session-plan/wave-template.md +8 -15
- package/skills/session-start/SKILL.md +18 -2
- package/skills/session-start/phase-2-5-docs-planning.md +1 -1
- package/skills/session-start/phase-8-5-express-path.md +12 -9
- package/skills/session-start/references/phase-1-5-session-continuity.md +2 -0
- package/skills/session-start/references/phase-4-ssot-environment-check.md +21 -5
- package/skills/session-start/references/phase-6-7-memory-banner-telemetry-consent.md +3 -1
- package/skills/test-runner/rubric-v1.md +2 -2
- package/skills/wave-executor/SKILL.md +42 -12
- package/skills/wave-executor/circuit-breaker.md +3 -1
- package/skills/wave-executor/references/wave-loop-dispatch.md +4 -2
- package/skills/wave-executor/references/wave-loop-review.md +1 -1
- package/skills/wave-executor/references/wave-loop-scope-manifest.md +6 -2
- package/templates/nextjs-minimal/package.json +1 -1
- package/templates/node-minimal/package.json +1 -1
- package/scripts/lib/multi-provider-build/providers.mjs +0 -64
- package/scripts/lib/multi-provider-build/templating.mjs +0 -130
- package/scripts/lib/owner-config/coerce.mjs +0 -29
- package/scripts/lib/owner-config/constants.mjs +0 -21
- package/scripts/lib/owner-config/defaults.mjs +0 -50
- package/scripts/lib/owner-config/error.mjs +0 -19
- package/scripts/lib/owner-config/index.mjs +0 -13
- package/scripts/lib/owner-config/merge.mjs +0 -52
- package/scripts/lib/owner-config/validate.mjs +0 -259
- package/scripts/lib/owner-config-loader.mjs +0 -170
- package/scripts/lib/owner-config.mjs +0 -28
- package/scripts/lib/soul-resolve.mjs +0 -130
- package/scripts/lib/vault-mirror/render.mjs +0 -8
|
@@ -44,6 +44,8 @@
|
|
|
44
44
|
import { execFileSync } from 'node:child_process';
|
|
45
45
|
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
46
46
|
import { join } from 'node:path';
|
|
47
|
+
import yaml from 'js-yaml';
|
|
48
|
+
import { tokenizeCommand, splitChainSegments, resolveSegmentVerb } from './command-blocker.mjs';
|
|
47
49
|
|
|
48
50
|
/** Commits past the newest tag before release hygiene is worth mentioning. */
|
|
49
51
|
export const DEFAULT_RELEASE_DRIFT_COMMITS = 50;
|
|
@@ -408,8 +410,8 @@ export function checkStaleArtifacts(repoRoot, ageDays = DEFAULT_ARTIFACT_AGE_DAY
|
|
|
408
410
|
/**
|
|
409
411
|
* H4 — CI configuration hygiene.
|
|
410
412
|
*
|
|
411
|
-
* Measured 5/6.
|
|
412
|
-
* pipeline
|
|
413
|
+
* Measured 5/6. Checks locally declared executable commands without evaluating
|
|
414
|
+
* pipeline conditions or external includes. The dependency-audit gap is the load-bearing one: three
|
|
413
415
|
* of the tested repos carried known-vulnerable dependencies that no pipeline
|
|
414
416
|
* would ever surface.
|
|
415
417
|
*
|
|
@@ -438,19 +440,21 @@ export function checkCiConfig(repoRoot) {
|
|
|
438
440
|
return findings;
|
|
439
441
|
}
|
|
440
442
|
|
|
441
|
-
|
|
442
|
-
if (hasGitlab)
|
|
443
|
+
const ciConfigs = [];
|
|
444
|
+
if (hasGitlab) ciConfigs.push({ text: safeRead(gitlabCi), platform: 'gitlab' });
|
|
443
445
|
if (hasGithub) {
|
|
444
446
|
try {
|
|
445
447
|
for (const f of readdirSync(ghWorkflows)) {
|
|
446
|
-
if (f.endsWith('.yml') || f.endsWith('.yaml'))
|
|
448
|
+
if (f.endsWith('.yml') || f.endsWith('.yaml')) {
|
|
449
|
+
ciConfigs.push({ text: safeRead(join(ghWorkflows, f)), platform: 'github' });
|
|
450
|
+
}
|
|
447
451
|
}
|
|
448
452
|
} catch {
|
|
449
453
|
/* unreadable workflows dir — fall through with what we have */
|
|
450
454
|
}
|
|
451
455
|
}
|
|
452
456
|
|
|
453
|
-
if (
|
|
457
|
+
if (ciConfigs.some(({ text }) => text) && !ciConfigs.some(hasAuditStep)) {
|
|
454
458
|
findings.push({
|
|
455
459
|
check: 'ci-audit-job',
|
|
456
460
|
fixable: true,
|
|
@@ -461,6 +465,178 @@ export function checkCiConfig(repoRoot) {
|
|
|
461
465
|
return findings;
|
|
462
466
|
}
|
|
463
467
|
|
|
468
|
+
const GITLAB_COMMAND_FIELDS = ['script', 'before_script', 'after_script'];
|
|
469
|
+
const GITLAB_GLOBAL_FIELDS = new Set([
|
|
470
|
+
'stages', 'types', 'variables', 'default', 'include', 'workflow',
|
|
471
|
+
'image', 'services', 'cache', 'before_script', 'after_script', 'spec',
|
|
472
|
+
]);
|
|
473
|
+
const GITLAB_REFERENCE = Symbol('gitlab-reference');
|
|
474
|
+
const GITLAB_SCHEMA = yaml.DEFAULT_SCHEMA.extend([
|
|
475
|
+
new yaml.Type('!reference', {
|
|
476
|
+
kind: 'sequence',
|
|
477
|
+
construct: (path) => ({ [GITLAB_REFERENCE]: path }),
|
|
478
|
+
}),
|
|
479
|
+
]);
|
|
480
|
+
const AUDIT_VALUE_FLAGS = new Set([
|
|
481
|
+
'--filter', '-F', '--dir', '-C', '--prefix', '--cwd', '--registry',
|
|
482
|
+
'--userconfig', '--globalconfig', '--workspace', '-w', '--cache', '--pm-on-fail',
|
|
483
|
+
]);
|
|
484
|
+
const AUDIT_BOOLEAN_FLAGS = new Set([
|
|
485
|
+
'-r', '--recursive', '-s', '--silent', '-g', '--global', '--workspaces', '--offline',
|
|
486
|
+
]);
|
|
487
|
+
|
|
488
|
+
/** Inspect executable job/step locations, never arbitrary keys named run/script. */
|
|
489
|
+
function hasAuditStep({ text, platform }) {
|
|
490
|
+
let config;
|
|
491
|
+
try {
|
|
492
|
+
if (platform === 'gitlab') {
|
|
493
|
+
const documents = yaml.loadAll(text, undefined, { schema: GITLAB_SCHEMA });
|
|
494
|
+
if (documents.length === 1) {
|
|
495
|
+
[config] = documents;
|
|
496
|
+
} else if (documents.length === 2 && isConfigMap(documents[0])
|
|
497
|
+
&& Object.keys(documents[0]).length === 1 && isConfigMap(documents[0].spec)) {
|
|
498
|
+
// GitLab permits a spec header followed by --- and the job document.
|
|
499
|
+
// Header input defaults are data; unrelated multi-document YAML is ambiguous.
|
|
500
|
+
[, config] = documents;
|
|
501
|
+
} else {
|
|
502
|
+
return false;
|
|
503
|
+
}
|
|
504
|
+
} else {
|
|
505
|
+
config = yaml.load(text);
|
|
506
|
+
}
|
|
507
|
+
} catch {
|
|
508
|
+
return false;
|
|
509
|
+
}
|
|
510
|
+
if (!isConfigMap(config)) return false;
|
|
511
|
+
if (platform === 'github') {
|
|
512
|
+
if (!isConfigMap(config.jobs)) return false;
|
|
513
|
+
return Object.values(config.jobs).some((job) => isConfigMap(job)
|
|
514
|
+
&& job.if !== false && Array.isArray(job.steps)
|
|
515
|
+
&& job.steps.some((step) => isConfigMap(step) && step.if !== false
|
|
516
|
+
&& typeof step.run === 'string' && hasAuditCommand(step.run)));
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
return Object.keys(config).some((name) => {
|
|
520
|
+
if (name.startsWith('.') || GITLAB_GLOBAL_FIELDS.has(name)) return false;
|
|
521
|
+
const job = resolveGitlabJob(config, name);
|
|
522
|
+
if (!job?.script || job.when === 'never') return false;
|
|
523
|
+
return GITLAB_COMMAND_FIELDS.some((field) => {
|
|
524
|
+
let commands = job[field];
|
|
525
|
+
if (!Object.hasOwn(job, field) && field !== 'script') {
|
|
526
|
+
const inherit = job.inherit?.default;
|
|
527
|
+
if (inherit === false || (Array.isArray(inherit) && !inherit.includes(field))) return false;
|
|
528
|
+
commands = Object.hasOwn(config.default ?? {}, field) ? config.default[field] : config[field];
|
|
529
|
+
}
|
|
530
|
+
return hasGitlabAuditCommands(commands, config);
|
|
531
|
+
});
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function isConfigMap(value) {
|
|
536
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
537
|
+
&& !Object.hasOwn(value, GITLAB_REFERENCE);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/** Local extends uses later-parent/child replacement for command arrays. */
|
|
541
|
+
function resolveGitlabJob(config, name, seen = new Set()) {
|
|
542
|
+
if (typeof name !== 'string' || !Object.hasOwn(config, name)
|
|
543
|
+
|| !isConfigMap(config[name]) || seen.has(name)) return null;
|
|
544
|
+
seen.add(name);
|
|
545
|
+
const job = config[name];
|
|
546
|
+
let merged = {};
|
|
547
|
+
const parents = Array.isArray(job.extends) ? job.extends : [job.extends];
|
|
548
|
+
for (const parent of parents.filter((entry) => entry !== undefined)) {
|
|
549
|
+
const inherited = resolveGitlabJob(config, parent, seen);
|
|
550
|
+
// An unresolved later parent may override earlier commands. Only fields
|
|
551
|
+
// supplied by later local parents or the job itself remain evidence.
|
|
552
|
+
merged = inherited ? mergeGitlabJob(merged, inherited) : {};
|
|
553
|
+
}
|
|
554
|
+
seen.delete(name);
|
|
555
|
+
return mergeGitlabJob(merged, job);
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
function mergeGitlabJob(parent, child) {
|
|
559
|
+
const merged = { ...parent, ...child };
|
|
560
|
+
// Command fields replace wholesale; inherit is a map, so its default flag
|
|
561
|
+
// survives a child that overrides only inherit.variables.
|
|
562
|
+
if (isConfigMap(parent.inherit) && isConfigMap(child.inherit)) {
|
|
563
|
+
merged.inherit = { ...parent.inherit, ...child.inherit };
|
|
564
|
+
}
|
|
565
|
+
return merged;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/** Resolve only command strings/arrays and local !reference paths, with cycle guards. */
|
|
569
|
+
function hasGitlabAuditCommands(commands, config) {
|
|
570
|
+
const seen = new WeakSet();
|
|
571
|
+
function visit(value) {
|
|
572
|
+
if (typeof value === 'string') return hasAuditCommand(value);
|
|
573
|
+
if (!value || typeof value !== 'object' || seen.has(value)) return false;
|
|
574
|
+
seen.add(value);
|
|
575
|
+
let found = false;
|
|
576
|
+
if (Array.isArray(value)) {
|
|
577
|
+
found = value.some(visit);
|
|
578
|
+
} else if (Array.isArray(value[GITLAB_REFERENCE])) {
|
|
579
|
+
let target = config;
|
|
580
|
+
for (const key of value[GITLAB_REFERENCE]) {
|
|
581
|
+
target = target && typeof target === 'object' && Object.hasOwn(target, key) ? target[key] : undefined;
|
|
582
|
+
}
|
|
583
|
+
found = visit(target);
|
|
584
|
+
}
|
|
585
|
+
seen.delete(value);
|
|
586
|
+
return found;
|
|
587
|
+
}
|
|
588
|
+
return visit(commands);
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/**
|
|
592
|
+
* Bounded command recognition: wrappers and options may precede the audit
|
|
593
|
+
* subcommand, but another verb (install/run/echo) ends the search. Unknown
|
|
594
|
+
* value-taking flags and commands hidden in external includes are not resolved.
|
|
595
|
+
*/
|
|
596
|
+
function hasAuditCommand(command) {
|
|
597
|
+
return splitChainSegments(tokenizeCommand(command)).some((segment) => hasAuditSegment(segment));
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
function hasAuditSegment(segment, depth = 0) {
|
|
601
|
+
const resolved = resolveSegmentVerb(segment);
|
|
602
|
+
if (resolved.alt) return false;
|
|
603
|
+
let { verb, index } = resolved;
|
|
604
|
+
if (resolved.payloads.length > 0) {
|
|
605
|
+
if (depth >= 3 || resolved.payloads.length !== 1) return false;
|
|
606
|
+
const tokens = tokenizeCommand(resolved.payloads[0]);
|
|
607
|
+
const parts = splitChainSegments(tokens);
|
|
608
|
+
// env -S splits argv; it does not execute shell operators. Restrict this
|
|
609
|
+
// reuse of the shared tokenizer to one plain command with no redirects.
|
|
610
|
+
if (parts.length !== 1 || parts[0].length !== tokens.length
|
|
611
|
+
|| tokens.some((token) => token.redirect)) return false;
|
|
612
|
+
const trailingArgs = index < 0 ? [] : segment.slice(index);
|
|
613
|
+
return hasAuditSegment([...tokens, ...trailingArgs], depth + 1);
|
|
614
|
+
}
|
|
615
|
+
if (verb === 'corepack' || verb === 'npx') {
|
|
616
|
+
index++;
|
|
617
|
+
while (['-y', '--yes', '--no-install', '--'].includes(segment[index]?.text)) index++;
|
|
618
|
+
verb = segment[index]?.text;
|
|
619
|
+
}
|
|
620
|
+
if (segment.slice(index + 1).some((token) => ['-h', '--help', '--version'].includes(token.text))) return false;
|
|
621
|
+
if (/^(?:pip-audit|osv-scanner|dependency-check(?:\.sh)?)$/.test(verb ?? '')) return true;
|
|
622
|
+
if (!/^(?:npm|pnpm|yarn|bun)(?:@[\w.+-]+)?$/.test(verb ?? '') && verb !== 'cargo') return false;
|
|
623
|
+
let i = index + 1;
|
|
624
|
+
while (segment[i]?.text.startsWith('-')) {
|
|
625
|
+
const flag = segment[i].text;
|
|
626
|
+
if (flag === '--') { i++; break; }
|
|
627
|
+
if (AUDIT_VALUE_FLAGS.has(flag)) {
|
|
628
|
+
if (!segment[i + 1] || segment[i + 1].redirect) return false;
|
|
629
|
+
i += 2;
|
|
630
|
+
} else if (AUDIT_BOOLEAN_FLAGS.has(flag) || /^--[\w-]+=/.test(flag)) {
|
|
631
|
+
i++;
|
|
632
|
+
} else {
|
|
633
|
+
return false;
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
if (/^yarn(?:@|$)/.test(verb) && segment[i]?.text === 'npm') i++;
|
|
637
|
+
return segment[i]?.text === 'audit';
|
|
638
|
+
}
|
|
639
|
+
|
|
464
640
|
/** @param {string} p @returns {string} */
|
|
465
641
|
function safeRead(p) {
|
|
466
642
|
try {
|
|
@@ -117,16 +117,18 @@ function resolveRepoRoot(explicit) {
|
|
|
117
117
|
* Closed set of `degraded` reasons for {@link loadCommandsFromSessionConfigDetailed}.
|
|
118
118
|
*
|
|
119
119
|
* Its own enum, deliberately not shared with `ci-status-banner.mjs`'s
|
|
120
|
-
* `DEGRADED_REASONS`: the members below are
|
|
120
|
+
* `DEGRADED_REASONS`: the members below are the ways THIS
|
|
121
121
|
* config-read can fail, and an enum whose members are not exhaustively
|
|
122
122
|
* reachable cannot be switched on exhaustively.
|
|
123
123
|
*
|
|
124
|
+
* - `invalid-repo-root` — no non-blank string root was supplied.
|
|
124
125
|
* - `script-missing` — `scripts/parse-config.mjs` is not on disk.
|
|
125
126
|
* - `spawn-failed` — the subprocess exited non-zero, timed out, or wrote
|
|
126
127
|
* nothing to stdout.
|
|
127
128
|
* - `parse-error` — stdout was not parseable JSON (or the read threw).
|
|
128
129
|
*/
|
|
129
130
|
export const CONFIG_READ_DEGRADED_REASONS = Object.freeze([
|
|
131
|
+
'invalid-repo-root',
|
|
130
132
|
'script-missing',
|
|
131
133
|
'spawn-failed',
|
|
132
134
|
'parse-error',
|
|
@@ -168,13 +170,18 @@ function sessionConfigFileExists(repoRoot) {
|
|
|
168
170
|
* `degraded` is OMITTED, not set to null, on the success path, so a strict
|
|
169
171
|
* `toEqual({commands: {…}})` pin holds for every readable config.
|
|
170
172
|
*
|
|
171
|
-
*
|
|
173
|
+
* Requires an explicit, non-blank repository root; callers that intend cwd
|
|
174
|
+
* must resolve that default before invoking this loader. Never throws.
|
|
172
175
|
*
|
|
173
176
|
* @param {string} repoRoot
|
|
174
177
|
* @returns {{commands: {lint?: string, typecheck?: string, test?: string},
|
|
175
|
-
* degraded?: 'script-missing'|'spawn-failed'|'parse-error'}}
|
|
178
|
+
* degraded?: 'invalid-repo-root'|'script-missing'|'spawn-failed'|'parse-error'}}
|
|
176
179
|
*/
|
|
177
180
|
export function loadCommandsFromSessionConfigDetailed(repoRoot) {
|
|
181
|
+
// Node inherits cwd for undefined/null, which would read another repo's config.
|
|
182
|
+
if (typeof repoRoot !== 'string' || !repoRoot.trim()) {
|
|
183
|
+
return { commands: {}, degraded: 'invalid-repo-root' };
|
|
184
|
+
}
|
|
178
185
|
try {
|
|
179
186
|
const scriptPath = join(
|
|
180
187
|
dirname(fileURLToPath(import.meta.url)),
|
|
@@ -229,9 +236,9 @@ export function loadCommandsFromSessionConfigDetailed(repoRoot) {
|
|
|
229
236
|
* Returns a partial object — keys that fail to resolve are simply absent
|
|
230
237
|
* (the caller falls through to DEFAULT_COMMANDS for those).
|
|
231
238
|
*
|
|
232
|
-
*
|
|
233
|
-
*
|
|
234
|
-
*
|
|
239
|
+
* Returns the commands half of {@link loadCommandsFromSessionConfigDetailed},
|
|
240
|
+
* including an empty object for an invalid root. Callers that need to tell a
|
|
241
|
+
* failed read from an empty config use the detailed variant.
|
|
235
242
|
*
|
|
236
243
|
* Never throws.
|
|
237
244
|
*
|
|
@@ -61,6 +61,14 @@
|
|
|
61
61
|
* of starts is a broken instrument to be re-aimed, not a policy to obey.
|
|
62
62
|
*/
|
|
63
63
|
|
|
64
|
+
// `session-shape.mjs` is the ONE exported `agents-per-wave` resolver; this
|
|
65
|
+
// module used to carry a private third copy. Import-graph note (this file is
|
|
66
|
+
// hook-reachable via `hooks/_lib/hook-import-set.json`): `session-shape.mjs`
|
|
67
|
+
// statically imports only `session-schema/constants.mjs`, a pure-data leaf, and
|
|
68
|
+
// defers `events.mjs` to a lazy `await import()` — so nothing runs at module
|
|
69
|
+
// load here that did not run before.
|
|
70
|
+
import { resolveAgentCap } from '../session-shape.mjs';
|
|
71
|
+
|
|
64
72
|
// ---------------------------------------------------------------------------
|
|
65
73
|
// Verdict precedence helper
|
|
66
74
|
// ---------------------------------------------------------------------------
|
|
@@ -252,29 +260,19 @@ function concurrencySignal(snapshot, concWarn) {
|
|
|
252
260
|
}
|
|
253
261
|
|
|
254
262
|
/**
|
|
255
|
-
*
|
|
256
|
-
* when no cap should apply. Mirrors `resolveApwCap()` in
|
|
257
|
-
* `../wave-resource-gate.mjs` — kept as a local pure helper here rather than
|
|
258
|
-
* a cross-module import since both sites are ≤10 lines and evolve
|
|
259
|
-
* independently per their own gate's options shape.
|
|
263
|
+
* The session type this module resolves `agents-per-wave` FOR: none.
|
|
260
264
|
*
|
|
261
|
-
* `evaluate()` has no session-mode input in scope, so
|
|
262
|
-
*
|
|
263
|
-
* override as `<default> (mode: <higher-ceiling>)`, i.e.
|
|
264
|
-
* MORE restrictive of the pair
|
|
265
|
-
* mode-specific ceiling,
|
|
265
|
+
* `evaluate()` has no session-mode input in scope, so it deliberately asks
|
|
266
|
+
* `resolveAgentCap` for no type and takes the `.default` fallback. The HR-003
|
|
267
|
+
* convention writes the override as `<default> (mode: <higher-ceiling>)`, i.e.
|
|
268
|
+
* `default` is the MORE restrictive of the pair — under-applying a looser
|
|
269
|
+
* mode-specific ceiling is safe, while passing a guessed `'deep'` here would
|
|
270
|
+
* silently RAISE a heavy repo's ceiling to the deep override on a loaded host.
|
|
271
|
+
* Naming the absence is what keeps that from being written by accident.
|
|
266
272
|
*
|
|
267
|
-
* @
|
|
268
|
-
* @returns {number|null}
|
|
273
|
+
* @type {undefined}
|
|
269
274
|
*/
|
|
270
|
-
|
|
271
|
-
if (typeof cap === 'number') return Number.isFinite(cap) ? cap : null;
|
|
272
|
-
if (cap !== null && typeof cap === 'object' && !Array.isArray(cap)) {
|
|
273
|
-
const def = cap.default;
|
|
274
|
-
return typeof def === 'number' && Number.isFinite(def) ? def : null;
|
|
275
|
-
}
|
|
276
|
-
return null;
|
|
277
|
-
}
|
|
275
|
+
const MODE_BLIND_SESSION_TYPE = undefined;
|
|
278
276
|
|
|
279
277
|
/**
|
|
280
278
|
* Evaluate a snapshot against `resource-thresholds` (from Session Config #166)
|
|
@@ -423,7 +421,7 @@ export function evaluate(snapshot, thresholds, options = {}) {
|
|
|
423
421
|
// the live-probe signals already computed.
|
|
424
422
|
// ---------------------------------------------------------------------------
|
|
425
423
|
const { heavyRepo, agentsPerWave } = options;
|
|
426
|
-
const resolvedApwCap =
|
|
424
|
+
const resolvedApwCap = resolveAgentCap(agentsPerWave, MODE_BLIND_SESSION_TYPE);
|
|
427
425
|
if (heavyRepo === true && resolvedApwCap !== null) {
|
|
428
426
|
cap = cap === null ? resolvedApwCap : Math.min(cap, resolvedApwCap);
|
|
429
427
|
}
|
|
@@ -121,7 +121,7 @@ function listManifestCategories(indexContent) {
|
|
|
121
121
|
* @param {string|null} explicitArchetype
|
|
122
122
|
* @returns {{archetype: string|null, known: boolean}}
|
|
123
123
|
*/
|
|
124
|
-
function resolveArchetype(repoRoot, explicitArchetype) {
|
|
124
|
+
export function resolveArchetype(repoRoot, explicitArchetype) {
|
|
125
125
|
if (explicitArchetype) {
|
|
126
126
|
return { archetype: explicitArchetype.trim().toLowerCase(), known: true };
|
|
127
127
|
}
|
|
@@ -194,6 +194,12 @@ function escapeRegex(s) {
|
|
|
194
194
|
* at least one concrete bullet entry. This keeps `/bootstrap --sync-rules`
|
|
195
195
|
* ready for future opt-in categories without requiring CLI changes.
|
|
196
196
|
*
|
|
197
|
+
* `requiredBasenames` optionally adds explicitly required manifest entries,
|
|
198
|
+
* regardless of category or archetype scope. The caller supplies this data;
|
|
199
|
+
* this synchronous writer performs no external lookup. Every requested name
|
|
200
|
+
* must resolve uniquely in the full manifest before ANY file is written.
|
|
201
|
+
* Source validation, provenance, local overrides and dry-run still apply.
|
|
202
|
+
*
|
|
197
203
|
* Vendoring sanitizer (issue #1098): every source file that reaches the write
|
|
198
204
|
* decision is additionally scanned by `scanVendoringLeaks()`, and its findings
|
|
199
205
|
* are collected into the additive `sanitizer[]` array. This runs in
|
|
@@ -214,7 +220,8 @@ function escapeRegex(s) {
|
|
|
214
220
|
* dryRun?: boolean,
|
|
215
221
|
* validate?: boolean,
|
|
216
222
|
* requireProvenance?: boolean,
|
|
217
|
-
* archetype?: string|null
|
|
223
|
+
* archetype?: string|null,
|
|
224
|
+
* requiredBasenames?: string[]|null
|
|
218
225
|
* }} opts
|
|
219
226
|
* @returns {{
|
|
220
227
|
* written: string[],
|
|
@@ -233,6 +240,7 @@ export function syncRules({
|
|
|
233
240
|
validate = true,
|
|
234
241
|
requireProvenance = true,
|
|
235
242
|
archetype = null,
|
|
243
|
+
requiredBasenames = null,
|
|
236
244
|
} = {}) {
|
|
237
245
|
const written = [];
|
|
238
246
|
const skipped = [];
|
|
@@ -268,13 +276,35 @@ export function syncRules({
|
|
|
268
276
|
? categories
|
|
269
277
|
: listManifestCategories(indexContent);
|
|
270
278
|
const entries = parseIndex(indexContent, selectedCategories);
|
|
279
|
+
const fullManifest = parseIndex(indexContent, listManifestCategories(indexContent));
|
|
280
|
+
const required = new Set();
|
|
281
|
+
if (requiredBasenames !== null) {
|
|
282
|
+
if (!Array.isArray(requiredBasenames)) {
|
|
283
|
+
errors.push({ file: '_index.md', reason: 'requiredBasenames must be an array of unique manifest basenames' });
|
|
284
|
+
} else {
|
|
285
|
+
for (const name of requiredBasenames) {
|
|
286
|
+
if (typeof name !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9_.-]*\.md$/.test(name) || required.has(name)) {
|
|
287
|
+
errors.push({ file: '_index.md', reason: 'invalid or duplicate required basename' });
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
required.add(name);
|
|
291
|
+
const matching = fullManifest.filter((entry) => basename(entry.relPath) === name);
|
|
292
|
+
if (matching.length !== 1) {
|
|
293
|
+
errors.push({ file: name, reason: 'required basename must resolve uniquely in _index.md' });
|
|
294
|
+
} else if (!entries.some((entry) => entry.relPath === matching[0].relPath)) {
|
|
295
|
+
entries.push(matching[0]);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
if (errors.length > 0) return { written, skipped, preserved, errors, warnings, sanitizer };
|
|
300
|
+
}
|
|
271
301
|
|
|
272
302
|
// Resolvability for the See-Also sanitizer is judged against the FULL
|
|
273
303
|
// manifest, not `selectedCategories`: an archetype-scoped rule is a
|
|
274
304
|
// legitimate citation target in every repo whose archetype matches, so
|
|
275
305
|
// narrowing this to the current selection would report false leaks.
|
|
276
306
|
const manifestBasenames = new Set(
|
|
277
|
-
|
|
307
|
+
fullManifest.map((e) => basename(e.relPath)),
|
|
278
308
|
);
|
|
279
309
|
|
|
280
310
|
if (entries.length === 0) {
|
|
@@ -297,7 +327,7 @@ export function syncRules({
|
|
|
297
327
|
|
|
298
328
|
// Archetype filter (issue #722 Epic A Wave 3) — evaluated before any
|
|
299
329
|
// file IO, so a skip never triggers a spurious "source file not found".
|
|
300
|
-
if (archetypes !== null) {
|
|
330
|
+
if (archetypes !== null && !required.has(basename(relPath))) {
|
|
301
331
|
if (!resolvedArchetype.known) {
|
|
302
332
|
skipped.push({ file: relPath, reason: 'archetype-unknown' });
|
|
303
333
|
continue;
|