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
|
@@ -12,7 +12,18 @@
|
|
|
12
12
|
* full via tmp-file + atomic rename. It is NEVER appended to with `>>` semantics,
|
|
13
13
|
* because supersession reconciliation must mutate existing lines (set
|
|
14
14
|
* `superseded_by`) — an append would leave stale duplicates and corrupt the
|
|
15
|
-
* supersession graph.
|
|
15
|
+
* supersession graph. The store is OWNED by this module: nothing else — no
|
|
16
|
+
* report, no analysis run, no agent — may append to it; a read-side shape guard
|
|
17
|
+
* ({@link isRepairCandidateShape}) drops any record that is not a
|
|
18
|
+
* RepairCandidate, COUNTS the drop, and WARNs on stderr.
|
|
19
|
+
*
|
|
20
|
+
* The warning lives in {@link readStore} — the ONE seam every public function
|
|
21
|
+
* reads through — rather than in each caller's return shape, because only
|
|
22
|
+
* `mergeCandidates` has a result object with room for a count: `markProcessed`
|
|
23
|
+
* returns `{ok, reason}` and `isProcessed` returns a boolean, yet both rewrite
|
|
24
|
+
* or gate on a store whose foreign lines the rewrite DELETES. Those two run far
|
|
25
|
+
* more often than a merge, so a count reported only by `mergeCandidates` would
|
|
26
|
+
* leave the most-travelled deletion path silent.
|
|
16
27
|
*
|
|
17
28
|
* Three responsibilities:
|
|
18
29
|
* 1. Persistence — `loadCandidates` (read) + atomic full rewrite (internal).
|
|
@@ -63,12 +74,90 @@ function resolveStorePath(repoRoot, storePath) {
|
|
|
63
74
|
return rel;
|
|
64
75
|
}
|
|
65
76
|
|
|
77
|
+
/**
|
|
78
|
+
* Minimal shape guard for a persisted store line. A record is accepted only when
|
|
79
|
+
* it carries the three fields THIS store's own consumers read:
|
|
80
|
+
* - `id` — THE idempotency key (`mergeCandidates` byId index,
|
|
81
|
+
* `markProcessed`, `isProcessed` all look records up by it).
|
|
82
|
+
* - `source` — one half of the supersession key (`mergeCandidates`
|
|
83
|
+
* compares `rec.source === cand.source`).
|
|
84
|
+
* - `target_path` — the other half of the supersession key.
|
|
85
|
+
*
|
|
86
|
+
* Deliberately NOT a full schema check, and NOT a copy of the reconcile store's
|
|
87
|
+
* guard: the two record types share only 5 fields, and the reconcile guard keys
|
|
88
|
+
* on `learning_key`, which a {@link RepairCandidate} does not have at all —
|
|
89
|
+
* applying it here would reject 100% of legitimate repair candidates. The fields
|
|
90
|
+
* above were chosen by what consumers actually READ, not by the typedef: a grep
|
|
91
|
+
* of `scripts/lib/skill-evolution/` (2026-07-31) found ZERO consumer reads of
|
|
92
|
+
* `created_at` — only the producer `candidate-intake.mjs` sets it — so requiring
|
|
93
|
+
* it would be schema validation, not a load-bearing check. The store is a
|
|
94
|
+
* mutable work-queue whose records may gain fields across schema versions, so
|
|
95
|
+
* over-strict validation would silently drop legitimate future records.
|
|
96
|
+
*
|
|
97
|
+
* What it DOES reject is a record no writer in this repo produces — the concrete
|
|
98
|
+
* incident class being a hand-written report artefact landing in the sibling
|
|
99
|
+
* reconcile store using `candidate_id` / `generated_at` / `status:"candidate"`
|
|
100
|
+
* (2026-07-31, GitLab #955; this store is the near-twin that kept the old
|
|
101
|
+
* permissive `typeof parsed === 'object'` check).
|
|
102
|
+
* @param {unknown} rec
|
|
103
|
+
* @returns {boolean}
|
|
104
|
+
*/
|
|
105
|
+
function isRepairCandidateShape(rec) {
|
|
106
|
+
if (!rec || typeof rec !== 'object' || Array.isArray(rec)) return false;
|
|
107
|
+
const r = /** @type {Record<string, unknown>} */ (rec);
|
|
108
|
+
if (typeof r.id !== 'string' || r.id.length === 0) return false;
|
|
109
|
+
if (typeof r.source !== 'string' || r.source.length === 0) return false;
|
|
110
|
+
if (typeof r.target_path !== 'string' || r.target_path.length === 0) return false;
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Per-store memo of the skip count already WARNed about, keyed by resolved
|
|
116
|
+
* absolute store path. Without it a caller that reads the same store in a loop
|
|
117
|
+
* (the engine calls {@link isProcessed} once per candidate) would emit one
|
|
118
|
+
* identical line per read. A read that comes back CLEAN clears the entry, so a
|
|
119
|
+
* store contaminated again later warns again instead of being muted forever.
|
|
120
|
+
* @type {Map<string, number>}
|
|
121
|
+
*/
|
|
122
|
+
const warnedSkips = new Map();
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Emit ONE stderr WARN per (store, skip-count) transition. Called from every
|
|
126
|
+
* {@link readStore} exit — including the ENOENT path, whose `skipped: 0` clears
|
|
127
|
+
* the memo. Never throws: a stderr write failure must not break a store read.
|
|
128
|
+
* @param {string} absPath
|
|
129
|
+
* @param {number} skipped
|
|
130
|
+
* @returns {void}
|
|
131
|
+
*/
|
|
132
|
+
function warnOnSkipped(absPath, skipped) {
|
|
133
|
+
if (skipped <= 0) {
|
|
134
|
+
warnedSkips.delete(absPath);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
if (warnedSkips.get(absPath) === skipped) return;
|
|
138
|
+
warnedSkips.set(absPath, skipped);
|
|
139
|
+
try {
|
|
140
|
+
process.stderr.write(
|
|
141
|
+
`WARN repair-candidate store: ${skipped} unreadable record(s) skipped in ${absPath} — ` +
|
|
142
|
+
'not RepairCandidate shape (need id/source/target_path) or unparseable JSON. ' +
|
|
143
|
+
'The next full rewrite (mergeCandidates/markProcessed) DELETES them from disk.\n',
|
|
144
|
+
);
|
|
145
|
+
} catch {
|
|
146
|
+
// stderr unavailable (closed pipe) — the count is still returned to callers.
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
66
150
|
/**
|
|
67
151
|
* Read + defensively parse the store's JSONL lines into RepairCandidate records.
|
|
68
|
-
* Malformed lines (bad JSON
|
|
69
|
-
*
|
|
152
|
+
* Malformed lines (bad JSON) and shape-foreign records (see
|
|
153
|
+
* {@link isRepairCandidateShape}) are skipped — and COUNTED, because every
|
|
154
|
+
* writer in this module rewrites the store in FULL, so a skipped line is dropped
|
|
155
|
+
* from disk on the next merge or stamp; a silent drop would be unattributable
|
|
156
|
+
* data loss. Every skip is ALSO reported on stderr via {@link warnOnSkipped},
|
|
157
|
+
* which is what covers the three callers whose return contract has no room for
|
|
158
|
+
* the count. A missing file yields `{ records: [], skipped: 0 }`. Never throws.
|
|
70
159
|
* @param {string} absPath
|
|
71
|
-
* @returns {RepairCandidate[]}
|
|
160
|
+
* @returns {{ records: RepairCandidate[], skipped: number }}
|
|
72
161
|
*/
|
|
73
162
|
function readStore(absPath) {
|
|
74
163
|
let raw;
|
|
@@ -76,11 +165,13 @@ function readStore(absPath) {
|
|
|
76
165
|
raw = readFileSync(absPath, 'utf8');
|
|
77
166
|
} catch {
|
|
78
167
|
// ENOENT or any read error → empty store.
|
|
79
|
-
|
|
168
|
+
warnOnSkipped(absPath, 0);
|
|
169
|
+
return { records: [], skipped: 0 };
|
|
80
170
|
}
|
|
81
171
|
|
|
82
172
|
/** @type {RepairCandidate[]} */
|
|
83
173
|
const records = [];
|
|
174
|
+
let skipped = 0;
|
|
84
175
|
for (const line of raw.split('\n')) {
|
|
85
176
|
const trimmed = line.trim();
|
|
86
177
|
if (trimmed.length === 0) continue;
|
|
@@ -88,13 +179,17 @@ function readStore(absPath) {
|
|
|
88
179
|
try {
|
|
89
180
|
parsed = JSON.parse(trimmed);
|
|
90
181
|
} catch {
|
|
182
|
+
skipped += 1;
|
|
91
183
|
continue; // skip malformed line
|
|
92
184
|
}
|
|
93
|
-
if (
|
|
185
|
+
if (isRepairCandidateShape(parsed)) {
|
|
94
186
|
records.push(/** @type {RepairCandidate} */ (parsed));
|
|
187
|
+
} else {
|
|
188
|
+
skipped += 1;
|
|
95
189
|
}
|
|
96
190
|
}
|
|
97
|
-
|
|
191
|
+
warnOnSkipped(absPath, skipped);
|
|
192
|
+
return { records, skipped };
|
|
98
193
|
}
|
|
99
194
|
|
|
100
195
|
/**
|
|
@@ -127,8 +222,15 @@ function writeStore(absPath, records) {
|
|
|
127
222
|
|
|
128
223
|
/**
|
|
129
224
|
* Load every RepairCandidate currently persisted in the store. Reads JSONL,
|
|
130
|
-
* skips malformed lines
|
|
131
|
-
*
|
|
225
|
+
* skips malformed lines AND shape-foreign records (missing `id` / `source` /
|
|
226
|
+
* `target_path`), returns `[]` for a missing file. Does NOT create the runtime
|
|
227
|
+
* dir (mkdir -p happens only on write). Never throws.
|
|
228
|
+
*
|
|
229
|
+
* Read-only: the skipped lines are still on disk after this call. The count is
|
|
230
|
+
* not in the return value — this function's plain-array contract is unchanged —
|
|
231
|
+
* but it is never silent: {@link readStore} WARNs it on stderr, and a caller
|
|
232
|
+
* that needs the number programmatically gets it from {@link mergeCandidates}
|
|
233
|
+
* as `dropped_on_read`.
|
|
132
234
|
* @param {Object} params
|
|
133
235
|
* @param {string} [params.repoRoot] - repo root; relative `storePath` is resolved against it.
|
|
134
236
|
* @param {string} [params.storePath] - store path (relative ⇒ joined to repoRoot). Defaults to {@link DEFAULT_STORE_PATH}.
|
|
@@ -136,7 +238,7 @@ function writeStore(absPath, records) {
|
|
|
136
238
|
*/
|
|
137
239
|
export function loadCandidates({ repoRoot, storePath } = {}) {
|
|
138
240
|
const absPath = resolveStorePath(repoRoot, storePath);
|
|
139
|
-
return readStore(absPath);
|
|
241
|
+
return readStore(absPath).records;
|
|
140
242
|
}
|
|
141
243
|
|
|
142
244
|
/**
|
|
@@ -157,15 +259,25 @@ export function loadCandidates({ repoRoot, storePath } = {}) {
|
|
|
157
259
|
* The store is rewritten in full (read-all → merge → atomic tmp+rename), never
|
|
158
260
|
* appended. Never throws; a write failure still returns counts with
|
|
159
261
|
* `total` reflecting the in-memory final line count.
|
|
262
|
+
*
|
|
263
|
+
* `dropped_on_read` reports how many PERSISTED lines the read-side guard
|
|
264
|
+
* rejected (malformed JSON, or a record missing `id`/`source`/`target_path`).
|
|
265
|
+
* Because the store is rewritten in full, those lines are DROPPED from disk by
|
|
266
|
+
* this call — the count is what makes that loss attributable instead of silent.
|
|
267
|
+
* It is deliberately NOT named `skipped`: `skipped_processed` above already
|
|
268
|
+
* means "an INCOMING candidate was skipped because its id is already processed",
|
|
269
|
+
* a different population (incoming, not persisted) with a different cause
|
|
270
|
+
* (terminal verdict, not contamination). Confusing the two would read a
|
|
271
|
+
* contaminated store as a healthy idempotent no-op.
|
|
160
272
|
* @param {Object} params
|
|
161
273
|
* @param {RepairCandidate[]} params.candidates - newly minted candidates to merge.
|
|
162
274
|
* @param {string} [params.repoRoot]
|
|
163
275
|
* @param {string} [params.storePath]
|
|
164
|
-
* @returns {{ written: number, superseded: number, skipped_processed: number, total: number }}
|
|
276
|
+
* @returns {{ written: number, superseded: number, skipped_processed: number, dropped_on_read: number, total: number }}
|
|
165
277
|
*/
|
|
166
278
|
export function mergeCandidates({ candidates, repoRoot, storePath } = {}) {
|
|
167
279
|
const absPath = resolveStorePath(repoRoot, storePath);
|
|
168
|
-
const store = readStore(absPath);
|
|
280
|
+
const { records: store, skipped: droppedOnRead } = readStore(absPath);
|
|
169
281
|
|
|
170
282
|
// Index existing records by id for O(1) lookup.
|
|
171
283
|
/** @type {Map<string, RepairCandidate>} */
|
|
@@ -219,6 +331,7 @@ export function mergeCandidates({ candidates, repoRoot, storePath } = {}) {
|
|
|
219
331
|
written,
|
|
220
332
|
superseded,
|
|
221
333
|
skipped_processed: skippedProcessed,
|
|
334
|
+
dropped_on_read: droppedOnRead,
|
|
222
335
|
total: store.length,
|
|
223
336
|
};
|
|
224
337
|
}
|
|
@@ -230,7 +343,11 @@ export function mergeCandidates({ candidates, repoRoot, storePath } = {}) {
|
|
|
230
343
|
* - id not found → `{ ok: false, reason: 'not-found' }`
|
|
231
344
|
* - already processed → `{ ok: true }` (idempotent no-op; store untouched)
|
|
232
345
|
* - filesystem write failure → `{ ok: false, reason: 'fs-error' }`
|
|
233
|
-
* Never throws.
|
|
346
|
+
* Never throws. Like {@link mergeCandidates} this rewrites the store in full, so
|
|
347
|
+
* shape-foreign lines are DELETED from disk here too. This API's result shape
|
|
348
|
+
* has no room to report the count (it is `{ ok, reason }` by contract), so the
|
|
349
|
+
* deletion is surfaced by {@link readStore}'s stderr WARN instead — this is the
|
|
350
|
+
* most-travelled of the three rewrite paths, so it must not be the silent one.
|
|
234
351
|
* @param {Object} params
|
|
235
352
|
* @param {string} params.id - candidate id to stamp.
|
|
236
353
|
* @param {string} [params.repoRoot]
|
|
@@ -244,7 +361,7 @@ export function markProcessed({ id, repoRoot, storePath, now } = {}) {
|
|
|
244
361
|
}
|
|
245
362
|
|
|
246
363
|
const absPath = resolveStorePath(repoRoot, storePath);
|
|
247
|
-
const store = readStore(absPath);
|
|
364
|
+
const { records: store } = readStore(absPath);
|
|
248
365
|
|
|
249
366
|
const target = store.find((rec) => rec && rec.id === id);
|
|
250
367
|
if (!target) return { ok: false, reason: 'not-found' };
|
|
@@ -263,7 +380,9 @@ export function markProcessed({ id, repoRoot, storePath, now } = {}) {
|
|
|
263
380
|
|
|
264
381
|
/**
|
|
265
382
|
* True iff a candidate with the given id exists in the store AND has been
|
|
266
|
-
* processed (`processed_at !== null`).
|
|
383
|
+
* processed (`processed_at !== null`). Read-only — it does not itself rewrite
|
|
384
|
+
* the store — but it reads through the same shape guard, so a contaminated
|
|
385
|
+
* store is reported here too via {@link readStore}'s stderr WARN. Never throws.
|
|
267
386
|
* @param {Object} params
|
|
268
387
|
* @param {string} params.id
|
|
269
388
|
* @param {string} [params.repoRoot]
|
|
@@ -273,7 +392,7 @@ export function markProcessed({ id, repoRoot, storePath, now } = {}) {
|
|
|
273
392
|
export function isProcessed({ id, repoRoot, storePath } = {}) {
|
|
274
393
|
if (typeof id !== 'string' || id.length === 0) return false;
|
|
275
394
|
const absPath = resolveStorePath(repoRoot, storePath);
|
|
276
|
-
const store = readStore(absPath);
|
|
395
|
+
const { records: store } = readStore(absPath);
|
|
277
396
|
const rec = store.find((r) => r && r.id === id);
|
|
278
397
|
return !!rec && rec.processed_at !== null && rec.processed_at !== undefined;
|
|
279
398
|
}
|
|
@@ -30,6 +30,9 @@
|
|
|
30
30
|
* - opts.vcsDetect — ({ config, projectRoot }) => { bin } VCS detector
|
|
31
31
|
* - opts.createMr — async (loop, draftOpts) => { created, mrUrl } MR opener
|
|
32
32
|
* - opts.log — (level, msg) => void diagnostic logger
|
|
33
|
+
* - opts.resolveRepoSpecFn — optional passthrough to the createMr seam's own
|
|
34
|
+
* -R/--repo host-pinning resolver (#872); undefined lets createMr fall
|
|
35
|
+
* back to its own real-resolveRepoSpec default.
|
|
33
36
|
*/
|
|
34
37
|
|
|
35
38
|
import { execFile as execFileCb, spawnSync } from 'node:child_process';
|
|
@@ -468,6 +471,11 @@ export async function openRepairMr(
|
|
|
468
471
|
}
|
|
469
472
|
|
|
470
473
|
// Delegate MR creation to the mr-draft seam (collision check + draft create).
|
|
474
|
+
// repoRoot is passed through so maybeCreateDraftMR's -R/--repo host-pinning
|
|
475
|
+
// resolution (#872) resolves against THIS repo, not process.cwd() (which
|
|
476
|
+
// may differ inside a dispatched agent/worktree context); resolveRepoSpecFn
|
|
477
|
+
// is an optional passthrough seam for tests — undefined here simply means
|
|
478
|
+
// maybeCreateDraftMR falls back to its own real-resolveRepoSpec default.
|
|
471
479
|
let mrResult;
|
|
472
480
|
try {
|
|
473
481
|
mrResult = await createMr(
|
|
@@ -480,7 +488,7 @@ export async function openRepairMr(
|
|
|
480
488
|
parentRunId: `repair/${candidate.id}`,
|
|
481
489
|
worktreePath: candidate.target_path ?? '(unknown)',
|
|
482
490
|
},
|
|
483
|
-
{ execFile: opts.execFile, log },
|
|
491
|
+
{ execFile: opts.execFile, log, repoRoot, resolveRepoSpecFn: opts.resolveRepoSpecFn },
|
|
484
492
|
);
|
|
485
493
|
} catch (err) {
|
|
486
494
|
const msg = err instanceof MrDraftError ? err.message : String(err?.message ?? err);
|
|
@@ -23,10 +23,21 @@
|
|
|
23
23
|
*
|
|
24
24
|
* This module intentionally does NOT reuse `scripts/lib/vault-backfill/glab.mjs`
|
|
25
25
|
* — that helper is vault-specific and wider in scope than needed here.
|
|
26
|
+
*
|
|
27
|
+
* Host pinning (#839): every `glab`/`gh` spawn below (the dedup lookup AND the
|
|
28
|
+
* `issue create` write) is pinned to the resolved repo via `-R <spec>`. A bare
|
|
29
|
+
* spawn falls back to the ambient `GITLAB_HOST`/`GH_HOST`, which can silently
|
|
30
|
+
* resolve to the WRONG GitLab instance on a multi-host machine — for a WRITE
|
|
31
|
+
* path (`issue create`) that means either filing into the wrong project, or
|
|
32
|
+
* failing open and defeating `findExistingLabeledIssue`'s dedup (which itself
|
|
33
|
+
* fails open to `{exists:false}` on any CLI error), risking double-filed
|
|
34
|
+
* issues. See `scripts/lib/vcs-repo-spec.mjs` for the full rationale.
|
|
26
35
|
*/
|
|
27
36
|
|
|
28
37
|
import { execFileSync } from 'node:child_process';
|
|
29
38
|
import { digestSha256Short } from './crypto-digest-utils.mjs';
|
|
39
|
+
import { chargeIssueBudget, formatBlockReason } from './issue-budget.mjs';
|
|
40
|
+
import { resolveRepoSpec } from './vcs-repo-spec.mjs';
|
|
30
41
|
|
|
31
42
|
/**
|
|
32
43
|
* Compute a stable 8-char sha256 hash of a task description.
|
|
@@ -51,13 +62,57 @@ function truncate(s, max) {
|
|
|
51
62
|
return `${str.slice(0, Math.max(0, max - 1))}…`;
|
|
52
63
|
}
|
|
53
64
|
|
|
65
|
+
/**
|
|
66
|
+
* True when the argv describes an issue-CREATE call (not a list/search).
|
|
67
|
+
* The dedup lookups in this module also go through `runCli`, and those must
|
|
68
|
+
* never be charged against the issue budget.
|
|
69
|
+
*
|
|
70
|
+
* @param {string} cmd
|
|
71
|
+
* @param {string[]} args
|
|
72
|
+
* @returns {boolean}
|
|
73
|
+
*/
|
|
74
|
+
function isIssueCreateArgv(cmd, args) {
|
|
75
|
+
if (cmd !== 'gh' && cmd !== 'glab') return false;
|
|
76
|
+
if (!Array.isArray(args) || args.length < 2) return false;
|
|
77
|
+
return args[0] === 'issue' && (args[1] === 'create' || args[1] === 'new');
|
|
78
|
+
}
|
|
79
|
+
|
|
54
80
|
/**
|
|
55
81
|
* Run a CLI command and return { ok, stdout, stderr }. Never throws.
|
|
82
|
+
*
|
|
83
|
+
* ISSUE-BUDGET GATE (both Node producers funnel through here): before shelling
|
|
84
|
+
* out to an issue-create call, the same `chargeIssueBudget` decision the
|
|
85
|
+
* `pre-bash-issue-budget` hook applies is evaluated here — otherwise the
|
|
86
|
+
* programmatic path would be a hole straight through the shell-level cap.
|
|
87
|
+
*
|
|
88
|
+
* In practice BOTH current callers are exempt by class (`createSpiralCarryoverIssue`
|
|
89
|
+
* emits `[Carryover] [SPIRAL|FAILED] …`, `createBrokenWindowIssue` emits the
|
|
90
|
+
* `broken-window` label), so this gate is a no-op for them by design — that is
|
|
91
|
+
* exactly the session-end promise at SKILL.md:319 / :1113 being preserved. It
|
|
92
|
+
* bites for any FUTURE non-exempt producer added to this module.
|
|
93
|
+
*
|
|
56
94
|
* @param {string} cmd
|
|
57
95
|
* @param {string[]} args
|
|
58
|
-
* @returns {{ ok: boolean, stdout: string, stderr: string }}
|
|
96
|
+
* @returns {{ ok: boolean, stdout: string, stderr: string, budgetBlocked?: boolean }}
|
|
59
97
|
*/
|
|
60
98
|
function runCli(cmd, args) {
|
|
99
|
+
if (isIssueCreateArgv(cmd, args)) {
|
|
100
|
+
try {
|
|
101
|
+
const repoRoot = process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
102
|
+
const titleIdx = args.indexOf('--title');
|
|
103
|
+
const verdict = chargeIssueBudget({
|
|
104
|
+
repoRoot,
|
|
105
|
+
sessionId: process.env.CLAUDE_SESSION_ID || null,
|
|
106
|
+
command: [cmd, ...args].join(' '),
|
|
107
|
+
title: titleIdx >= 0 ? (args[titleIdx + 1] ?? null) : null,
|
|
108
|
+
});
|
|
109
|
+
if (verdict.decision === 'block') {
|
|
110
|
+
return { ok: false, stdout: '', stderr: formatBlockReason(verdict), budgetBlocked: true };
|
|
111
|
+
}
|
|
112
|
+
} catch {
|
|
113
|
+
// Fail open — a budget-bookkeeping failure must never lose a carryover.
|
|
114
|
+
}
|
|
115
|
+
}
|
|
61
116
|
try {
|
|
62
117
|
const stdout = execFileSync(cmd, args, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
|
|
63
118
|
return { ok: true, stdout: String(stdout ?? ''), stderr: '' };
|
|
@@ -96,18 +151,33 @@ function parseIssueCreateOutput(stdout) {
|
|
|
96
151
|
* Returns `{ exists: false }` on any CLI failure — caller treats this as
|
|
97
152
|
* "probably no duplicate" and proceeds with creation (fail-open).
|
|
98
153
|
*
|
|
99
|
-
* @param {{
|
|
154
|
+
* @param {{
|
|
155
|
+
* taskHash: string,
|
|
156
|
+
* label: string,
|
|
157
|
+
* vcs?: 'gitlab' | 'github',
|
|
158
|
+
* repoRoot?: string,
|
|
159
|
+
* resolveRepoSpecFn?: (opts: { repoRoot: string, vcs: 'gitlab' | 'github' }) => string | undefined
|
|
160
|
+
* }} opts
|
|
100
161
|
* @returns {Promise<{ exists: boolean, issueId?: number, issueUrl?: string }>}
|
|
101
162
|
*/
|
|
102
|
-
async function findExistingLabeledIssue({
|
|
163
|
+
async function findExistingLabeledIssue({
|
|
164
|
+
taskHash,
|
|
165
|
+
label,
|
|
166
|
+
vcs = 'gitlab',
|
|
167
|
+
repoRoot = process.cwd(),
|
|
168
|
+
resolveRepoSpecFn = resolveRepoSpec,
|
|
169
|
+
} = {}) {
|
|
103
170
|
if (!taskHash || typeof taskHash !== 'string') {
|
|
104
171
|
return { exists: false };
|
|
105
172
|
}
|
|
106
173
|
|
|
174
|
+
const vcsResolved = vcs === 'github' ? 'github' : 'gitlab';
|
|
175
|
+
const spec = resolveRepoSpecFn({ repoRoot, vcs: vcsResolved });
|
|
176
|
+
|
|
107
177
|
try {
|
|
108
|
-
if (
|
|
178
|
+
if (vcsResolved === 'github') {
|
|
109
179
|
// gh: list open issues carrying the label; body match is done locally.
|
|
110
|
-
const
|
|
180
|
+
const args = [
|
|
111
181
|
'issue',
|
|
112
182
|
'list',
|
|
113
183
|
'--label',
|
|
@@ -118,7 +188,9 @@ async function findExistingLabeledIssue({ taskHash, label, vcs = 'gitlab' } = {}
|
|
|
118
188
|
'100',
|
|
119
189
|
'--json',
|
|
120
190
|
'number,url,body',
|
|
121
|
-
]
|
|
191
|
+
];
|
|
192
|
+
if (spec) args.push('-R', spec);
|
|
193
|
+
const res = runCli('gh', args);
|
|
122
194
|
if (!res.ok) return { exists: false };
|
|
123
195
|
let arr;
|
|
124
196
|
try {
|
|
@@ -136,16 +208,9 @@ async function findExistingLabeledIssue({ taskHash, label, vcs = 'gitlab' } = {}
|
|
|
136
208
|
}
|
|
137
209
|
|
|
138
210
|
// Default: gitlab via glab.
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
'--label',
|
|
143
|
-
label,
|
|
144
|
-
'--per-page',
|
|
145
|
-
'100',
|
|
146
|
-
'--output',
|
|
147
|
-
'json',
|
|
148
|
-
]);
|
|
211
|
+
const args = ['issue', 'list', '--label', label, '--per-page', '100', '--output', 'json'];
|
|
212
|
+
if (spec) args.push('-R', spec);
|
|
213
|
+
const res = runCli('glab', args);
|
|
149
214
|
if (!res.ok) return { exists: false };
|
|
150
215
|
let arr;
|
|
151
216
|
try {
|
|
@@ -173,22 +238,42 @@ async function findExistingLabeledIssue({ taskHash, label, vcs = 'gitlab' } = {}
|
|
|
173
238
|
* Check whether a carryover issue already exists for this task.
|
|
174
239
|
* Thin wrapper over `findExistingLabeledIssue` with the `type:carryover` label.
|
|
175
240
|
*
|
|
176
|
-
* @param {{
|
|
241
|
+
* @param {{
|
|
242
|
+
* taskHash: string,
|
|
243
|
+
* vcs?: 'gitlab' | 'github',
|
|
244
|
+
* repoRoot?: string,
|
|
245
|
+
* resolveRepoSpecFn?: (opts: { repoRoot: string, vcs: 'gitlab' | 'github' }) => string | undefined
|
|
246
|
+
* }} opts
|
|
177
247
|
* @returns {Promise<{ exists: boolean, issueId?: number, issueUrl?: string }>}
|
|
178
248
|
*/
|
|
179
|
-
export async function findExistingCarryover({
|
|
180
|
-
|
|
249
|
+
export async function findExistingCarryover({
|
|
250
|
+
taskHash,
|
|
251
|
+
vcs = 'gitlab',
|
|
252
|
+
repoRoot = process.cwd(),
|
|
253
|
+
resolveRepoSpecFn = resolveRepoSpec,
|
|
254
|
+
} = {}) {
|
|
255
|
+
return findExistingLabeledIssue({ taskHash, label: 'type:carryover', vcs, repoRoot, resolveRepoSpecFn });
|
|
181
256
|
}
|
|
182
257
|
|
|
183
258
|
/**
|
|
184
259
|
* Check whether a broken-window closure issue already exists for this task.
|
|
185
260
|
* Thin wrapper over `findExistingLabeledIssue` with the `broken-window` label.
|
|
186
261
|
*
|
|
187
|
-
* @param {{
|
|
262
|
+
* @param {{
|
|
263
|
+
* taskHash: string,
|
|
264
|
+
* vcs?: 'gitlab' | 'github',
|
|
265
|
+
* repoRoot?: string,
|
|
266
|
+
* resolveRepoSpecFn?: (opts: { repoRoot: string, vcs: 'gitlab' | 'github' }) => string | undefined
|
|
267
|
+
* }} opts
|
|
188
268
|
* @returns {Promise<{ exists: boolean, issueId?: number, issueUrl?: string }>}
|
|
189
269
|
*/
|
|
190
|
-
export async function findExistingBrokenWindow({
|
|
191
|
-
|
|
270
|
+
export async function findExistingBrokenWindow({
|
|
271
|
+
taskHash,
|
|
272
|
+
vcs = 'gitlab',
|
|
273
|
+
repoRoot = process.cwd(),
|
|
274
|
+
resolveRepoSpecFn = resolveRepoSpec,
|
|
275
|
+
} = {}) {
|
|
276
|
+
return findExistingLabeledIssue({ taskHash, label: 'broken-window', vcs, repoRoot, resolveRepoSpecFn });
|
|
192
277
|
}
|
|
193
278
|
|
|
194
279
|
/**
|
|
@@ -245,7 +330,9 @@ function buildCarryoverBody({ taskDescription, kind, context, taskHash }) {
|
|
|
245
330
|
* kind: 'SPIRAL' | 'FAILED',
|
|
246
331
|
* context: string,
|
|
247
332
|
* priority?: 'high' | 'medium',
|
|
248
|
-
* vcs?: 'gitlab' | 'github'
|
|
333
|
+
* vcs?: 'gitlab' | 'github',
|
|
334
|
+
* repoRoot?: string,
|
|
335
|
+
* resolveRepoSpecFn?: (opts: { repoRoot: string, vcs: 'gitlab' | 'github' }) => string | undefined
|
|
249
336
|
* }} opts
|
|
250
337
|
* @returns {Promise<{
|
|
251
338
|
* created: boolean,
|
|
@@ -261,6 +348,8 @@ export async function createSpiralCarryoverIssue({
|
|
|
261
348
|
context,
|
|
262
349
|
priority = 'high',
|
|
263
350
|
vcs = 'gitlab',
|
|
351
|
+
repoRoot = process.cwd(),
|
|
352
|
+
resolveRepoSpecFn = resolveRepoSpec,
|
|
264
353
|
} = {}) {
|
|
265
354
|
try {
|
|
266
355
|
if (kind !== 'SPIRAL' && kind !== 'FAILED') {
|
|
@@ -272,10 +361,19 @@ export async function createSpiralCarryoverIssue({
|
|
|
272
361
|
}
|
|
273
362
|
const vcsResolved = vcs === 'github' ? 'github' : 'gitlab';
|
|
274
363
|
|
|
364
|
+
// Resolve the -R/--repo host-pinning spec ONCE (#839); reuse the same
|
|
365
|
+
// resolved value for the dedup lookup below instead of re-resolving.
|
|
366
|
+
const spec = resolveRepoSpecFn({ repoRoot, vcs: vcsResolved });
|
|
367
|
+
|
|
275
368
|
const taskHash = computeTaskHash(taskDescription);
|
|
276
369
|
|
|
277
370
|
// Dedup check first.
|
|
278
|
-
const existing = await findExistingCarryover({
|
|
371
|
+
const existing = await findExistingCarryover({
|
|
372
|
+
taskHash,
|
|
373
|
+
vcs: vcsResolved,
|
|
374
|
+
repoRoot,
|
|
375
|
+
resolveRepoSpecFn: () => spec,
|
|
376
|
+
});
|
|
279
377
|
if (existing.exists) {
|
|
280
378
|
return {
|
|
281
379
|
created: false,
|
|
@@ -288,7 +386,7 @@ export async function createSpiralCarryoverIssue({
|
|
|
288
386
|
const truncatedDesc = truncate(String(taskDescription ?? '').trim() || '(untitled task)', 80);
|
|
289
387
|
const title = `[Carryover] [${kind}] ${truncatedDesc}`;
|
|
290
388
|
const body = buildCarryoverBody({ taskDescription, kind, context, taskHash });
|
|
291
|
-
const labels = `priority
|
|
389
|
+
const labels = `priority::${priority},status:ready,type:carryover`;
|
|
292
390
|
|
|
293
391
|
let cmd;
|
|
294
392
|
let args;
|
|
@@ -299,6 +397,7 @@ export async function createSpiralCarryoverIssue({
|
|
|
299
397
|
cmd = 'glab';
|
|
300
398
|
args = ['issue', 'create', '--title', title, '--description', body, '--label', labels];
|
|
301
399
|
}
|
|
400
|
+
if (spec) args.push('-R', spec);
|
|
302
401
|
|
|
303
402
|
const res = runCli(cmd, args);
|
|
304
403
|
if (!res.ok) {
|
|
@@ -403,13 +502,15 @@ function buildBrokenWindowBody({ item, taskHash, dueDate, vcs }) {
|
|
|
403
502
|
* 5. Parse stdout for the issue URL and return `{ created: true, issueId, issueUrl, due }`.
|
|
404
503
|
*
|
|
405
504
|
* Never throws. On any CLI failure returns `{ created: false, skipped: 'error', error }`.
|
|
406
|
-
* `repoRoot`
|
|
407
|
-
*
|
|
505
|
+
* `repoRoot` defaults to `process.cwd()` and is used (#839) to resolve the
|
|
506
|
+
* `-R`/`--repo` host-pinning spec via `resolveRepoSpecFn` — previously accepted
|
|
507
|
+
* only for signature symmetry and left unused.
|
|
408
508
|
*
|
|
409
509
|
* @param {{
|
|
410
510
|
* item: { title?: string, source?: string, description?: string, sessionId?: string },
|
|
411
511
|
* dueDays?: number,
|
|
412
512
|
* repoRoot?: string,
|
|
513
|
+
* resolveRepoSpecFn?: (opts: { repoRoot: string, vcs: 'gitlab' | 'github' }) => string | undefined,
|
|
413
514
|
* vcs?: 'gitlab' | 'github'
|
|
414
515
|
* }} opts
|
|
415
516
|
* @returns {Promise<{
|
|
@@ -424,7 +525,8 @@ function buildBrokenWindowBody({ item, taskHash, dueDate, vcs }) {
|
|
|
424
525
|
export async function createBrokenWindowIssue({
|
|
425
526
|
item,
|
|
426
527
|
dueDays = 7,
|
|
427
|
-
repoRoot
|
|
528
|
+
repoRoot = process.cwd(),
|
|
529
|
+
resolveRepoSpecFn = resolveRepoSpec,
|
|
428
530
|
vcs = 'gitlab',
|
|
429
531
|
} = {}) {
|
|
430
532
|
try {
|
|
@@ -437,11 +539,20 @@ export async function createBrokenWindowIssue({
|
|
|
437
539
|
|
|
438
540
|
const dueDate = computeDueDate(dueDays);
|
|
439
541
|
|
|
542
|
+
// Resolve the -R/--repo host-pinning spec ONCE (#839); reuse the same
|
|
543
|
+
// resolved value for the dedup lookup below instead of re-resolving.
|
|
544
|
+
const spec = resolveRepoSpecFn({ repoRoot, vcs: vcsResolved });
|
|
545
|
+
|
|
440
546
|
// Dedup key: (source, title) pair — two different sources with the same
|
|
441
547
|
// title are genuinely distinct broken windows and each file separately.
|
|
442
548
|
const taskHash = computeTaskHash(`${source}::${title}`);
|
|
443
549
|
|
|
444
|
-
const existing = await findExistingBrokenWindow({
|
|
550
|
+
const existing = await findExistingBrokenWindow({
|
|
551
|
+
taskHash,
|
|
552
|
+
vcs: vcsResolved,
|
|
553
|
+
repoRoot,
|
|
554
|
+
resolveRepoSpecFn: () => spec,
|
|
555
|
+
});
|
|
445
556
|
if (existing.exists) {
|
|
446
557
|
return {
|
|
447
558
|
created: false,
|
|
@@ -453,7 +564,7 @@ export async function createBrokenWindowIssue({
|
|
|
453
564
|
|
|
454
565
|
const issueTitle = `[Broken-Window] ${truncate(title, 80)}`;
|
|
455
566
|
const body = buildBrokenWindowBody({ item, taskHash, dueDate, vcs: vcsResolved });
|
|
456
|
-
const labels = 'broken-window,priority
|
|
567
|
+
const labels = 'broken-window,priority::high';
|
|
457
568
|
|
|
458
569
|
let cmd;
|
|
459
570
|
let args;
|
|
@@ -475,6 +586,7 @@ export async function createBrokenWindowIssue({
|
|
|
475
586
|
dueDate,
|
|
476
587
|
];
|
|
477
588
|
}
|
|
589
|
+
if (spec) args.push('-R', spec);
|
|
478
590
|
|
|
479
591
|
const res = runCli(cmd, args);
|
|
480
592
|
if (!res.ok) {
|