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.
Files changed (187) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.codex-plugin/plugin.json +1 -1
  4. package/.cursor/rules/030-wave-execution.mdc +17 -1
  5. package/CHANGELOG.md +105 -412
  6. package/README.md +12 -9
  7. package/SECURITY.md +190 -27
  8. package/agents/AGENTS.md +20 -3
  9. package/agents/code-implementer.md +6 -6
  10. package/agents/db-specialist.md +1 -1
  11. package/agents/qa-strategist.md +31 -6
  12. package/agents/schemas/qa-strategist.schema.json +27 -0
  13. package/agents/schemas/test-writer.schema.json +60 -2
  14. package/agents/security-reviewer.md +1 -1
  15. package/agents/session-reviewer.md +1 -1
  16. package/agents/test-writer.md +29 -10
  17. package/agents/ui-developer.md +1 -1
  18. package/commands/contract-version-bump.md +28 -0
  19. package/commands/portfolio.md +1 -1
  20. package/docs/USER-GUIDE.md +8 -3
  21. package/docs/ci-setup.md +121 -7
  22. package/docs/codex-setup.md +1 -1
  23. package/docs/components.md +6 -6
  24. package/docs/cursor-setup.md +22 -9
  25. package/docs/events-schema.md +5 -1
  26. package/docs/instruction-delivery.md +444 -0
  27. package/docs/rule-authoring.md +58 -9
  28. package/docs/session-config-reference.md +244 -9
  29. package/docs/session-config-template.md +39 -3
  30. package/hooks/_lib/guard-source-loader.mjs +467 -0
  31. package/hooks/_lib/lock-bootstrap.mjs +21 -0
  32. package/hooks/_lib/vcs-create-matcher.mjs +119 -0
  33. package/hooks/config-protection.mjs +0 -0
  34. package/hooks/enforce-commands.mjs +10 -2
  35. package/hooks/hooks-codex.json +1 -1
  36. package/hooks/hooks-cursor.json +11 -2
  37. package/hooks/hooks-pi.json +10 -0
  38. package/hooks/hooks.json +21 -1
  39. package/hooks/on-session-end.mjs +178 -18
  40. package/hooks/on-session-start.mjs +23 -0
  41. package/hooks/post-bash-write-verify.mjs +977 -0
  42. package/hooks/post-subagent-discovery-validator.mjs +256 -41
  43. package/hooks/pre-bash-destructive-guard.mjs +525 -160
  44. package/hooks/pre-bash-issue-budget.mjs +167 -0
  45. package/hooks/pre-bash-sessions-ledger-guard.mjs +627 -0
  46. package/hooks/pre-bash-templates-first.mjs +96 -63
  47. package/hooks/subagent-telemetry.mjs +527 -37
  48. package/package.json +5 -2
  49. package/pi/prompts/contract-version-bump.md +12 -0
  50. package/rules/README.md +32 -0
  51. package/scripts/archive-closed-prds.mjs +12 -22
  52. package/scripts/autopilot-multi.mjs +103 -20
  53. package/scripts/backfill-abandoned-sessions.mjs +160 -4
  54. package/scripts/check-doc-consistency.sh +17 -1
  55. package/scripts/eval-session.mjs +50 -9
  56. package/scripts/fleet-instruction-scan.mjs +141 -0
  57. package/scripts/lib/autopilot/mr-draft.mjs +31 -1
  58. package/scripts/lib/autopilot/worktree-pipeline.mjs +113 -5
  59. package/scripts/lib/backlog-scan.mjs +39 -6
  60. package/scripts/lib/blocked-commands-policy.mjs +340 -0
  61. package/scripts/lib/ci-status-banner.mjs +75 -12
  62. package/scripts/lib/claude-md-budget-lint.mjs +283 -34
  63. package/scripts/lib/command-blocker.mjs +1013 -58
  64. package/scripts/lib/config/config-protection.mjs +2 -1
  65. package/scripts/lib/config/drift-check.mjs +9 -1
  66. package/scripts/lib/config/gitlab-portfolio.mjs +1 -1
  67. package/scripts/lib/config/issue-budget.mjs +123 -0
  68. package/scripts/lib/config/reconcile.mjs +21 -0
  69. package/scripts/lib/config/section-extractor.mjs +121 -1
  70. package/scripts/lib/config-schema.mjs +23 -3
  71. package/scripts/lib/config.mjs +17 -0
  72. package/scripts/lib/convergence-monitor.mjs +49 -3
  73. package/scripts/lib/description-surface.mjs +535 -0
  74. package/scripts/lib/dispatcher/enumerate.mjs +26 -40
  75. package/scripts/lib/ecosystem-wizard/config-writer.mjs +26 -24
  76. package/scripts/lib/ecosystem-wizard/wizard-prompt.mjs +1 -1
  77. package/scripts/lib/eval/engine.mjs +47 -5
  78. package/scripts/lib/events.mjs +59 -7
  79. package/scripts/lib/gates/gate-full.mjs +15 -3
  80. package/scripts/lib/gates/gate-helpers.mjs +132 -6
  81. package/scripts/lib/gitlab-ops/stale-mr-sweep.mjs +28 -8
  82. package/scripts/lib/gitlab-portfolio/aggregator.mjs +8 -2
  83. package/scripts/lib/gitlab-portfolio/cli.mjs +1 -1
  84. package/scripts/lib/handover-gate.mjs +7 -3
  85. package/scripts/lib/harness-audit/categories/category4.mjs +9 -3
  86. package/scripts/lib/instruction-budget-guard.mjs +402 -51
  87. package/scripts/lib/io.mjs +345 -10
  88. package/scripts/lib/issue-budget.mjs +269 -0
  89. package/scripts/lib/issue-close-strip-labels.mjs +39 -9
  90. package/scripts/lib/label-scope.mjs +47 -0
  91. package/scripts/lib/learnings/schema.mjs +43 -3
  92. package/scripts/lib/lock-reaper.mjs +1 -2
  93. package/scripts/lib/memory-proposals/schema.mjs +36 -1
  94. package/scripts/lib/peer-discovery.mjs +645 -0
  95. package/scripts/lib/pi-hook-bridge.mjs +146 -17
  96. package/scripts/lib/product-repo-detect.mjs +9 -8
  97. package/scripts/lib/project-hygiene.mjs +432 -0
  98. package/scripts/lib/quality-gate.mjs +167 -0
  99. package/scripts/lib/recommendations-v0.mjs +1 -1
  100. package/scripts/lib/reconcile/eligibility.mjs +1 -1
  101. package/scripts/lib/reconcile/emitter.mjs +23 -4
  102. package/scripts/lib/reconcile/engine.mjs +147 -39
  103. package/scripts/lib/reconcile/idempotency.mjs +114 -14
  104. package/scripts/lib/reconcile-nudge-banner.mjs +65 -9
  105. package/scripts/lib/resource-probe/evaluate.mjs +70 -4
  106. package/scripts/lib/resource-probe.mjs +19 -0
  107. package/scripts/lib/rule-loader.mjs +6 -0
  108. package/scripts/lib/scope-baseline.mjs +564 -0
  109. package/scripts/lib/scope-gate.mjs +399 -98
  110. package/scripts/lib/session-close-backfill.mjs +61 -6
  111. package/scripts/lib/session-end/phase-skip.mjs +1 -0
  112. package/scripts/lib/session-id.mjs +221 -41
  113. package/scripts/lib/session-lock.mjs +304 -6
  114. package/scripts/lib/session-schema/constants.mjs +22 -3
  115. package/scripts/lib/session-schema/validator.mjs +16 -0
  116. package/scripts/lib/sessions-integrity-banner.mjs +294 -0
  117. package/scripts/lib/sessions-staleness-banner.mjs +121 -12
  118. package/scripts/lib/skill-evolution/idempotency.mjs +135 -16
  119. package/scripts/lib/skill-evolution/mr-opener.mjs +9 -1
  120. package/scripts/lib/spiral-carryover.mjs +142 -30
  121. package/scripts/lib/state-md/mission-status.mjs +53 -3
  122. package/scripts/lib/subagents-schema.mjs +43 -9
  123. package/scripts/lib/test-runner/issue-reconcile.mjs +53 -13
  124. package/scripts/lib/tests-src-ratio.mjs +484 -0
  125. package/scripts/lib/validate/check-agents.mjs +56 -0
  126. package/scripts/lib/validate/check-hooks-symmetry.mjs +244 -10
  127. package/scripts/lib/validate/check-rules.mjs +217 -35
  128. package/scripts/lib/validate/check-test-value-bans.mjs +782 -0
  129. package/scripts/lib/validate/check-unicode-safety.mjs +1 -0
  130. package/scripts/lib/validate-vendored-rules.mjs +10 -2
  131. package/scripts/lib/vault-archive.mjs +17 -2
  132. package/scripts/lib/vault-backfill/glab.mjs +8 -0
  133. package/scripts/lib/vault-mirror/process.mjs +30 -0
  134. package/scripts/lib/vault-mirror/render-sessions.mjs +293 -36
  135. package/scripts/lib/vcs-repo-spec.mjs +362 -0
  136. package/scripts/lib/wave-resource-gate.mjs +115 -11
  137. package/scripts/lib/worktree/listing.mjs +44 -7
  138. package/scripts/mcp-server.sh +17 -3
  139. package/scripts/measure-context-overhead.sh +151 -0
  140. package/scripts/memory-propose.mjs +72 -9
  141. package/scripts/print-applicable-rules.mjs +51 -12
  142. package/scripts/release.mjs +534 -0
  143. package/scripts/run-quality-gate.mjs +123 -5
  144. package/scripts/validate-wave-scope.mjs +182 -17
  145. package/scripts/vault-integration-watcher.mjs +32 -10
  146. package/skills/_shared/config-reading.md +2 -2
  147. package/skills/bootstrap/fast-template.md +1 -1
  148. package/skills/claude-md-drift-check/checker.mjs +145 -28
  149. package/skills/contract-version-bump/SKILL.md +219 -0
  150. package/skills/discovery/SKILL.md +4 -4
  151. package/skills/discovery/issue-templates.md +11 -11
  152. package/skills/discovery/probes-audit.md +1 -1
  153. package/skills/discovery/probes-feature.md +1 -1
  154. package/skills/discovery/probes-session.md +26 -5
  155. package/skills/ecosystem-health/SKILL.md +1 -1
  156. package/skills/ecosystem-health/wizard.md +4 -4
  157. package/skills/evolve/SKILL.md +1 -0
  158. package/skills/gitlab-ops/SKILL.md +20 -12
  159. package/skills/gitlab-portfolio/SKILL.md +2 -2
  160. package/skills/hook-development/SKILL.md +1 -1
  161. package/skills/mode-selector/SKILL.md +1 -1
  162. package/skills/npm-publish/SKILL.md +17 -1
  163. package/skills/plan/SKILL.md +5 -5
  164. package/skills/plan/mode-feature.md +4 -4
  165. package/skills/plan/mode-new.md +10 -10
  166. package/skills/plan/mode-retro.md +1 -1
  167. package/skills/quality-gates/SKILL.md +1 -1
  168. package/skills/reconcile/SKILL.md +21 -4
  169. package/skills/session-end/SKILL.md +34 -13
  170. package/skills/session-end/discovery-scan.md +4 -2
  171. package/skills/session-end/drift-operations.md +4 -4
  172. package/skills/session-end/metrics-collection.md +13 -0
  173. package/skills/session-end/phase-3-2-docs-verification.md +1 -1
  174. package/skills/session-end/phase-3-6-tail.md +2 -1
  175. package/skills/session-end/plan-verification.md +5 -2
  176. package/skills/session-end/vault-operations.md +1 -1
  177. package/skills/session-end/verification-checklist.md +1 -1
  178. package/skills/session-plan/SKILL.md +6 -2
  179. package/skills/session-plan/wave-template.md +2 -0
  180. package/skills/session-start/SKILL.md +73 -7
  181. package/skills/session-start/phase-4-5-resource-health.md +15 -2
  182. package/skills/test-runner/SKILL.md +2 -2
  183. package/skills/vault-sync/validator.mjs +108 -7
  184. package/skills/wave-executor/SKILL.md +5 -2
  185. package/skills/wave-executor/circuit-breaker.md +2 -0
  186. package/skills/wave-executor/wave-loop.md +163 -10
  187. 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 res = runCli('gh', ['issue', 'view', id, '--json', 'labels']);
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 res = runCli('glab', ['issue', 'view', id, '--output', 'json']);
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:high", ...] }
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({ issueId, vcs = 'gitlab' } = {}) {
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
- stripRes = runCli('glab', ['issue', 'update', id, '--unlabel', toStrip.join(',')]);
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
- 'workflow-pattern': Object.freeze({ ttlDays: 90, agentProposable: true, ruleConvertible: false }),
87
- 'proven-pattern': Object.freeze({ ttlDays: 90, agentProposable: true, ruleConvertible: false }),
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
- { filePath: path.join(repoRoot, EVENTS_RELPATH) },
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 — identifier of the submitting agent
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
  }