@deftai/directive-core 0.96.0 → 0.98.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/dist/authz/classify.js +291 -0
- package/dist/cache/archive.js +10 -4
- package/dist/check/cached-orchestrator.d.ts +5 -0
- package/dist/check/cached-orchestrator.js +18 -1
- package/dist/check/gate-lists.d.ts +20 -0
- package/dist/check/gate-lists.js +54 -9
- package/dist/check/index.d.ts +1 -1
- package/dist/check/index.js +1 -1
- package/dist/check/orchestrator.d.ts +4 -0
- package/dist/check/orchestrator.js +4 -0
- package/dist/consumer-check-contract/evaluate.d.ts +124 -0
- package/dist/consumer-check-contract/evaluate.js +699 -0
- package/dist/consumer-check-contract/index.d.ts +5 -0
- package/dist/consumer-check-contract/index.js +5 -0
- package/dist/delivery-attempt/disk-begin.d.ts +51 -0
- package/dist/delivery-attempt/disk-begin.js +68 -0
- package/dist/delivery-attempt/evaluate.d.ts +26 -0
- package/dist/delivery-attempt/evaluate.js +443 -0
- package/dist/delivery-attempt/fingerprint.d.ts +32 -0
- package/dist/delivery-attempt/fingerprint.js +100 -0
- package/dist/delivery-attempt/handoff.d.ts +25 -0
- package/dist/delivery-attempt/handoff.js +102 -0
- package/dist/delivery-attempt/index.d.ts +17 -0
- package/dist/delivery-attempt/index.js +17 -0
- package/dist/delivery-attempt/ledger.d.ts +169 -0
- package/dist/delivery-attempt/ledger.js +758 -0
- package/dist/delivery-attempt/material-delta.d.ts +38 -0
- package/dist/delivery-attempt/material-delta.js +126 -0
- package/dist/delivery-attempt/types.d.ts +210 -0
- package/dist/delivery-attempt/types.js +77 -0
- package/dist/doctor/checks.d.ts +7 -0
- package/dist/doctor/checks.js +83 -0
- package/dist/doctor/index.d.ts +1 -0
- package/dist/doctor/index.js +1 -0
- package/dist/doctor/main.js +12 -0
- package/dist/doctor/openclaw-soft-rebind.d.ts +26 -0
- package/dist/doctor/openclaw-soft-rebind.js +164 -0
- package/dist/hooks/dispatcher.d.ts +7 -1
- package/dist/hooks/dispatcher.js +117 -13
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/init-deposit/gitignore.js +7 -0
- package/dist/init-deposit/hygiene.d.ts +70 -1
- package/dist/init-deposit/hygiene.js +582 -8
- package/dist/init-deposit/init-deposit.js +5 -0
- package/dist/init-deposit/refresh.js +3 -0
- package/dist/init-deposit/scaffold.js +277 -4
- package/dist/init-deposit/skill-discovery-deposit.js +15 -0
- package/dist/policy/check-resume.d.ts +72 -0
- package/dist/policy/check-resume.js +253 -0
- package/dist/policy/coverage-check-resume-presets.d.ts +46 -0
- package/dist/policy/coverage-check-resume-presets.js +228 -0
- package/dist/policy/coverage-debt.d.ts +76 -0
- package/dist/policy/coverage-debt.js +262 -0
- package/dist/policy/index.d.ts +3 -0
- package/dist/policy/index.js +50 -21
- package/dist/pr-merge-readiness/ci-gate.d.ts +29 -1
- package/dist/pr-merge-readiness/ci-gate.js +191 -24
- package/dist/pr-merge-readiness/compute.js +10 -1
- package/dist/pr-merge-readiness/index.d.ts +2 -1
- package/dist/pr-merge-readiness/index.js +2 -1
- package/dist/pr-merge-readiness/output.js +14 -0
- package/dist/pr-merge-readiness/platform-status.d.ts +29 -0
- package/dist/pr-merge-readiness/platform-status.js +49 -0
- package/dist/pr-watch/constants.d.ts +10 -0
- package/dist/pr-watch/constants.js +12 -1
- package/dist/pr-watch/main.js +16 -1
- package/dist/pr-watch/probe.js +13 -1
- package/dist/pr-watch/types.d.ts +3 -2
- package/dist/pr-watch/watch.js +14 -7
- package/dist/release/auto-hatch.d.ts +114 -0
- package/dist/release/auto-hatch.js +301 -0
- package/dist/release/coverage-debt-ledger.d.ts +22 -0
- package/dist/release/coverage-debt-ledger.js +157 -0
- package/dist/release/index.d.ts +3 -0
- package/dist/release/index.js +3 -0
- package/dist/release/pipeline.js +164 -12
- package/dist/release/suite-stamp.d.ts +44 -0
- package/dist/release/suite-stamp.js +133 -0
- package/dist/release/types.d.ts +19 -0
- package/dist/scope-provenance/digest.d.ts +67 -0
- package/dist/scope-provenance/digest.js +188 -0
- package/dist/scope-provenance/evaluate.d.ts +82 -0
- package/dist/scope-provenance/evaluate.js +528 -0
- package/dist/scope-provenance/index.d.ts +6 -0
- package/dist/scope-provenance/index.js +6 -0
- package/dist/session/compact-ritual.d.ts +96 -0
- package/dist/session/compact-ritual.js +237 -0
- package/dist/session/compact-ritual.spec.d.ts +2 -0
- package/dist/session/compact-ritual.spec.js +21 -0
- package/dist/session/coverage-check-resume-nudge.d.ts +34 -0
- package/dist/session/coverage-check-resume-nudge.js +66 -0
- package/dist/session/index.d.ts +3 -0
- package/dist/session/index.js +3 -0
- package/dist/session/openclaw-soft-rebind-deposit.d.ts +46 -0
- package/dist/session/openclaw-soft-rebind-deposit.js +165 -0
- package/dist/session/session-start.js +21 -0
- package/dist/test-boundary/evaluate.d.ts +54 -0
- package/dist/test-boundary/evaluate.js +368 -0
- package/dist/test-boundary/index.d.ts +6 -0
- package/dist/test-boundary/index.js +6 -0
- package/dist/test-boundary/policy.d.ts +52 -0
- package/dist/test-boundary/policy.js +182 -0
- package/dist/triage/bootstrap/gitignore.d.ts +1 -1
- package/dist/triage/bootstrap/gitignore.js +15 -1
- package/dist/triage/classify/label-mirror.d.ts +31 -1
- package/dist/triage/classify/label-mirror.js +78 -6
- package/dist/triage/help/registry-data.d.ts +6 -6
- package/dist/triage/help/registry-data.js +12 -3
- package/dist/vbrief-activate/activate.js +22 -6
- package/dist/vbrief-validate/plan-hooks.d.ts +4 -0
- package/dist/vbrief-validate/plan-hooks.js +54 -0
- package/dist/xbrief/styles.js +33 -17
- package/package.json +7 -3
|
@@ -8,7 +8,7 @@ export declare function gitignoreTriageCacheEntries(projectRoot: string): readon
|
|
|
8
8
|
export declare function gitattributesTriageCacheGlob(projectRoot: string): string;
|
|
9
9
|
export declare const GITATTRIBUTES_EVAL_RULE = "vbrief/.triage-cache/*.jsonl merge=union";
|
|
10
10
|
export declare const FORBIDDEN_BLANKET_EVAL_LINES: readonly string[];
|
|
11
|
-
export declare const EVAL_README_BODY = "# `vbrief/.triage-cache/` \u2014 triage working-set files\n\nThis directory holds JSON-lines logs and scratch files that Deft triage and\nslicing workflows emit. Deft configures your repo's `.gitignore` and\n`.gitattributes` so some files stay local while team-shared records can be\ncommitted.\n\n## What lives here\n\n| File | Committed? | Notes |\n| --- | --- | --- |\n| `slices.jsonl` | Yes | Team-shared cohort records from slicing skills. New teammates use prior cohort outputs to spot orphans and avoid re-slicing the same scope. |\n| `candidates.jsonl` | No | Your local triage accept / defer / reject stream. Re-create on a fresh clone with `deft triage:bootstrap`. |\n| `summary-history.jsonl` | No | Local history of `deft triage:summary` output; not required for day-to-day work. |\n| `scope-lifecycle.jsonl` | No | Local audit trail for scope demotions (`deft scope:demote`). Each operator's stream stays on their machine. |\n| `decompositions/` | No | Draft story-decomposition scratch. Produced child story xBRIEFs live in lifecycle folders via `deft scope:decompose`. |\n| `doctor-state.json` | No | Per-clone throttle state for `deft doctor` re-probe timing. |\n\nPaths listed as \"No\" above are added to `.gitignore` during bootstrap; anything\nnot listed remains committable by default. The selective ignore entries live in\nthe repo-root `.gitignore` (`vbrief/.triage-cache/candidates.jsonl`,\n`vbrief/.triage-cache/summary-history.jsonl`, `vbrief/.triage-cache/scope-lifecycle.jsonl`,\n`vbrief/.triage-cache/decompositions/`,
|
|
11
|
+
export declare const EVAL_README_BODY = "# `vbrief/.triage-cache/` \u2014 triage working-set files\n\nThis directory holds JSON-lines logs and scratch files that Deft triage and\nslicing workflows emit. Deft configures your repo's `.gitignore` and\n`.gitattributes` so some files stay local while team-shared records can be\ncommitted.\n\n## What lives here\n\n| File | Committed? | Notes |\n| --- | --- | --- |\n| `slices.jsonl` | Yes | Team-shared cohort records from slicing skills. New teammates use prior cohort outputs to spot orphans and avoid re-slicing the same scope. |\n| `candidates.jsonl` | No | Your local triage accept / defer / reject stream. Re-create on a fresh clone with `deft triage:bootstrap`. |\n| `summary-history.jsonl` | No | Local history of `deft triage:summary` output; not required for day-to-day work. |\n| `scope-lifecycle.jsonl` | No | Local audit trail for scope demotions (`deft scope:demote`). Each operator's stream stays on their machine. |\n| `decompositions/` | No | Draft story-decomposition scratch. Produced child story xBRIEFs live in lifecycle folders via `deft scope:decompose`. |\n| `doctor-state.json` | No | Per-clone throttle state for `deft doctor` re-probe timing. |\n| `staleness-tickler-state.json` | No | Per-clone upgrade-tickler throttle state. |\n| `release-availability-state.json` | No | Per-clone release-availability probe throttle state. |\n\nPaths listed as \"No\" above are added to `.gitignore` during bootstrap; anything\nnot listed remains committable by default. The selective ignore entries live in\nthe repo-root `.gitignore` (`vbrief/.triage-cache/candidates.jsonl`,\n`vbrief/.triage-cache/summary-history.jsonl`, `vbrief/.triage-cache/scope-lifecycle.jsonl`,\n`vbrief/.triage-cache/decompositions/`, `vbrief/.triage-cache/doctor-state.json`,\n`vbrief/.triage-cache/staleness-tickler-state.json`, and\n`vbrief/.triage-cache/release-availability-state.json`).\n\n## Fresh clone\n\nIf `candidates.jsonl` is missing, run:\n\n```\ndeft triage:bootstrap\n```\n\nBootstrap rebuilds the local candidates log without altering committed\n`slices.jsonl`.\n\n## Merge behavior for `*.jsonl`\n\nThe repo-root `.gitattributes` may declare:\n\n```\nvbrief/.triage-cache/*.jsonl merge=union\n```\n\nThe `union` merge driver concatenates both sides' appended lines on auto-merge,\nso parallel append-only edits to the same JSON-lines file rebase without manual\nconflict surgery. It does not dedupe semantically similar records \u2014 downstream\nreaders should tolerate duplicate-looking entries.\n\n## See also\n\n- `.gitignore` \u2014 selective ignore rules for operator-private files\n- `.gitattributes` \u2014 merge driver for committed JSON-lines logs\n";
|
|
12
12
|
/** Layout-aware triage-cache README body for the active lifecycle tree (#2344 / #2349). */
|
|
13
13
|
export declare function generateTriageCacheReadmeBody(projectRoot: string): string;
|
|
14
14
|
/** Strip an inline `# ...` comment from a gitignore line. */
|
|
@@ -20,6 +20,9 @@ export const GITIGNORE_EVAL_ENTRIES = [
|
|
|
20
20
|
"xbrief/.triage-cache/scope-lifecycle.jsonl",
|
|
21
21
|
"xbrief/.triage-cache/decompositions/",
|
|
22
22
|
"xbrief/.triage-cache/doctor-state.json",
|
|
23
|
+
// Per-clone session state (#3146); selective only — hybrid policy preserved.
|
|
24
|
+
"xbrief/.triage-cache/staleness-tickler-state.json",
|
|
25
|
+
"xbrief/.triage-cache/release-availability-state.json",
|
|
23
26
|
];
|
|
24
27
|
/** Layout-aware gitignore lines for triage working-set files (#1703). */
|
|
25
28
|
export function gitignoreTriageCacheEntries(projectRoot) {
|
|
@@ -30,6 +33,9 @@ export function gitignoreTriageCacheEntries(projectRoot) {
|
|
|
30
33
|
triageCacheRelPath(projectRoot, "scope-lifecycle.jsonl"),
|
|
31
34
|
decomp.endsWith("/") ? decomp : `${decomp}/`,
|
|
32
35
|
triageCacheRelPath(projectRoot, "doctor-state.json"),
|
|
36
|
+
// Per-clone session state (#3146); selective only — hybrid policy preserved.
|
|
37
|
+
triageCacheRelPath(projectRoot, "staleness-tickler-state.json"),
|
|
38
|
+
triageCacheRelPath(projectRoot, "release-availability-state.json"),
|
|
33
39
|
];
|
|
34
40
|
}
|
|
35
41
|
export function gitattributesTriageCacheGlob(projectRoot) {
|
|
@@ -76,6 +82,10 @@ const EVAL_ENTRIES_RATIONALE = "\n# vbrief/.triage-cache/ tracking governance (#
|
|
|
76
82
|
"# throttle state gating the 24h/4h\n" +
|
|
77
83
|
"# re-probe window; #1308 / #1464). Local\n" +
|
|
78
84
|
"# to each clone; never committed.\n" +
|
|
85
|
+
"# - staleness-tickler-state.json -> gitignored (per-clone upgrade\n" +
|
|
86
|
+
"# tickler throttle; #2488 / #3146).\n" +
|
|
87
|
+
"# - release-availability-state.json -> gitignored (per-clone npm\n" +
|
|
88
|
+
"# release-availability throttle; #1692 / #3146).\n" +
|
|
79
89
|
"# - slices.jsonl -> TRACKED (team-shared cohort records\n" +
|
|
80
90
|
"# produced by slicing skills; see\n" +
|
|
81
91
|
"# #1132 / D13).\n" +
|
|
@@ -104,12 +114,16 @@ committed.
|
|
|
104
114
|
| \`scope-lifecycle.jsonl\` | No | Local audit trail for scope demotions (\`deft scope:demote\`). Each operator's stream stays on their machine. |
|
|
105
115
|
| \`decompositions/\` | No | Draft story-decomposition scratch. Produced child story xBRIEFs live in lifecycle folders via \`deft scope:decompose\`. |
|
|
106
116
|
| \`doctor-state.json\` | No | Per-clone throttle state for \`deft doctor\` re-probe timing. |
|
|
117
|
+
| \`staleness-tickler-state.json\` | No | Per-clone upgrade-tickler throttle state. |
|
|
118
|
+
| \`release-availability-state.json\` | No | Per-clone release-availability probe throttle state. |
|
|
107
119
|
|
|
108
120
|
Paths listed as "No" above are added to \`.gitignore\` during bootstrap; anything
|
|
109
121
|
not listed remains committable by default. The selective ignore entries live in
|
|
110
122
|
the repo-root \`.gitignore\` (\`vbrief/.triage-cache/candidates.jsonl\`,
|
|
111
123
|
\`vbrief/.triage-cache/summary-history.jsonl\`, \`vbrief/.triage-cache/scope-lifecycle.jsonl\`,
|
|
112
|
-
\`vbrief/.triage-cache/decompositions/\`,
|
|
124
|
+
\`vbrief/.triage-cache/decompositions/\`, \`vbrief/.triage-cache/doctor-state.json\`,
|
|
125
|
+
\`vbrief/.triage-cache/staleness-tickler-state.json\`, and
|
|
126
|
+
\`vbrief/.triage-cache/release-availability-state.json\`).
|
|
113
127
|
|
|
114
128
|
## Fresh clone
|
|
115
129
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Tier-1 deterministic SCM label mirror (#1423 Wave 1 + Wave 2 bootstrap).
|
|
2
|
+
* Tier-1 deterministic SCM label mirror (#1423 Wave 1 + Wave 2 bootstrap + #3197 re-enrich).
|
|
3
3
|
*
|
|
4
4
|
* Classifies cached issues with the existing #1129 engine, then mirrors the
|
|
5
5
|
* outcome as SCM labels (dry-run default, --apply to write). Never accepts into
|
|
@@ -9,6 +9,10 @@
|
|
|
9
9
|
* + samples), batched rate-limit-aware apply. Bootstrap mass-triage entrypoint is
|
|
10
10
|
* `triage:classify -- --mirror` with these filters (not triage:accept).
|
|
11
11
|
*
|
|
12
|
+
* #3197 re-enrich: default keeps one-shot skip on idempotencyLabel; opt-in
|
|
13
|
+
* `--re-enrich` re-classifies already-stamped issues and plans **additive**
|
|
14
|
+
* label deltas only (v1; no removals / no full reconcile).
|
|
15
|
+
*
|
|
12
16
|
* Intentionally does NOT import from ./index.js (SLizard P1 cycle). The classify
|
|
13
17
|
* engine is injected via LabelMirrorEngine / mirrorLabels() wrapper in index.ts.
|
|
14
18
|
*/
|
|
@@ -98,6 +102,12 @@ export interface LabelMirrorItem {
|
|
|
98
102
|
readonly add: readonly string[];
|
|
99
103
|
readonly status: LabelMirrorStatus;
|
|
100
104
|
readonly message?: string;
|
|
105
|
+
/**
|
|
106
|
+
* True when this row re-planned an issue that already carried the idempotency
|
|
107
|
+
* label under opt-in re-enrich mode (#3197). Distinguishes first-time stamp
|
|
108
|
+
* rows from re-enrich additive backfill in dry-run digests.
|
|
109
|
+
*/
|
|
110
|
+
readonly re_enrich?: boolean;
|
|
101
111
|
}
|
|
102
112
|
/** Operator digest aggregates for bootstrap mass-triage (#3125 / #1423 Wave 2). */
|
|
103
113
|
export interface LabelMirrorDigest {
|
|
@@ -116,6 +126,8 @@ export interface LabelMirrorFilters {
|
|
|
116
126
|
readonly author: string | null;
|
|
117
127
|
/** Resolved author logins for machine consumers. */
|
|
118
128
|
readonly author_logins: readonly string[] | null;
|
|
129
|
+
/** Whether this run used opt-in re-enrich mode (#3197). */
|
|
130
|
+
readonly re_enrich: boolean;
|
|
119
131
|
}
|
|
120
132
|
export interface LabelMirrorOutcome {
|
|
121
133
|
readonly project_root: string;
|
|
@@ -132,6 +144,16 @@ export interface LabelMirrorOutcome {
|
|
|
132
144
|
/** Issues skipped by --author filter (#3129). */
|
|
133
145
|
readonly skipped_author: number;
|
|
134
146
|
readonly errors: number;
|
|
147
|
+
/**
|
|
148
|
+
* Planned rows that re-enriched already-stamped issues (subset of planned; #3197).
|
|
149
|
+
* Zero when re_enrich mode is off.
|
|
150
|
+
*/
|
|
151
|
+
readonly re_enrich_planned: number;
|
|
152
|
+
/**
|
|
153
|
+
* Applied rows that re-enriched already-stamped issues (subset of applied; #3197).
|
|
154
|
+
* Zero when re_enrich mode is off.
|
|
155
|
+
*/
|
|
156
|
+
readonly re_enrich_applied: number;
|
|
135
157
|
readonly filters: LabelMirrorFilters;
|
|
136
158
|
readonly digest: LabelMirrorDigest;
|
|
137
159
|
readonly items: readonly LabelMirrorItem[];
|
|
@@ -169,6 +191,14 @@ export interface LabelMirrorOptions {
|
|
|
169
191
|
readonly delayMs?: number;
|
|
170
192
|
/** Injectable sleep for tests (receives ms). Default busy-wait when delayMs > 0. */
|
|
171
193
|
readonly sleepMs?: LabelMirrorSleepFn;
|
|
194
|
+
/**
|
|
195
|
+
* Opt-in re-enrich mode (#3197): re-classify issues that already carry the
|
|
196
|
+
* idempotency label and plan **additive** label deltas only (no removals).
|
|
197
|
+
* Default false preserves one-shot `skipped_already_triaged` behavior.
|
|
198
|
+
* Still dry-run by default; pair with dryRun:false / CLI `--apply` to write.
|
|
199
|
+
* Never triage:accept / never xBRIEF writes.
|
|
200
|
+
*/
|
|
201
|
+
readonly reEnrich?: boolean;
|
|
172
202
|
/** Required: classify engine (provided by classify/index mirrorLabels wrapper). */
|
|
173
203
|
readonly engine: LabelMirrorEngine;
|
|
174
204
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Tier-1 deterministic SCM label mirror (#1423 Wave 1 + Wave 2 bootstrap).
|
|
2
|
+
* Tier-1 deterministic SCM label mirror (#1423 Wave 1 + Wave 2 bootstrap + #3197 re-enrich).
|
|
3
3
|
*
|
|
4
4
|
* Classifies cached issues with the existing #1129 engine, then mirrors the
|
|
5
5
|
* outcome as SCM labels (dry-run default, --apply to write). Never accepts into
|
|
@@ -9,6 +9,10 @@
|
|
|
9
9
|
* + samples), batched rate-limit-aware apply. Bootstrap mass-triage entrypoint is
|
|
10
10
|
* `triage:classify -- --mirror` with these filters (not triage:accept).
|
|
11
11
|
*
|
|
12
|
+
* #3197 re-enrich: default keeps one-shot skip on idempotencyLabel; opt-in
|
|
13
|
+
* `--re-enrich` re-classifies already-stamped issues and plans **additive**
|
|
14
|
+
* label deltas only (v1; no removals / no full reconcile).
|
|
15
|
+
*
|
|
12
16
|
* Intentionally does NOT import from ./index.js (SLizard P1 cycle). The classify
|
|
13
17
|
* engine is injected via LabelMirrorEngine / mirrorLabels() wrapper in index.ts.
|
|
14
18
|
*/
|
|
@@ -399,6 +403,7 @@ export function mirrorLabels(projectRoot, options) {
|
|
|
399
403
|
? options.authorFilter
|
|
400
404
|
: null;
|
|
401
405
|
const sampleLimit = options.sampleLimit ?? DEFAULT_DIGEST_SAMPLE_LIMIT;
|
|
406
|
+
const reEnrich = options.reEnrich === true;
|
|
402
407
|
const batchSize = options.batchSize !== undefined
|
|
403
408
|
? Math.max(1, Math.floor(options.batchSize))
|
|
404
409
|
: DEFAULT_APPLY_BATCH_SIZE;
|
|
@@ -416,6 +421,7 @@ export function mirrorLabels(projectRoot, options) {
|
|
|
416
421
|
repo: repoFilter,
|
|
417
422
|
author: authorFilter !== null ? authorFilter.display : null,
|
|
418
423
|
author_logins: authorFilter !== null ? authorFilter.allowLogins : null,
|
|
424
|
+
re_enrich: reEnrich,
|
|
419
425
|
};
|
|
420
426
|
const items = [];
|
|
421
427
|
const outcomeBase = {
|
|
@@ -441,6 +447,8 @@ export function mirrorLabels(projectRoot, options) {
|
|
|
441
447
|
skipped_unreadable: 0,
|
|
442
448
|
skipped_closed: 0,
|
|
443
449
|
skipped_author: 0,
|
|
450
|
+
re_enrich_planned: 0,
|
|
451
|
+
re_enrich_applied: 0,
|
|
444
452
|
errors: 0,
|
|
445
453
|
digest: emptyDigest,
|
|
446
454
|
items: [
|
|
@@ -481,6 +489,8 @@ export function mirrorLabels(projectRoot, options) {
|
|
|
481
489
|
let skippedUnreadable = 0;
|
|
482
490
|
let skippedClosed = 0;
|
|
483
491
|
let skippedAuthor = 0;
|
|
492
|
+
let reEnrichPlanned = 0;
|
|
493
|
+
let reEnrichApplied = 0;
|
|
484
494
|
let errors = 0;
|
|
485
495
|
let applyWritesSinceSleep = 0;
|
|
486
496
|
for (const [repo, issueNumber] of pairs) {
|
|
@@ -573,7 +583,10 @@ export function mirrorLabels(projectRoot, options) {
|
|
|
573
583
|
else {
|
|
574
584
|
current = issueLabelNames(issue);
|
|
575
585
|
}
|
|
576
|
-
|
|
586
|
+
// Default one-shot: already stamped → skip. Opt-in --re-enrich continues past
|
|
587
|
+
// the stamp and plans additive deltas only (#3197; never removals in v1).
|
|
588
|
+
const alreadyStamped = policy.idempotencyLabel.length > 0 && current.includes(policy.idempotencyLabel);
|
|
589
|
+
if (alreadyStamped && !reEnrich) {
|
|
577
590
|
skippedAlready += 1;
|
|
578
591
|
items.push({
|
|
579
592
|
repo,
|
|
@@ -590,6 +603,7 @@ export function mirrorLabels(projectRoot, options) {
|
|
|
590
603
|
});
|
|
591
604
|
continue;
|
|
592
605
|
}
|
|
606
|
+
const isReEnrichRow = alreadyStamped && reEnrich;
|
|
593
607
|
// Pass number set only when this exact repo+number is referenced from xBRIEF.
|
|
594
608
|
const key = `${repo.toLowerCase()}\0${issueNumber}`;
|
|
595
609
|
const vbriefReferenced = referencedKeys.has(key)
|
|
@@ -603,6 +617,26 @@ export function mirrorLabels(projectRoot, options) {
|
|
|
603
617
|
now,
|
|
604
618
|
});
|
|
605
619
|
if (classification === null) {
|
|
620
|
+
// Re-enrich of a stamped issue with no current rule match: leave labels as-is
|
|
621
|
+
// (additive-only v1 never strips triaged / action chips). Count as unchanged.
|
|
622
|
+
if (isReEnrichRow) {
|
|
623
|
+
unchanged += 1;
|
|
624
|
+
items.push({
|
|
625
|
+
repo,
|
|
626
|
+
issue_number: issueNumber,
|
|
627
|
+
state,
|
|
628
|
+
action: null,
|
|
629
|
+
reason: null,
|
|
630
|
+
ruleKind: null,
|
|
631
|
+
current: [...current].sort(),
|
|
632
|
+
desired: [...current].sort(),
|
|
633
|
+
add: [],
|
|
634
|
+
status: "unchanged",
|
|
635
|
+
re_enrich: true,
|
|
636
|
+
message: `re-enrich: no classify match; left existing labels (incl. ${policy.idempotencyLabel})`,
|
|
637
|
+
});
|
|
638
|
+
continue;
|
|
639
|
+
}
|
|
606
640
|
skippedNoMatch += 1;
|
|
607
641
|
items.push({
|
|
608
642
|
repo,
|
|
@@ -620,7 +654,9 @@ export function mirrorLabels(projectRoot, options) {
|
|
|
620
654
|
}
|
|
621
655
|
const desired = desiredLabelsForClassification(classification.action, policy);
|
|
622
656
|
const currentSet = new Set(current);
|
|
657
|
+
// Additive-only: never plan removals even under re-enrich (#3197 v1).
|
|
623
658
|
const add = desired.filter((label) => !currentSet.has(label)).sort();
|
|
659
|
+
const reEnrichFlag = isReEnrichRow ? { re_enrich: true } : {};
|
|
624
660
|
if (add.length === 0) {
|
|
625
661
|
unchanged += 1;
|
|
626
662
|
items.push({
|
|
@@ -634,11 +670,15 @@ export function mirrorLabels(projectRoot, options) {
|
|
|
634
670
|
desired,
|
|
635
671
|
add: [],
|
|
636
672
|
status: "unchanged",
|
|
673
|
+
...reEnrichFlag,
|
|
637
674
|
});
|
|
638
675
|
continue;
|
|
639
676
|
}
|
|
640
677
|
if (dryRun) {
|
|
641
678
|
planned += 1;
|
|
679
|
+
if (isReEnrichRow) {
|
|
680
|
+
reEnrichPlanned += 1;
|
|
681
|
+
}
|
|
642
682
|
items.push({
|
|
643
683
|
repo,
|
|
644
684
|
issue_number: issueNumber,
|
|
@@ -650,6 +690,7 @@ export function mirrorLabels(projectRoot, options) {
|
|
|
650
690
|
desired,
|
|
651
691
|
add,
|
|
652
692
|
status: "planned",
|
|
693
|
+
...reEnrichFlag,
|
|
653
694
|
});
|
|
654
695
|
continue;
|
|
655
696
|
}
|
|
@@ -673,6 +714,7 @@ export function mirrorLabels(projectRoot, options) {
|
|
|
673
714
|
add,
|
|
674
715
|
status: "error",
|
|
675
716
|
message: mutateGate.reason ?? `refusing cross-repo mutation on ${repo}`,
|
|
717
|
+
...reEnrichFlag,
|
|
676
718
|
});
|
|
677
719
|
continue;
|
|
678
720
|
}
|
|
@@ -690,6 +732,7 @@ export function mirrorLabels(projectRoot, options) {
|
|
|
690
732
|
add,
|
|
691
733
|
status: "error",
|
|
692
734
|
message: "no LabelClient available for apply",
|
|
735
|
+
...reEnrichFlag,
|
|
693
736
|
});
|
|
694
737
|
continue;
|
|
695
738
|
}
|
|
@@ -700,8 +743,12 @@ export function mirrorLabels(projectRoot, options) {
|
|
|
700
743
|
sleepMs(delayMs);
|
|
701
744
|
}
|
|
702
745
|
applyWritesSinceSleep += 1;
|
|
746
|
+
// Additive-only: remove list is always empty (never strip labels; #3197).
|
|
703
747
|
client.apply(repo, issueNumber, add, []);
|
|
704
748
|
applied += 1;
|
|
749
|
+
if (isReEnrichRow) {
|
|
750
|
+
reEnrichApplied += 1;
|
|
751
|
+
}
|
|
705
752
|
items.push({
|
|
706
753
|
repo,
|
|
707
754
|
issue_number: issueNumber,
|
|
@@ -713,6 +760,7 @@ export function mirrorLabels(projectRoot, options) {
|
|
|
713
760
|
desired,
|
|
714
761
|
add,
|
|
715
762
|
status: "applied",
|
|
763
|
+
...reEnrichFlag,
|
|
716
764
|
});
|
|
717
765
|
}
|
|
718
766
|
catch (exc) {
|
|
@@ -730,6 +778,7 @@ export function mirrorLabels(projectRoot, options) {
|
|
|
730
778
|
add,
|
|
731
779
|
status: "error",
|
|
732
780
|
message: formatMissingLabelHint(rawMsg, add),
|
|
781
|
+
...reEnrichFlag,
|
|
733
782
|
});
|
|
734
783
|
// Partial failure: continue remaining issues (idempotent re-run skips applied).
|
|
735
784
|
}
|
|
@@ -746,6 +795,8 @@ export function mirrorLabels(projectRoot, options) {
|
|
|
746
795
|
skipped_unreadable: skippedUnreadable,
|
|
747
796
|
skipped_closed: skippedClosed,
|
|
748
797
|
skipped_author: skippedAuthor,
|
|
798
|
+
re_enrich_planned: reEnrichPlanned,
|
|
799
|
+
re_enrich_applied: reEnrichApplied,
|
|
749
800
|
errors,
|
|
750
801
|
digest,
|
|
751
802
|
items,
|
|
@@ -763,16 +814,27 @@ function formatCountMap(map) {
|
|
|
763
814
|
export function renderLabelMirrorReport(outcome) {
|
|
764
815
|
const lines = [];
|
|
765
816
|
const mode = outcome.dry_run ? "dry-run" : "apply";
|
|
766
|
-
|
|
817
|
+
const reEnrichOn = outcome.filters.re_enrich === true;
|
|
818
|
+
lines.push(reEnrichOn
|
|
819
|
+
? `triage:classify --mirror --re-enrich (${mode}) — additive re-enrich (#3197 / #1423)`
|
|
820
|
+
: `triage:classify --mirror (${mode}) — bootstrap mass-triage (#1423 Wave 2)`);
|
|
767
821
|
const stateFilter = outcome.filters.include_closed ? "all (include-closed)" : "open-only";
|
|
768
822
|
const repoPart = outcome.filters.repo ?? "*";
|
|
769
823
|
const authorPart = outcome.filters.author ?? "*";
|
|
770
|
-
|
|
824
|
+
const reEnrichPart = reEnrichOn ? "on (additive-only)" : "off";
|
|
825
|
+
lines.push(`filters: state=${stateFilter} repo=${repoPart} author=${authorPart} re_enrich=${reEnrichPart}`);
|
|
826
|
+
const firstTimePlanned = Math.max(0, outcome.planned - outcome.re_enrich_planned);
|
|
827
|
+
const firstTimeApplied = Math.max(0, outcome.applied - outcome.re_enrich_applied);
|
|
771
828
|
lines.push(`scanned=${outcome.scanned} planned=${outcome.planned} applied=${outcome.applied} ` +
|
|
772
829
|
`unchanged=${outcome.unchanged} already_triaged=${outcome.skipped_already_triaged} ` +
|
|
773
830
|
`no_match=${outcome.skipped_no_match} closed_skipped=${outcome.skipped_closed} ` +
|
|
774
831
|
`author_skipped=${outcome.skipped_author} ` +
|
|
775
832
|
`unreadable=${outcome.skipped_unreadable} errors=${outcome.errors}`);
|
|
833
|
+
// Distinguish first-time stamp rows vs re-enrich additive backfill (#3197 / #3124 re-run vs re-enrich).
|
|
834
|
+
lines.push(`planned_kind: first_time=${firstTimePlanned} re_enrich=${outcome.re_enrich_planned}` +
|
|
835
|
+
(outcome.dry_run
|
|
836
|
+
? ""
|
|
837
|
+
: ` | applied_kind: first_time=${firstTimeApplied} re_enrich=${outcome.re_enrich_applied}`));
|
|
776
838
|
lines.push(`idempotencyLabel=${outcome.policy.idempotencyLabel} alwaysLabels=${JSON.stringify(outcome.policy.alwaysLabels)}`);
|
|
777
839
|
if (!outcome.dry_run) {
|
|
778
840
|
lines.push(`apply: batch_size=${outcome.batch_size ?? DEFAULT_APPLY_BATCH_SIZE} delay_ms=${outcome.delay_ms ?? DEFAULT_APPLY_DELAY_MS}`);
|
|
@@ -797,8 +859,9 @@ export function renderLabelMirrorReport(outcome) {
|
|
|
797
859
|
const actionPart = item.action !== null ? ` action=${item.action}` : "";
|
|
798
860
|
const rulePart = item.ruleKind !== null ? ` rule=${item.ruleKind}` : "";
|
|
799
861
|
const statePart = item.state !== null ? ` state=${item.state}` : "";
|
|
862
|
+
const kindPart = item.re_enrich === true ? " kind=re-enrich" : " kind=first-time";
|
|
800
863
|
const addPart = sanitizeReportFragment(item.add.join(", +"));
|
|
801
|
-
lines.push(`- ${item.repo}#${item.issue_number}:${statePart}${actionPart}${rulePart} +${addPart}`);
|
|
864
|
+
lines.push(`- ${item.repo}#${item.issue_number}:${statePart}${actionPart}${rulePart}${kindPart} +${addPart}`);
|
|
802
865
|
}
|
|
803
866
|
if (outcome.digest.sample_truncated) {
|
|
804
867
|
const remaining = writeTotal - outcome.digest.samples.length;
|
|
@@ -817,6 +880,9 @@ export function renderLabelMirrorReport(outcome) {
|
|
|
817
880
|
if (already.length > 0) {
|
|
818
881
|
lines.push("");
|
|
819
882
|
lines.push(`Skipped (already ${outcome.policy.idempotencyLabel}): ${already.length}`);
|
|
883
|
+
if (!reEnrichOn) {
|
|
884
|
+
lines.push(" Tip: after actionLabels / rule changes, use --mirror --re-enrich (dry-run) to plan additive chips on already-stamped issues (#3197; re-run vs re-enrich #3124).");
|
|
885
|
+
}
|
|
820
886
|
}
|
|
821
887
|
const noMatch = outcome.items.filter((i) => i.status === "skipped_no_match");
|
|
822
888
|
if (noMatch.length > 0) {
|
|
@@ -833,7 +899,9 @@ export function renderLabelMirrorReport(outcome) {
|
|
|
833
899
|
}
|
|
834
900
|
if (outcome.dry_run && writeTotal > 0) {
|
|
835
901
|
lines.push("");
|
|
836
|
-
lines.push(
|
|
902
|
+
lines.push(reEnrichOn
|
|
903
|
+
? "Dry-run — re-run with --mirror --re-enrich --apply to write additive labels via SCM (batched; never triage:accept; never removals)."
|
|
904
|
+
: "Dry-run — re-run with --mirror --apply to write these labels via SCM (batched; never triage:accept).");
|
|
837
905
|
}
|
|
838
906
|
return `${lines.join("\n")}\n`;
|
|
839
907
|
}
|
|
@@ -851,12 +919,15 @@ export function labelMirrorOutcomeToJson(outcome) {
|
|
|
851
919
|
skipped_unreadable: outcome.skipped_unreadable,
|
|
852
920
|
skipped_closed: outcome.skipped_closed,
|
|
853
921
|
skipped_author: outcome.skipped_author,
|
|
922
|
+
re_enrich_planned: outcome.re_enrich_planned,
|
|
923
|
+
re_enrich_applied: outcome.re_enrich_applied,
|
|
854
924
|
errors: outcome.errors,
|
|
855
925
|
filters: {
|
|
856
926
|
include_closed: outcome.filters.include_closed,
|
|
857
927
|
repo: outcome.filters.repo,
|
|
858
928
|
author: outcome.filters.author,
|
|
859
929
|
author_logins: outcome.filters.author_logins,
|
|
930
|
+
re_enrich: outcome.filters.re_enrich,
|
|
860
931
|
},
|
|
861
932
|
digest: {
|
|
862
933
|
by_state: { ...outcome.digest.by_state },
|
|
@@ -890,6 +961,7 @@ function itemToJson(i) {
|
|
|
890
961
|
add: [...i.add],
|
|
891
962
|
status: i.status,
|
|
892
963
|
...(i.message !== undefined ? { message: i.message } : {}),
|
|
964
|
+
...(i.re_enrich === true ? { re_enrich: true } : {}),
|
|
893
965
|
};
|
|
894
966
|
}
|
|
895
967
|
/** Validate triageLabelMirror on a plan object (vbrief_validate hook). */
|
|
@@ -169,12 +169,12 @@ export declare const registryData: {
|
|
|
169
169
|
readonly "task triage:classify": {
|
|
170
170
|
readonly name: "task triage:classify";
|
|
171
171
|
readonly summary: "Inspect / validate auto-classification; bootstrap mass-triage label mirror";
|
|
172
|
-
readonly refs: "(D10 / #1129, #1423 Wave 1–2 / #3125, #3129)";
|
|
173
|
-
readonly description: "Inspect or validate the auto-classification rule set. --list renders effective rules (framework universal first, consumer overrides next). --validate exits non-zero on a malformed plan.policy.triageAutoClassify or triageLabelMirror. --mirror is the bootstrap mass-triage entrypoint (#3125): classifies the github-issue cache and mirrors outcomes as SCM labels. Open-only by default (opt-in --include-closed); optional --author LOGIN scopes plan/apply to matching author.login (AND with open-only; #3129);
|
|
174
|
-
readonly usage: "task triage:classify -- [--list | --validate | --mirror [--apply] [--include-closed] [--author LOGIN|@me] [--repo owner/name] [--batch-size N] [--delay-ms N] [--sample-limit N] [--json] [--allow-cross-repo]]";
|
|
175
|
-
readonly flags: readonly [readonly ["--list", "(default)", "Print effective rules + hold markers."], readonly ["--validate", "(off)", "Validate plan.policy.triageAutoClassify + triageLabelMirror."], readonly ["--mirror", "(off)", "Bootstrap mass-triage label mirror: classify cache → planned/applied labels (dry-run default, open-only)."], readonly ["--apply", "(off)", "With --mirror: write labels via SCM (batched; requires github SCM boundary)."], readonly ["--include-closed", "(off)", "With --mirror: include closed issues (default open-only avoids archive mass-stamp)."], readonly ["--author LOGIN|@me", "(none)", "With --mirror: only plan/apply issues whose cache author.login matches (exact; @me; comma allow-list) (#3129)."], readonly ["--author-mine", "(off)", "With --mirror: alias for --author @me (#3129)."], readonly ["--repo owner/name", "(all cached)", "Limit mirror to one repo."], readonly ["--batch-size N", "10", "With --mirror --apply: writes per batch before delay."], readonly ["--delay-ms N", "1000", "With --mirror --apply: ms sleep between batches (rate-limit)."], readonly ["--sample-limit N", "15", "With --mirror: max samples in human digest."], readonly ["--json", "(off)", "With --mirror: structured JSON outcome including digest aggregates."], readonly ["--allow-cross-repo", "(off)", "With --mirror --apply: allow non-project repos (#2601)."]];
|
|
176
|
-
readonly examples: readonly ["task triage:classify -- --list", "task triage:classify -- --validate", "task triage:classify -- --mirror", "task triage:classify -- --mirror --author @me --repo owner/name", "task triage:classify -- --mirror --repo owner/name --json", "task triage:classify -- --mirror --apply --repo owner/name --batch-size 10 --delay-ms 1000", "task triage:classify -- --mirror --include-closed --repo owner/name"];
|
|
177
|
-
readonly see_also: readonly ["task triage:bootstrap", "task triage:queue", "task vbrief:reconcile:labels", "#1119 / D10", "#1423", "#3125", "#3129"];
|
|
172
|
+
readonly refs: "(D10 / #1129, #1423 Wave 1–2 / #3125, #3129, #3197)";
|
|
173
|
+
readonly description: "Inspect or validate the auto-classification rule set. --list renders effective rules (framework universal first, consumer overrides next). --validate exits non-zero on a malformed plan.policy.triageAutoClassify or triageLabelMirror. --mirror is the bootstrap mass-triage entrypoint (#3125): classifies the github-issue cache and mirrors outcomes as SCM labels. Open-only by default (opt-in --include-closed); optional --author LOGIN scopes plan/apply to matching author.login (AND with open-only; #3129). Default re-run skips already-stamped (idempotency) issues; after policy/rule changes use --re-enrich to re-classify already-triaged issues and plan additive label deltas only (#3197; re-run vs re-enrich discovery #3124). Dry-run digest shows totals + by state/rule/action + samples, first-time vs re-enrich planned rows, and surfaces the author filter; --apply batches writes with rate-limit delay. Never calls triage:accept / never writes proposed/ xBRIEFs.";
|
|
174
|
+
readonly usage: "task triage:classify -- [--list | --validate | --mirror [--apply] [--re-enrich] [--include-closed] [--author LOGIN|@me] [--repo owner/name] [--batch-size N] [--delay-ms N] [--sample-limit N] [--json] [--allow-cross-repo]]";
|
|
175
|
+
readonly flags: readonly [readonly ["--list", "(default)", "Print effective rules + hold markers."], readonly ["--validate", "(off)", "Validate plan.policy.triageAutoClassify + triageLabelMirror."], readonly ["--mirror", "(off)", "Bootstrap mass-triage label mirror: classify cache → planned/applied labels (dry-run default, open-only)."], readonly ["--apply", "(off)", "With --mirror: write labels via SCM (batched; requires github SCM boundary)."], readonly ["--re-enrich", "(off)", "With --mirror: re-classify already-stamped issues; plan additive label deltas only (dry-run default; #3197 / #3124 re-run vs re-enrich)."], readonly ["--include-closed", "(off)", "With --mirror: include closed issues (default open-only avoids archive mass-stamp)."], readonly ["--author LOGIN|@me", "(none)", "With --mirror: only plan/apply issues whose cache author.login matches (exact; @me; comma allow-list) (#3129)."], readonly ["--author-mine", "(off)", "With --mirror: alias for --author @me (#3129)."], readonly ["--repo owner/name", "(all cached)", "Limit mirror to one repo."], readonly ["--batch-size N", "10", "With --mirror --apply: writes per batch before delay."], readonly ["--delay-ms N", "1000", "With --mirror --apply: ms sleep between batches (rate-limit)."], readonly ["--sample-limit N", "15", "With --mirror: max samples in human digest."], readonly ["--json", "(off)", "With --mirror: structured JSON outcome including digest aggregates."], readonly ["--allow-cross-repo", "(off)", "With --mirror --apply: allow non-project repos (#2601)."]];
|
|
176
|
+
readonly examples: readonly ["task triage:classify -- --list", "task triage:classify -- --validate", "task triage:classify -- --mirror", "task triage:classify -- --mirror --author @me --repo owner/name", "task triage:classify -- --mirror --repo owner/name --json", "task triage:classify -- --mirror --apply --repo owner/name --batch-size 10 --delay-ms 1000", "task triage:classify -- --mirror --re-enrich --repo owner/name", "task triage:classify -- --mirror --re-enrich --apply --repo owner/name", "task triage:classify -- --mirror --include-closed --repo owner/name"];
|
|
177
|
+
readonly see_also: readonly ["task triage:bootstrap", "task triage:queue", "task vbrief:reconcile:labels", "#1119 / D10", "#1423", "#3124", "#3125", "#3129", "#3197"];
|
|
178
178
|
readonly placeholder: false;
|
|
179
179
|
};
|
|
180
180
|
readonly "task triage:bootstrap": {
|
|
@@ -278,9 +278,9 @@ export const registryData = {
|
|
|
278
278
|
"task triage:classify": {
|
|
279
279
|
name: "task triage:classify",
|
|
280
280
|
summary: "Inspect / validate auto-classification; bootstrap mass-triage label mirror",
|
|
281
|
-
refs: "(D10 / #1129, #1423 Wave 1–2 / #3125, #3129)",
|
|
282
|
-
description: "Inspect or validate the auto-classification rule set. --list renders effective rules (framework universal first, consumer overrides next). --validate exits non-zero on a malformed plan.policy.triageAutoClassify or triageLabelMirror. --mirror is the bootstrap mass-triage entrypoint (#3125): classifies the github-issue cache and mirrors outcomes as SCM labels. Open-only by default (opt-in --include-closed); optional --author LOGIN scopes plan/apply to matching author.login (AND with open-only; #3129);
|
|
283
|
-
usage: "task triage:classify -- [--list | --validate | --mirror [--apply] [--include-closed] [--author LOGIN|@me] [--repo owner/name] [--batch-size N] [--delay-ms N] [--sample-limit N] [--json] [--allow-cross-repo]]",
|
|
281
|
+
refs: "(D10 / #1129, #1423 Wave 1–2 / #3125, #3129, #3197)",
|
|
282
|
+
description: "Inspect or validate the auto-classification rule set. --list renders effective rules (framework universal first, consumer overrides next). --validate exits non-zero on a malformed plan.policy.triageAutoClassify or triageLabelMirror. --mirror is the bootstrap mass-triage entrypoint (#3125): classifies the github-issue cache and mirrors outcomes as SCM labels. Open-only by default (opt-in --include-closed); optional --author LOGIN scopes plan/apply to matching author.login (AND with open-only; #3129). Default re-run skips already-stamped (idempotency) issues; after policy/rule changes use --re-enrich to re-classify already-triaged issues and plan additive label deltas only (#3197; re-run vs re-enrich discovery #3124). Dry-run digest shows totals + by state/rule/action + samples, first-time vs re-enrich planned rows, and surfaces the author filter; --apply batches writes with rate-limit delay. Never calls triage:accept / never writes proposed/ xBRIEFs.",
|
|
283
|
+
usage: "task triage:classify -- [--list | --validate | --mirror [--apply] [--re-enrich] [--include-closed] [--author LOGIN|@me] [--repo owner/name] [--batch-size N] [--delay-ms N] [--sample-limit N] [--json] [--allow-cross-repo]]",
|
|
284
284
|
flags: [
|
|
285
285
|
["--list", "(default)", "Print effective rules + hold markers."],
|
|
286
286
|
["--validate", "(off)", "Validate plan.policy.triageAutoClassify + triageLabelMirror."],
|
|
@@ -294,6 +294,11 @@ export const registryData = {
|
|
|
294
294
|
"(off)",
|
|
295
295
|
"With --mirror: write labels via SCM (batched; requires github SCM boundary).",
|
|
296
296
|
],
|
|
297
|
+
[
|
|
298
|
+
"--re-enrich",
|
|
299
|
+
"(off)",
|
|
300
|
+
"With --mirror: re-classify already-stamped issues; plan additive label deltas only (dry-run default; #3197 / #3124 re-run vs re-enrich).",
|
|
301
|
+
],
|
|
297
302
|
[
|
|
298
303
|
"--include-closed",
|
|
299
304
|
"(off)",
|
|
@@ -319,6 +324,8 @@ export const registryData = {
|
|
|
319
324
|
"task triage:classify -- --mirror --author @me --repo owner/name",
|
|
320
325
|
"task triage:classify -- --mirror --repo owner/name --json",
|
|
321
326
|
"task triage:classify -- --mirror --apply --repo owner/name --batch-size 10 --delay-ms 1000",
|
|
327
|
+
"task triage:classify -- --mirror --re-enrich --repo owner/name",
|
|
328
|
+
"task triage:classify -- --mirror --re-enrich --apply --repo owner/name",
|
|
322
329
|
"task triage:classify -- --mirror --include-closed --repo owner/name",
|
|
323
330
|
],
|
|
324
331
|
see_also: [
|
|
@@ -327,8 +334,10 @@ export const registryData = {
|
|
|
327
334
|
"task vbrief:reconcile:labels",
|
|
328
335
|
"#1119 / D10",
|
|
329
336
|
"#1423",
|
|
337
|
+
"#3124",
|
|
330
338
|
"#3125",
|
|
331
339
|
"#3129",
|
|
340
|
+
"#3197",
|
|
332
341
|
],
|
|
333
342
|
placeholder: false,
|
|
334
343
|
},
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, statSync, unlinkSync, } from "node:fs";
|
|
2
2
|
import { basename, dirname, resolve } from "node:path";
|
|
3
3
|
import { containedWrite } from "../fs/contained-write.js";
|
|
4
|
+
import { assertProjectionContained, ProjectionContainmentError, } from "../fs/projection-containment.js";
|
|
4
5
|
import { utcNowIso } from "../scope/vbrief-json.js";
|
|
5
6
|
import { pythonJsonPretty } from "../vbrief-build/json.js";
|
|
6
7
|
import { ACTIVE_FOLDER, ELIGIBLE_STATUSES_FOR_FLIP, formatEligibleStatusList, SOURCE_FOLDERS, TARGET_STATUS, } from "./constants.js";
|
|
@@ -120,6 +121,22 @@ export function activate(vbriefPath, options = {}) {
|
|
|
120
121
|
`'${TARGET_STATUS}'.`,
|
|
121
122
|
};
|
|
122
123
|
}
|
|
124
|
+
// Resolve destination early so containment can refuse before any mutation.
|
|
125
|
+
// Parity with scope:activate / #2447: projectRoot is parent of the xbrief/ root.
|
|
126
|
+
const vbriefDir = dirname(dirname(vbriefPath));
|
|
127
|
+
const projectRoot = dirname(vbriefDir);
|
|
128
|
+
const activeDir = `${vbriefDir}/${ACTIVE_FOLDER}`;
|
|
129
|
+
try {
|
|
130
|
+
// #3147 / #2447: refuse when active/ (or a parent) is a symlink escaping the checkout.
|
|
131
|
+
// Run before mutating the source payload or unlinking pending so a refusal leaves state intact.
|
|
132
|
+
assertProjectionContained(projectRoot, activeDir);
|
|
133
|
+
}
|
|
134
|
+
catch (err) {
|
|
135
|
+
if (err instanceof ProjectionContainmentError) {
|
|
136
|
+
return { exitCode: 1, message: err.message };
|
|
137
|
+
}
|
|
138
|
+
throw err;
|
|
139
|
+
}
|
|
123
140
|
planObj.status = TARGET_STATUS;
|
|
124
141
|
let info = payload.vBRIEFInfo;
|
|
125
142
|
if (info === undefined) {
|
|
@@ -133,8 +150,6 @@ export function activate(vbriefPath, options = {}) {
|
|
|
133
150
|
};
|
|
134
151
|
}
|
|
135
152
|
info.updated = utcNowIso(now);
|
|
136
|
-
const vbriefDir = dirname(dirname(vbriefPath));
|
|
137
|
-
const activeDir = `${vbriefDir}/${ACTIVE_FOLDER}`;
|
|
138
153
|
try {
|
|
139
154
|
mkdirSync(activeDir, { recursive: true });
|
|
140
155
|
}
|
|
@@ -151,13 +166,14 @@ export function activate(vbriefPath, options = {}) {
|
|
|
151
166
|
"collision manually before re-running `task vbrief:activate`.",
|
|
152
167
|
};
|
|
153
168
|
}
|
|
154
|
-
const tmpBase = `${fileName}.tmp`;
|
|
155
169
|
const tmp = `${dest}.tmp`;
|
|
156
170
|
try {
|
|
157
|
-
// #2980 wave D: product write sink routes through containedWrite
|
|
171
|
+
// #2980 wave D + #3147: product write sink routes through containedWrite with
|
|
172
|
+
// checkout root as containment root (not resolve(activeDir), which treats an
|
|
173
|
+
// escaping active/ realpath as "contained" relative to itself).
|
|
158
174
|
containedWrite({
|
|
159
|
-
root: resolve(
|
|
160
|
-
target:
|
|
175
|
+
root: resolve(projectRoot),
|
|
176
|
+
target: resolve(tmp),
|
|
161
177
|
data: pythonJsonPretty(payload),
|
|
162
178
|
mode: "create",
|
|
163
179
|
});
|
|
@@ -14,6 +14,10 @@ export declare function validateHostSlashCommandsOnPlan(plan: unknown, filepath:
|
|
|
14
14
|
export declare function validateOpenClawProductCommandsOnPlan(plan: unknown, filepath: string): string[];
|
|
15
15
|
/** vbrief_validate hook: validate ``plan.policy.stalenessTickler`` (#2489). */
|
|
16
16
|
export declare function validateStalenessTicklerOnPlan(plan: unknown, filepath: string): string[];
|
|
17
|
+
/** vbrief_validate hook: validate ``plan.policy.coverageDebt`` (#3189). */
|
|
18
|
+
export declare function validateCoverageDebtOnPlan(plan: unknown, filepath: string): string[];
|
|
19
|
+
/** vbrief_validate hook: validate ``plan.policy.checkResume`` (#3189). */
|
|
20
|
+
export declare function validateCheckResumeOnPlan(plan: unknown, filepath: string): string[];
|
|
17
21
|
/** Run all PROJECT-DEFINITION policy hooks (mirrors lazy-import block in Python). */
|
|
18
22
|
export declare function runProjectDefinitionHooks(plan: unknown, filepath: string): string[];
|
|
19
23
|
//# sourceMappingURL=plan-hooks.d.ts.map
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { validateCheckResume } from "../policy/check-resume.js";
|
|
2
|
+
import { validateCoverageDebt } from "../policy/coverage-debt.js";
|
|
1
3
|
import { validateHostHooks } from "../policy/host-hooks.js";
|
|
2
4
|
import { validateHostSlashCommands } from "../policy/host-slash-commands.js";
|
|
3
5
|
import { readPlanPolicy } from "../policy/plan-extensions.js";
|
|
@@ -196,6 +198,51 @@ export function validateStalenessTicklerOnPlan(plan, filepath) {
|
|
|
196
198
|
}
|
|
197
199
|
return out;
|
|
198
200
|
}
|
|
201
|
+
/** vbrief_validate hook: validate ``plan.policy.coverageDebt`` (#3189). */
|
|
202
|
+
export function validateCoverageDebtOnPlan(plan, filepath) {
|
|
203
|
+
if (typeof plan !== "object" || plan === null || Array.isArray(plan)) {
|
|
204
|
+
return [];
|
|
205
|
+
}
|
|
206
|
+
const policy = readPlanPolicy(plan);
|
|
207
|
+
if (typeof policy !== "object" || policy === null || Array.isArray(policy)) {
|
|
208
|
+
return [];
|
|
209
|
+
}
|
|
210
|
+
if (!("coverageDebt" in policy)) {
|
|
211
|
+
return [];
|
|
212
|
+
}
|
|
213
|
+
const raw = policy.coverageDebt;
|
|
214
|
+
// Explicit null is a typed key with invalid value — fail closed at validation time.
|
|
215
|
+
if (raw === null) {
|
|
216
|
+
return [`${filepath}: plan.policy.coverageDebt must be an object; got null (#3189)`];
|
|
217
|
+
}
|
|
218
|
+
const out = [];
|
|
219
|
+
for (const err of validateCoverageDebt(raw)) {
|
|
220
|
+
out.push(`${filepath}: ${err} (#3189)`);
|
|
221
|
+
}
|
|
222
|
+
return out;
|
|
223
|
+
}
|
|
224
|
+
/** vbrief_validate hook: validate ``plan.policy.checkResume`` (#3189). */
|
|
225
|
+
export function validateCheckResumeOnPlan(plan, filepath) {
|
|
226
|
+
if (typeof plan !== "object" || plan === null || Array.isArray(plan)) {
|
|
227
|
+
return [];
|
|
228
|
+
}
|
|
229
|
+
const policy = readPlanPolicy(plan);
|
|
230
|
+
if (typeof policy !== "object" || policy === null || Array.isArray(policy)) {
|
|
231
|
+
return [];
|
|
232
|
+
}
|
|
233
|
+
if (!("checkResume" in policy)) {
|
|
234
|
+
return [];
|
|
235
|
+
}
|
|
236
|
+
const raw = policy.checkResume;
|
|
237
|
+
if (raw === null) {
|
|
238
|
+
return [`${filepath}: plan.policy.checkResume must be an object; got null (#3189)`];
|
|
239
|
+
}
|
|
240
|
+
const out = [];
|
|
241
|
+
for (const err of validateCheckResume(raw)) {
|
|
242
|
+
out.push(`${filepath}: ${err} (#3189)`);
|
|
243
|
+
}
|
|
244
|
+
return out;
|
|
245
|
+
}
|
|
199
246
|
/** Run all PROJECT-DEFINITION policy hooks (mirrors lazy-import block in Python). */
|
|
200
247
|
export function runProjectDefinitionHooks(plan, filepath) {
|
|
201
248
|
const errors = [];
|
|
@@ -266,6 +313,13 @@ export function runProjectDefinitionHooks(plan, filepath) {
|
|
|
266
313
|
catch {
|
|
267
314
|
/* hook must not break validation */
|
|
268
315
|
}
|
|
316
|
+
try {
|
|
317
|
+
errors.push(...validateCoverageDebtOnPlan(plan, filepath));
|
|
318
|
+
errors.push(...validateCheckResumeOnPlan(plan, filepath));
|
|
319
|
+
}
|
|
320
|
+
catch {
|
|
321
|
+
/* hook must not break validation */
|
|
322
|
+
}
|
|
269
323
|
return errors;
|
|
270
324
|
}
|
|
271
325
|
//# sourceMappingURL=plan-hooks.js.map
|