session-orchestrator 3.17.0 → 3.19.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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.cursor/rules/030-wave-execution.mdc +17 -1
- package/CHANGELOG.md +105 -412
- package/README.md +12 -9
- package/SECURITY.md +190 -27
- package/agents/AGENTS.md +20 -3
- package/agents/code-implementer.md +6 -6
- package/agents/db-specialist.md +1 -1
- package/agents/qa-strategist.md +31 -6
- package/agents/schemas/qa-strategist.schema.json +27 -0
- package/agents/schemas/test-writer.schema.json +60 -2
- package/agents/security-reviewer.md +1 -1
- package/agents/session-reviewer.md +1 -1
- package/agents/test-writer.md +29 -10
- package/agents/ui-developer.md +1 -1
- package/commands/contract-version-bump.md +28 -0
- package/commands/portfolio.md +1 -1
- package/docs/USER-GUIDE.md +8 -3
- package/docs/ci-setup.md +121 -7
- package/docs/codex-setup.md +1 -1
- package/docs/components.md +6 -6
- package/docs/cursor-setup.md +22 -9
- package/docs/events-schema.md +5 -1
- package/docs/instruction-delivery.md +444 -0
- package/docs/rule-authoring.md +58 -9
- package/docs/session-config-reference.md +244 -9
- package/docs/session-config-template.md +39 -3
- package/hooks/_lib/guard-source-loader.mjs +467 -0
- package/hooks/_lib/lock-bootstrap.mjs +21 -0
- package/hooks/_lib/vcs-create-matcher.mjs +119 -0
- package/hooks/config-protection.mjs +0 -0
- package/hooks/enforce-commands.mjs +10 -2
- package/hooks/hooks-codex.json +1 -1
- package/hooks/hooks-cursor.json +11 -2
- package/hooks/hooks-pi.json +10 -0
- package/hooks/hooks.json +21 -1
- package/hooks/on-session-end.mjs +178 -18
- package/hooks/on-session-start.mjs +23 -0
- package/hooks/post-bash-write-verify.mjs +977 -0
- package/hooks/post-subagent-discovery-validator.mjs +256 -41
- package/hooks/pre-bash-destructive-guard.mjs +525 -160
- package/hooks/pre-bash-issue-budget.mjs +167 -0
- package/hooks/pre-bash-sessions-ledger-guard.mjs +627 -0
- package/hooks/pre-bash-templates-first.mjs +96 -63
- package/hooks/subagent-telemetry.mjs +527 -37
- package/package.json +5 -2
- package/pi/prompts/contract-version-bump.md +12 -0
- package/rules/README.md +32 -0
- package/scripts/archive-closed-prds.mjs +12 -22
- package/scripts/autopilot-multi.mjs +103 -20
- package/scripts/backfill-abandoned-sessions.mjs +160 -4
- package/scripts/check-doc-consistency.sh +17 -1
- package/scripts/eval-session.mjs +50 -9
- package/scripts/fleet-instruction-scan.mjs +141 -0
- package/scripts/lib/autopilot/mr-draft.mjs +31 -1
- package/scripts/lib/autopilot/worktree-pipeline.mjs +113 -5
- package/scripts/lib/backlog-scan.mjs +39 -6
- package/scripts/lib/blocked-commands-policy.mjs +340 -0
- package/scripts/lib/ci-status-banner.mjs +75 -12
- package/scripts/lib/claude-md-budget-lint.mjs +283 -34
- package/scripts/lib/command-blocker.mjs +1013 -58
- package/scripts/lib/config/config-protection.mjs +2 -1
- package/scripts/lib/config/drift-check.mjs +9 -1
- package/scripts/lib/config/gitlab-portfolio.mjs +1 -1
- package/scripts/lib/config/issue-budget.mjs +123 -0
- package/scripts/lib/config/reconcile.mjs +21 -0
- package/scripts/lib/config/section-extractor.mjs +121 -1
- package/scripts/lib/config-schema.mjs +23 -3
- package/scripts/lib/config.mjs +17 -0
- package/scripts/lib/convergence-monitor.mjs +49 -3
- package/scripts/lib/description-surface.mjs +535 -0
- package/scripts/lib/dispatcher/enumerate.mjs +26 -40
- package/scripts/lib/ecosystem-wizard/config-writer.mjs +26 -24
- package/scripts/lib/ecosystem-wizard/wizard-prompt.mjs +1 -1
- package/scripts/lib/eval/engine.mjs +47 -5
- package/scripts/lib/events.mjs +59 -7
- package/scripts/lib/gates/gate-full.mjs +15 -3
- package/scripts/lib/gates/gate-helpers.mjs +132 -6
- package/scripts/lib/gitlab-ops/stale-mr-sweep.mjs +28 -8
- package/scripts/lib/gitlab-portfolio/aggregator.mjs +8 -2
- package/scripts/lib/gitlab-portfolio/cli.mjs +1 -1
- package/scripts/lib/handover-gate.mjs +7 -3
- package/scripts/lib/harness-audit/categories/category4.mjs +9 -3
- package/scripts/lib/instruction-budget-guard.mjs +402 -51
- package/scripts/lib/io.mjs +345 -10
- package/scripts/lib/issue-budget.mjs +269 -0
- package/scripts/lib/issue-close-strip-labels.mjs +39 -9
- package/scripts/lib/label-scope.mjs +47 -0
- package/scripts/lib/learnings/schema.mjs +43 -3
- package/scripts/lib/lock-reaper.mjs +1 -2
- package/scripts/lib/memory-proposals/schema.mjs +36 -1
- package/scripts/lib/peer-discovery.mjs +645 -0
- package/scripts/lib/pi-hook-bridge.mjs +146 -17
- package/scripts/lib/product-repo-detect.mjs +9 -8
- package/scripts/lib/project-hygiene.mjs +432 -0
- package/scripts/lib/quality-gate.mjs +167 -0
- package/scripts/lib/recommendations-v0.mjs +1 -1
- package/scripts/lib/reconcile/eligibility.mjs +1 -1
- package/scripts/lib/reconcile/emitter.mjs +23 -4
- package/scripts/lib/reconcile/engine.mjs +147 -39
- package/scripts/lib/reconcile/idempotency.mjs +114 -14
- package/scripts/lib/reconcile-nudge-banner.mjs +65 -9
- package/scripts/lib/resource-probe/evaluate.mjs +70 -4
- package/scripts/lib/resource-probe.mjs +19 -0
- package/scripts/lib/rule-loader.mjs +6 -0
- package/scripts/lib/scope-baseline.mjs +564 -0
- package/scripts/lib/scope-gate.mjs +399 -98
- package/scripts/lib/session-close-backfill.mjs +61 -6
- package/scripts/lib/session-end/phase-skip.mjs +1 -0
- package/scripts/lib/session-id.mjs +221 -41
- package/scripts/lib/session-lock.mjs +304 -6
- package/scripts/lib/session-schema/constants.mjs +22 -3
- package/scripts/lib/session-schema/validator.mjs +16 -0
- package/scripts/lib/sessions-integrity-banner.mjs +294 -0
- package/scripts/lib/sessions-staleness-banner.mjs +121 -12
- package/scripts/lib/skill-evolution/idempotency.mjs +135 -16
- package/scripts/lib/skill-evolution/mr-opener.mjs +9 -1
- package/scripts/lib/spiral-carryover.mjs +142 -30
- package/scripts/lib/state-md/mission-status.mjs +53 -3
- package/scripts/lib/subagents-schema.mjs +43 -9
- package/scripts/lib/test-runner/issue-reconcile.mjs +53 -13
- package/scripts/lib/tests-src-ratio.mjs +484 -0
- package/scripts/lib/validate/check-agents.mjs +56 -0
- package/scripts/lib/validate/check-hooks-symmetry.mjs +244 -10
- package/scripts/lib/validate/check-rules.mjs +217 -35
- package/scripts/lib/validate/check-test-value-bans.mjs +782 -0
- package/scripts/lib/validate/check-unicode-safety.mjs +1 -0
- package/scripts/lib/validate-vendored-rules.mjs +10 -2
- package/scripts/lib/vault-archive.mjs +17 -2
- package/scripts/lib/vault-backfill/glab.mjs +8 -0
- package/scripts/lib/vault-mirror/process.mjs +30 -0
- package/scripts/lib/vault-mirror/render-sessions.mjs +293 -36
- package/scripts/lib/vcs-repo-spec.mjs +362 -0
- package/scripts/lib/wave-resource-gate.mjs +115 -11
- package/scripts/lib/worktree/listing.mjs +44 -7
- package/scripts/mcp-server.sh +17 -3
- package/scripts/measure-context-overhead.sh +151 -0
- package/scripts/memory-propose.mjs +72 -9
- package/scripts/print-applicable-rules.mjs +51 -12
- package/scripts/release.mjs +534 -0
- package/scripts/run-quality-gate.mjs +123 -5
- package/scripts/validate-wave-scope.mjs +182 -17
- package/scripts/vault-integration-watcher.mjs +32 -10
- package/skills/_shared/config-reading.md +2 -2
- package/skills/bootstrap/fast-template.md +1 -1
- package/skills/claude-md-drift-check/checker.mjs +145 -28
- package/skills/contract-version-bump/SKILL.md +219 -0
- package/skills/discovery/SKILL.md +4 -4
- package/skills/discovery/issue-templates.md +11 -11
- package/skills/discovery/probes-audit.md +1 -1
- package/skills/discovery/probes-feature.md +1 -1
- package/skills/discovery/probes-session.md +26 -5
- package/skills/ecosystem-health/SKILL.md +1 -1
- package/skills/ecosystem-health/wizard.md +4 -4
- package/skills/evolve/SKILL.md +1 -0
- package/skills/gitlab-ops/SKILL.md +20 -12
- package/skills/gitlab-portfolio/SKILL.md +2 -2
- package/skills/hook-development/SKILL.md +1 -1
- package/skills/mode-selector/SKILL.md +1 -1
- package/skills/npm-publish/SKILL.md +17 -1
- package/skills/plan/SKILL.md +5 -5
- package/skills/plan/mode-feature.md +4 -4
- package/skills/plan/mode-new.md +10 -10
- package/skills/plan/mode-retro.md +1 -1
- package/skills/quality-gates/SKILL.md +1 -1
- package/skills/reconcile/SKILL.md +21 -4
- package/skills/session-end/SKILL.md +34 -13
- package/skills/session-end/discovery-scan.md +4 -2
- package/skills/session-end/drift-operations.md +4 -4
- package/skills/session-end/metrics-collection.md +13 -0
- package/skills/session-end/phase-3-2-docs-verification.md +1 -1
- package/skills/session-end/phase-3-6-tail.md +2 -1
- package/skills/session-end/plan-verification.md +5 -2
- package/skills/session-end/vault-operations.md +1 -1
- package/skills/session-end/verification-checklist.md +1 -1
- package/skills/session-plan/SKILL.md +6 -2
- package/skills/session-plan/wave-template.md +2 -0
- package/skills/session-start/SKILL.md +73 -7
- package/skills/session-start/phase-4-5-resource-health.md +15 -2
- package/skills/test-runner/SKILL.md +2 -2
- package/skills/vault-sync/validator.mjs +108 -7
- package/skills/wave-executor/SKILL.md +5 -2
- package/skills/wave-executor/circuit-breaker.md +2 -0
- package/skills/wave-executor/wave-loop.md +163 -10
- package/templates/_shared/loop.md +4 -4
|
@@ -14,6 +14,11 @@
|
|
|
14
14
|
* - Never throws — all errors are caught and returned as `{ stripped: [],
|
|
15
15
|
* error: string }` so callers can log and proceed with close unblocked.
|
|
16
16
|
* - Supports GitLab (`glab`) and GitHub (`gh`). VCS is a required param.
|
|
17
|
+
* - Every `glab`/`gh` spawn is pinned to the resolved repo via `-R <spec>`
|
|
18
|
+
* (#839) — see `scripts/lib/vcs-repo-spec.mjs` for why: a bare spawn
|
|
19
|
+
* falls back to the ambient `GITLAB_HOST`/`GH_HOST`, which can silently
|
|
20
|
+
* resolve to the wrong GitLab instance on a multi-host machine, turning
|
|
21
|
+
* this whole module into a silent no-op.
|
|
17
22
|
*
|
|
18
23
|
* Usage (from session-end Phase 5 issue-close loop):
|
|
19
24
|
*
|
|
@@ -28,6 +33,7 @@
|
|
|
28
33
|
*/
|
|
29
34
|
|
|
30
35
|
import { execFileSync } from 'node:child_process';
|
|
36
|
+
import { resolveRepoSpec } from './vcs-repo-spec.mjs';
|
|
31
37
|
|
|
32
38
|
/** Regex that matches any `status:*` label name. */
|
|
33
39
|
const STATUS_LABEL_RE = /^status:/;
|
|
@@ -57,15 +63,19 @@ function runCli(cmd, args) {
|
|
|
57
63
|
/**
|
|
58
64
|
* Fetch the label names currently applied to an issue.
|
|
59
65
|
*
|
|
60
|
-
* @param {{ issueId: number | string, vcs: 'gitlab' | 'github' }} opts
|
|
66
|
+
* @param {{ issueId: number | string, vcs: 'gitlab' | 'github', spec?: string }} opts
|
|
67
|
+
* `spec` is the resolved `-R`/`--repo` host-pinning value (#839); omitted
|
|
68
|
+
* (undefined) means no `-R` is appended — never emit `-R undefined`.
|
|
61
69
|
* @returns {{ ok: boolean, labels: string[], stderr: string }}
|
|
62
70
|
*/
|
|
63
|
-
function fetchLabels({ issueId, vcs }) {
|
|
71
|
+
function fetchLabels({ issueId, vcs, spec }) {
|
|
64
72
|
const id = String(issueId);
|
|
65
73
|
|
|
66
74
|
if (vcs === 'github') {
|
|
67
75
|
// `gh issue view <NUMBER> --json labels` returns { labels: [{name, ...}] }
|
|
68
|
-
const
|
|
76
|
+
const args = ['issue', 'view', id, '--json', 'labels'];
|
|
77
|
+
if (spec) args.push('-R', spec);
|
|
78
|
+
const res = runCli('gh', args);
|
|
69
79
|
if (!res.ok) return { ok: false, labels: [], stderr: res.stderr };
|
|
70
80
|
let parsed;
|
|
71
81
|
try {
|
|
@@ -81,7 +91,9 @@ function fetchLabels({ issueId, vcs }) {
|
|
|
81
91
|
|
|
82
92
|
// Default: gitlab via glab.
|
|
83
93
|
// `glab issue view <IID> --output json` returns an issue object with a `labels` array.
|
|
84
|
-
const
|
|
94
|
+
const args = ['issue', 'view', id, '--output', 'json'];
|
|
95
|
+
if (spec) args.push('-R', spec);
|
|
96
|
+
const res = runCli('glab', args);
|
|
85
97
|
if (!res.ok) return { ok: false, labels: [], stderr: res.stderr };
|
|
86
98
|
let parsed;
|
|
87
99
|
try {
|
|
@@ -89,7 +101,7 @@ function fetchLabels({ issueId, vcs }) {
|
|
|
89
101
|
} catch (e) {
|
|
90
102
|
return { ok: false, labels: [], stderr: `JSON parse failed: ${e && e.message ? e.message : String(e)}` };
|
|
91
103
|
}
|
|
92
|
-
// GitLab JSON shape: { labels: ["status:ready", "priority
|
|
104
|
+
// GitLab JSON shape: { labels: ["status:ready", "priority::high", ...] }
|
|
93
105
|
const labels = Array.isArray(parsed?.labels)
|
|
94
106
|
? parsed.labels.map((l) => (typeof l === 'string' ? l : String(l))).filter(Boolean)
|
|
95
107
|
: [];
|
|
@@ -105,11 +117,22 @@ function fetchLabels({ issueId, vcs }) {
|
|
|
105
117
|
*
|
|
106
118
|
* @param {{
|
|
107
119
|
* issueId: number | string,
|
|
108
|
-
* vcs?: 'gitlab' | 'github'
|
|
120
|
+
* vcs?: 'gitlab' | 'github',
|
|
121
|
+
* repoRoot?: string,
|
|
122
|
+
* resolveRepoSpecFn?: (opts: { repoRoot: string, vcs: 'gitlab' | 'github' }) => string | undefined
|
|
109
123
|
* }} opts
|
|
124
|
+
* `repoRoot` defaults to `process.cwd()`. `resolveRepoSpecFn` is the
|
|
125
|
+
* injectable seam for the `-R`/`--repo` host-pinning resolution (#839) —
|
|
126
|
+
* defaults to the real `resolveRepoSpec` (shells out to `git remote
|
|
127
|
+
* get-url`); tests inject a stub instead of shelling out.
|
|
110
128
|
* @returns {Promise<{ stripped: string[], error?: string }>}
|
|
111
129
|
*/
|
|
112
|
-
export async function stripStatusLabels({
|
|
130
|
+
export async function stripStatusLabels({
|
|
131
|
+
issueId,
|
|
132
|
+
vcs = 'gitlab',
|
|
133
|
+
repoRoot = process.cwd(),
|
|
134
|
+
resolveRepoSpecFn = resolveRepoSpec,
|
|
135
|
+
} = {}) {
|
|
113
136
|
try {
|
|
114
137
|
const id = String(issueId ?? '').trim();
|
|
115
138
|
if (!id || id === 'undefined' || id === 'null') {
|
|
@@ -118,8 +141,12 @@ export async function stripStatusLabels({ issueId, vcs = 'gitlab' } = {}) {
|
|
|
118
141
|
|
|
119
142
|
const vcsResolved = vcs === 'github' ? 'github' : 'gitlab';
|
|
120
143
|
|
|
144
|
+
// Resolve the -R/--repo host-pinning spec ONCE, reused by both the fetch
|
|
145
|
+
// and the strip call below (#839). undefined ⇒ no -R is appended anywhere.
|
|
146
|
+
const spec = resolveRepoSpecFn({ repoRoot, vcs: vcsResolved });
|
|
147
|
+
|
|
121
148
|
// Step 1: fetch current labels.
|
|
122
|
-
const { ok, labels, stderr } = fetchLabels({ issueId: id, vcs: vcsResolved });
|
|
149
|
+
const { ok, labels, stderr } = fetchLabels({ issueId: id, vcs: vcsResolved, spec });
|
|
123
150
|
if (!ok) {
|
|
124
151
|
return { stripped: [], error: `failed to fetch labels: ${stderr}` };
|
|
125
152
|
}
|
|
@@ -139,11 +166,14 @@ export async function stripStatusLabels({ issueId, vcs = 'gitlab' } = {}) {
|
|
|
139
166
|
for (const label of toStrip) {
|
|
140
167
|
args.push('--remove-label', label);
|
|
141
168
|
}
|
|
169
|
+
if (spec) args.push('-R', spec);
|
|
142
170
|
stripRes = runCli('gh', args);
|
|
143
171
|
} else {
|
|
144
172
|
// `glab issue update <IID> --unlabel label1,label2,...`
|
|
145
173
|
// glab accepts comma-separated list in a single --unlabel flag value.
|
|
146
|
-
|
|
174
|
+
const args = ['issue', 'update', id, '--unlabel', toStrip.join(',')];
|
|
175
|
+
if (spec) args.push('-R', spec);
|
|
176
|
+
stripRes = runCli('glab', args);
|
|
147
177
|
}
|
|
148
178
|
|
|
149
179
|
if (!stripRes.ok) {
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* label-scope.mjs — scope-tolerant label comparison.
|
|
3
|
+
*
|
|
4
|
+
* The canonical priority-label spelling is the GitLab SCOPED form
|
|
5
|
+
* `priority::<level>`. Producers were migrated to it because the instance had
|
|
6
|
+
* drifted to 416 scoped / 249 unscoped / 7 bare labels, and a data migration
|
|
7
|
+
* that is not preceded by a producer migration is undone within a day.
|
|
8
|
+
*
|
|
9
|
+
* The DATA migration is a separate, later step, so for now both spellings
|
|
10
|
+
* coexist on real issues. Every consumer that MATCHES a label therefore
|
|
11
|
+
* compares through `normalizeLabel()`, which collapses the scope separator:
|
|
12
|
+
* `priority::high` and `priority:high` are the same label for matching
|
|
13
|
+
* purposes, while the emitted/default spelling stays canonical.
|
|
14
|
+
*
|
|
15
|
+
* Deliberately NOT applied to label WRITES — those always emit the canonical
|
|
16
|
+
* scoped form.
|
|
17
|
+
*
|
|
18
|
+
* Stdlib-free leaf module.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Collapse a scoped label's double colon to a single colon and lowercase it,
|
|
23
|
+
* yielding a comparison key that is identical for `key::value` and `key:value`.
|
|
24
|
+
*
|
|
25
|
+
* @param {unknown} label
|
|
26
|
+
* @returns {string} normalized comparison key ('' for non-strings)
|
|
27
|
+
*/
|
|
28
|
+
export function normalizeLabel(label) {
|
|
29
|
+
if (typeof label !== 'string') return '';
|
|
30
|
+
return label.trim().toLowerCase().replace(/::/g, ':');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Build a Set of normalized comparison keys from a label list.
|
|
35
|
+
*
|
|
36
|
+
* @param {unknown} labels
|
|
37
|
+
* @returns {Set<string>}
|
|
38
|
+
*/
|
|
39
|
+
export function normalizedLabelSet(labels) {
|
|
40
|
+
const set = new Set();
|
|
41
|
+
if (!Array.isArray(labels)) return set;
|
|
42
|
+
for (const l of labels) {
|
|
43
|
+
const key = normalizeLabel(l);
|
|
44
|
+
if (key) set.add(key);
|
|
45
|
+
}
|
|
46
|
+
return set;
|
|
47
|
+
}
|
|
@@ -83,8 +83,13 @@ export const LEARNING_TYPE_REGISTRY = Object.freeze({
|
|
|
83
83
|
'fragile-file': Object.freeze({ ttlDays: 45, agentProposable: true, ruleConvertible: true }),
|
|
84
84
|
'effective-sizing': Object.freeze({ ttlDays: 45, agentProposable: true, ruleConvertible: false }),
|
|
85
85
|
'recurring-issue': Object.freeze({ ttlDays: 45, agentProposable: true, ruleConvertible: true }),
|
|
86
|
-
|
|
87
|
-
|
|
86
|
+
// workflow-pattern / proven-pattern: flipped ruleConvertible false->true
|
|
87
|
+
// (issue #900) — the real corpus census showed a large volume of live
|
|
88
|
+
// `workflow-pattern`/`proven-pattern` records (post type-alias-normalization,
|
|
89
|
+
// see LEARNING_TYPE_ALIASES below) that carried usable file_paths but were
|
|
90
|
+
// structurally unconvertible before this flip.
|
|
91
|
+
'workflow-pattern': Object.freeze({ ttlDays: 90, agentProposable: true, ruleConvertible: true }),
|
|
92
|
+
'proven-pattern': Object.freeze({ ttlDays: 90, agentProposable: true, ruleConvertible: true }),
|
|
88
93
|
'anti-pattern': Object.freeze({ ttlDays: 90, agentProposable: true, ruleConvertible: true }),
|
|
89
94
|
'autopilot-effectiveness': Object.freeze({ ttlDays: 90, agentProposable: true, ruleConvertible: false }),
|
|
90
95
|
// autonomy-verdict (#683): repo/scope readiness synthesis from autopilot
|
|
@@ -106,6 +111,30 @@ export const LEARNING_TYPE_REGISTRY = Object.freeze({
|
|
|
106
111
|
'stagnation-class-frequency': Object.freeze({ ttlDays: 60, agentProposable: false, ruleConvertible: true }),
|
|
107
112
|
});
|
|
108
113
|
|
|
114
|
+
/**
|
|
115
|
+
* Free-form producer-dialect type names -> their canonical
|
|
116
|
+
* {@link LEARNING_TYPE_REGISTRY} counterpart (issue #900). The real corpus
|
|
117
|
+
* accumulated learnings stamped with type names that were never registered
|
|
118
|
+
* (`gotcha`, `pattern`, ...) — these are semantically the SAME classes as
|
|
119
|
+
* two registered types, just written with a different literal. Applied by
|
|
120
|
+
* {@link normalizeDialects} on both the read funnel (`normalizeLearning`)
|
|
121
|
+
* and the write/migration funnel (`migrateLegacyLearning`), so every
|
|
122
|
+
* downstream consumer (TTL derivation, `CONVERT_TYPES` / `PROPOSAL_TYPES`
|
|
123
|
+
* membership, reconcile eligibility) sees one canonical type space —
|
|
124
|
+
* mirrors the existing `files` -> `file_paths` dialect-normalization
|
|
125
|
+
* pattern one level up (type name instead of field name).
|
|
126
|
+
*
|
|
127
|
+
* INVARIANT (guarded by a test): no alias KEY may collide with a
|
|
128
|
+
* `LEARNING_TYPE_REGISTRY` key — a collision would silently rewrite an
|
|
129
|
+
* already-canonical type into a different one.
|
|
130
|
+
*
|
|
131
|
+
* @type {Readonly<Record<string, string>>}
|
|
132
|
+
*/
|
|
133
|
+
export const LEARNING_TYPE_ALIASES = Object.freeze({
|
|
134
|
+
gotcha: 'anti-pattern',
|
|
135
|
+
pattern: 'proven-pattern',
|
|
136
|
+
});
|
|
137
|
+
|
|
109
138
|
/**
|
|
110
139
|
* Per-type TTL policy (in days) for `expires_at` derivation. Derived from
|
|
111
140
|
* {@link LEARNING_TYPE_REGISTRY} — see parent module documentation for tier
|
|
@@ -275,7 +304,10 @@ const _warnedSessionIdConflict = new Set();
|
|
|
275
304
|
* session_id/source_session conflict (see below); idempotent; never throws;
|
|
276
305
|
* non-objects (and arrays) pass through unchanged. Does NOT mutate its input.
|
|
277
306
|
*
|
|
278
|
-
* Dialect rules (Epic #723 B2 — census 2026-07-02
|
|
307
|
+
* Dialect rules (Epic #723 B2 — census 2026-07-02; type-alias rule added
|
|
308
|
+
* issue #900):
|
|
309
|
+
* - `type` → canonical type — resolved via {@link LEARNING_TYPE_ALIASES}
|
|
310
|
+
* (e.g. `gotcha` → `anti-pattern`, `pattern` → `proven-pattern`).
|
|
279
311
|
* - `files` → `file_paths` — verbatim value move; an empty array is
|
|
280
312
|
* preserved as an empty `file_paths`; a canonical
|
|
281
313
|
* `file_paths` already present wins (legacy `files` dropped).
|
|
@@ -310,6 +342,14 @@ export function normalizeDialects(entry, { reserializeTimestamps = true } = {})
|
|
|
310
342
|
|
|
311
343
|
const out = { ...entry };
|
|
312
344
|
|
|
345
|
+
// type alias resolution (#900) — canonicalize free-form producer type names
|
|
346
|
+
// (e.g. `gotcha`, `pattern`) to their LEARNING_TYPE_REGISTRY counterpart so
|
|
347
|
+
// every downstream consumer (TTL derivation, CONVERT_TYPES / PROPOSAL_TYPES
|
|
348
|
+
// membership, reconcile eligibility) sees one canonical type space.
|
|
349
|
+
if (typeof out.type === 'string' && out.type in LEARNING_TYPE_ALIASES) {
|
|
350
|
+
out.type = LEARNING_TYPE_ALIASES[out.type];
|
|
351
|
+
}
|
|
352
|
+
|
|
313
353
|
// files → file_paths (verbatim value move; canonical wins; empty array kept).
|
|
314
354
|
if ('files' in out) {
|
|
315
355
|
if (!('file_paths' in out)) {
|
|
@@ -70,7 +70,6 @@ import { readLock, isLockLive, isPidAliveOnHost, LOCK_PATH, DEFAULT_TTL_HOURS }
|
|
|
70
70
|
import { emitEvent } from './events.mjs';
|
|
71
71
|
|
|
72
72
|
const REAPED_ARCHIVE_SUBDIR = '.orchestrator/tmp/reaped-locks';
|
|
73
|
-
const EVENTS_RELPATH = '.orchestrator/metrics/events.jsonl';
|
|
74
73
|
const REAPED_EVENT = 'orchestrator.session.lock.reaped';
|
|
75
74
|
const CURRENT_SESSION_RELPATH = '.orchestrator/current-session.json';
|
|
76
75
|
|
|
@@ -550,7 +549,7 @@ async function evaluateRepo(repoRoot, { nowMs, dryRun, currentSessionId, reapMod
|
|
|
550
549
|
reap_mode: reapMode,
|
|
551
550
|
current_session: currentSession,
|
|
552
551
|
},
|
|
553
|
-
{
|
|
552
|
+
{ repoRoot },
|
|
554
553
|
);
|
|
555
554
|
} catch {
|
|
556
555
|
// Observability is best-effort — the archive-move already succeeded.
|
|
@@ -22,7 +22,11 @@
|
|
|
22
22
|
* schema_version 1
|
|
23
23
|
*
|
|
24
24
|
* Optional fields:
|
|
25
|
-
* proposed_by_agent string | undefined
|
|
25
|
+
* proposed_by_agent string | undefined — identifier of the submitting agent
|
|
26
|
+
* file_paths string[] | undefined — repo-relative path(s) this
|
|
27
|
+
* proposal applies to (issue #900 C). Omitted entirely when empty/absent
|
|
28
|
+
* — never set to `[]`. Required, downstream, for a promoted learning to
|
|
29
|
+
* ever become /reconcile-eligible (see `reconcile/eligibility.mjs`).
|
|
26
30
|
*/
|
|
27
31
|
|
|
28
32
|
import { randomUUID } from 'node:crypto';
|
|
@@ -86,6 +90,9 @@ const EVIDENCE_MAX = 5000;
|
|
|
86
90
|
* @param {number} opts.confidence — [0, 1]
|
|
87
91
|
* @param {string} opts.waveId — e.g. 'W2'
|
|
88
92
|
* @param {string} [opts.proposedByAgent] — optional agent identifier
|
|
93
|
+
* @param {string[]} [opts.filePaths] — optional repo-relative path(s) this
|
|
94
|
+
* proposal applies to (issue #900 C). Set on the record ONLY when a
|
|
95
|
+
* non-empty array is supplied — mirrors the `proposedByAgent` pattern.
|
|
89
96
|
* @returns {object} complete proposal record
|
|
90
97
|
*/
|
|
91
98
|
export function createProposalRecord({
|
|
@@ -96,6 +103,7 @@ export function createProposalRecord({
|
|
|
96
103
|
confidence,
|
|
97
104
|
waveId,
|
|
98
105
|
proposedByAgent,
|
|
106
|
+
filePaths,
|
|
99
107
|
}) {
|
|
100
108
|
const record = {
|
|
101
109
|
id: randomUUID(),
|
|
@@ -113,6 +121,10 @@ export function createProposalRecord({
|
|
|
113
121
|
record.proposed_by_agent = proposedByAgent;
|
|
114
122
|
}
|
|
115
123
|
|
|
124
|
+
if (Array.isArray(filePaths) && filePaths.length > 0) {
|
|
125
|
+
record.file_paths = filePaths;
|
|
126
|
+
}
|
|
127
|
+
|
|
116
128
|
return record;
|
|
117
129
|
}
|
|
118
130
|
|
|
@@ -238,6 +250,29 @@ export function validateProposalRecord(record) {
|
|
|
238
250
|
errors.push('proposed_by_agent must be a string when present');
|
|
239
251
|
}
|
|
240
252
|
|
|
253
|
+
// file_paths (optional, but if present must be a non-empty array of
|
|
254
|
+
// non-empty, newline-free, glob-metacharacter-free strings) — issue #900 C,
|
|
255
|
+
// Q3-MED fix pass: a glob metacharacter (* ? [ ] { }) surviving this schema
|
|
256
|
+
// gate would later reach emitter.mjs::globsFromFilePaths verbatim for a
|
|
257
|
+
// top-level (dirname==='.') entry, effectively producing an always-on rule
|
|
258
|
+
// glob (e.g. `file_paths: ['**']`). `createProposalRecord` never sets an
|
|
259
|
+
// empty array (it omits the key), so a present-but-empty `[]` is itself a
|
|
260
|
+
// signal of a malformed/tampered record — reject it explicitly.
|
|
261
|
+
if ('file_paths' in record) {
|
|
262
|
+
const fp = record.file_paths;
|
|
263
|
+
const isValidArray =
|
|
264
|
+
Array.isArray(fp) &&
|
|
265
|
+
fp.length > 0 &&
|
|
266
|
+
fp.every(
|
|
267
|
+
(p) => typeof p === 'string' && p.length > 0 && !/[\r\n]/.test(p) && !/[*?[\]{}]/.test(p),
|
|
268
|
+
);
|
|
269
|
+
if (!isValidArray) {
|
|
270
|
+
errors.push(
|
|
271
|
+
'file_paths must be a non-empty array of non-empty, newline-free, glob-metacharacter-free strings when present',
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
241
276
|
if (errors.length > 0) {
|
|
242
277
|
return { ok: false, errors };
|
|
243
278
|
}
|