@danmoisan/drm-copilot-mcp 1.1.5 → 1.1.6
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/out/mcp-server.js +49 -14
- package/package.json +1 -1
- package/resources/claude-customizations/.claude/hooks/enforce-epic-worktree-removal-gate.ps1 +163 -18
- package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadius.psm1 +11 -0
- package/resources/claude-customizations/.claude/rules/parallel-orchestration.md +1 -0
- package/resources/claude-customizations/.claude/skills/atomic-plan-contract/SKILL.md +28 -0
- package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/SKILL.md +136 -4
- package/resources/claude-customizations/.claude/skills/parallel-add/SKILL.md +3 -1
- package/resources/claude-customizations/.claude/skills/parallel-orchestrate/SKILL.md +18 -9
- package/resources/claude-customizations/.claude/skills/parallel-plan/SKILL.md +8 -1
- package/resources/claude-customizations/.claude/skills/pr-context-artifacts/SKILL.md +22 -0
- package/resources/claude-customizations/.claude/skills/remediation-handoff-atomic-planner/SKILL.md +10 -0
- package/resources/codex-and-agents-customizations/.agents/skills/pr-context-artifacts/SKILL.md +22 -0
- package/resources/codex-and-agents-customizations/.codex/config.toml +1 -1
- package/resources/customizations/.github/skills/pr-context-artifacts/SKILL.md +22 -0
package/out/mcp-server.js
CHANGED
|
@@ -31365,10 +31365,16 @@ function parseSection3(markdown, heading) {
|
|
|
31365
31365
|
function formatDiffPath2(pathText) {
|
|
31366
31366
|
return pathText !== null ? formatDiffPath(pathText) : "";
|
|
31367
31367
|
}
|
|
31368
|
-
|
|
31368
|
+
var GENERATED_CONTEXT_SECTION_TITLE = "Context generated";
|
|
31369
|
+
var HEAD_SHA_LABEL = "Head SHA:";
|
|
31370
|
+
var UNKNOWN_HEAD_SHA_PLACEHOLDER = "(unknown)";
|
|
31371
|
+
function appendGenerationTimestamp(clock = () => /* @__PURE__ */ new Date(), headSha = null) {
|
|
31369
31372
|
const now = clock();
|
|
31370
31373
|
const timestamp = formatUtcTimestamp(now);
|
|
31371
|
-
|
|
31374
|
+
const shaText = headSha !== null && headSha !== "" ? headSha : UNKNOWN_HEAD_SHA_PLACEHOLDER;
|
|
31375
|
+
return section(GENERATED_CONTEXT_SECTION_TITLE) + "\n" + timestamp + `
|
|
31376
|
+
${HEAD_SHA_LABEL} ${shaText}
|
|
31377
|
+
`;
|
|
31372
31378
|
}
|
|
31373
31379
|
function formatUtcTimestamp(date3) {
|
|
31374
31380
|
const year = date3.getUTCFullYear().toString().padStart(4, "0");
|
|
@@ -31716,7 +31722,7 @@ function renderVerificationEvidenceSection(fs10, resolvedRoot, featureDocs) {
|
|
|
31716
31722
|
}
|
|
31717
31723
|
return lines.join("\n");
|
|
31718
31724
|
}
|
|
31719
|
-
function buildSummaryText(collected, fs10, appendixPath) {
|
|
31725
|
+
function buildSummaryText(collected, fs10, appendixPath, generatedSection) {
|
|
31720
31726
|
const ctx = collected.contextResult;
|
|
31721
31727
|
const ghStatusText = resolveGhStatusText(collected);
|
|
31722
31728
|
const intentBlock = [
|
|
@@ -31727,6 +31733,7 @@ function buildSummaryText(collected, fs10, appendixPath) {
|
|
|
31727
31733
|
"Author-asserted autoclose issues:"
|
|
31728
31734
|
].join("\n");
|
|
31729
31735
|
const summarySections = [
|
|
31736
|
+
generatedSection,
|
|
31730
31737
|
section("GitHub CLI status"),
|
|
31731
31738
|
ghStatusText,
|
|
31732
31739
|
intentBlock,
|
|
@@ -31804,7 +31811,7 @@ function buildSummaryText(collected, fs10, appendixPath) {
|
|
|
31804
31811
|
}
|
|
31805
31812
|
return summaryText;
|
|
31806
31813
|
}
|
|
31807
|
-
function buildAppendixText(collected,
|
|
31814
|
+
function buildAppendixText(collected, generatedSection) {
|
|
31808
31815
|
const featureBlock = collected.featureDocs.map((doc) => doc.excerpt).join("\n");
|
|
31809
31816
|
const issueSections = collected.issueDetails.map(
|
|
31810
31817
|
(detail) => issueAppendix(detail)
|
|
@@ -31813,7 +31820,7 @@ function buildAppendixText(collected, clock) {
|
|
|
31813
31820
|
(detail) => prAppendix(detail)
|
|
31814
31821
|
);
|
|
31815
31822
|
const appendixParts = [
|
|
31816
|
-
|
|
31823
|
+
generatedSection,
|
|
31817
31824
|
collected.contextResult.text,
|
|
31818
31825
|
"",
|
|
31819
31826
|
section("Issue details"),
|
|
@@ -31846,17 +31853,23 @@ function writeOutput(fs10, text, outPath, append2) {
|
|
|
31846
31853
|
function collectAndWrite(options) {
|
|
31847
31854
|
const clock = options.clock ?? (() => /* @__PURE__ */ new Date());
|
|
31848
31855
|
const collected = collectPrContext(options);
|
|
31856
|
+
const generatedSection = appendGenerationTimestamp(
|
|
31857
|
+
clock,
|
|
31858
|
+
collected.contextResult.headSha
|
|
31859
|
+
);
|
|
31849
31860
|
const summaryText = buildSummaryText(
|
|
31850
31861
|
collected,
|
|
31851
31862
|
options.fs,
|
|
31852
|
-
options.appendixOut
|
|
31863
|
+
options.appendixOut,
|
|
31864
|
+
generatedSection
|
|
31853
31865
|
);
|
|
31854
|
-
const appendixText = buildAppendixText(collected,
|
|
31866
|
+
const appendixText = buildAppendixText(collected, generatedSection);
|
|
31855
31867
|
writeOutput(options.fs, summaryText, options.out, options.append);
|
|
31856
31868
|
writeOutput(options.fs, appendixText, options.appendixOut, options.append);
|
|
31857
31869
|
const log = options.log ?? (() => void 0);
|
|
31858
31870
|
log(`Wrote context summary to: ${options.out}`);
|
|
31859
31871
|
log(`Wrote context appendix to: ${options.appendixOut}`);
|
|
31872
|
+
return { summaryText, appendixText };
|
|
31860
31873
|
}
|
|
31861
31874
|
function resolveGhStatusText(collected) {
|
|
31862
31875
|
let ghStatusText = collected.ghStatusOverride || collected.ghStatusMessage || "GitHub CLI authenticated.";
|
|
@@ -31928,26 +31941,48 @@ function parentDir(path11) {
|
|
|
31928
31941
|
// ../../extensions/drm-copilot/src/lib/pr-context/pr-context-service-call.ts
|
|
31929
31942
|
var SUMMARY_OUT = "artifacts/pr_context.summary.txt";
|
|
31930
31943
|
var APPENDIX_OUT = "artifacts/pr_context.appendix.txt";
|
|
31944
|
+
function verifyWrittenArtifact(fileSystem, artifactPath, expected) {
|
|
31945
|
+
let actual;
|
|
31946
|
+
try {
|
|
31947
|
+
actual = fileSystem.readTextFile(artifactPath);
|
|
31948
|
+
} catch (error2) {
|
|
31949
|
+
const detail = error2 instanceof Error ? error2.message : String(error2);
|
|
31950
|
+
throw new Error(
|
|
31951
|
+
`Failed to verify PR context artifact '${artifactPath}': the file could not be read back after writing (${detail}).`,
|
|
31952
|
+
{ cause: error2 }
|
|
31953
|
+
);
|
|
31954
|
+
}
|
|
31955
|
+
if (actual !== expected) {
|
|
31956
|
+
throw new Error(
|
|
31957
|
+
`Failed to verify PR context artifact '${artifactPath}': the content read back is not the content this invocation rendered (expected ${String(expected.length)} characters, read back ${String(actual.length)}).`
|
|
31958
|
+
);
|
|
31959
|
+
}
|
|
31960
|
+
}
|
|
31931
31961
|
function collectPrContextServiceCall(input) {
|
|
31932
|
-
|
|
31962
|
+
const summaryOut = normalizeGeneratedPath(
|
|
31963
|
+
(0, import_node_path.join)(input.workspaceRoot, SUMMARY_OUT)
|
|
31964
|
+
);
|
|
31965
|
+
const appendixOut = normalizeGeneratedPath(
|
|
31966
|
+
(0, import_node_path.join)(input.workspaceRoot, APPENDIX_OUT)
|
|
31967
|
+
);
|
|
31968
|
+
const rendered = collectAndWrite({
|
|
31933
31969
|
base: input.base,
|
|
31934
31970
|
repoRoot: input.workspaceRoot,
|
|
31935
|
-
out:
|
|
31936
|
-
appendixOut
|
|
31971
|
+
out: summaryOut,
|
|
31972
|
+
appendixOut,
|
|
31937
31973
|
append: false,
|
|
31938
31974
|
includeUntracked: true,
|
|
31939
31975
|
fs: input.fileSystem,
|
|
31940
31976
|
runner: input.runner,
|
|
31941
31977
|
...input.log === void 0 ? {} : { log: input.log }
|
|
31942
31978
|
});
|
|
31979
|
+
verifyWrittenArtifact(input.fileSystem, summaryOut, rendered.summaryText);
|
|
31980
|
+
verifyWrittenArtifact(input.fileSystem, appendixOut, rendered.appendixText);
|
|
31943
31981
|
return {
|
|
31944
31982
|
tool: "collect_pr_context",
|
|
31945
31983
|
workspaceRoot: input.workspaceRoot,
|
|
31946
31984
|
summary: `Collected PR context against base '${input.base}'.`,
|
|
31947
|
-
artifacts: [
|
|
31948
|
-
normalizeGeneratedPath((0, import_node_path.join)(input.workspaceRoot, SUMMARY_OUT)),
|
|
31949
|
-
normalizeGeneratedPath((0, import_node_path.join)(input.workspaceRoot, APPENDIX_OUT))
|
|
31950
|
-
]
|
|
31985
|
+
artifacts: [summaryOut, appendixOut]
|
|
31951
31986
|
};
|
|
31952
31987
|
}
|
|
31953
31988
|
|
package/package.json
CHANGED
package/resources/claude-customizations/.claude/hooks/enforce-epic-worktree-removal-gate.ps1
CHANGED
|
@@ -4,14 +4,51 @@
|
|
|
4
4
|
|
|
5
5
|
.DESCRIPTION
|
|
6
6
|
Invoked by the Claude Code PreToolUse hook on the "Bash" matcher before any Bash
|
|
7
|
-
command runs. Regex-matches git worktree remove against the envelope's
|
|
8
|
-
extracts the target worktree path argument,
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
7
|
+
command runs. Regex-matches git worktree remove against the envelope's
|
|
8
|
+
tool_input.command, extracts the target worktree path argument, and allows the
|
|
9
|
+
removal only when one of two checkpoint-only conditions holds. The branches are
|
|
10
|
+
evaluated in this order and are ORed, not ANDed: the first that authorizes wins and
|
|
11
|
+
the second is not consulted.
|
|
12
|
+
|
|
13
|
+
1. Epic path: artifacts/orchestration/epic-orchestrator-state.json carries a
|
|
14
|
+
features[] record whose worktree_path matches the removal target and whose
|
|
15
|
+
merge_status is merged or worktree_removed.
|
|
16
|
+
2. Parallel path: artifacts/orchestration/parallel-orchestrator-state.json has
|
|
17
|
+
route_id == "parallel" and carries an items[] entry whose worktree_path matches
|
|
18
|
+
the removal target and whose merge_status is merged or worktree_removed.
|
|
19
|
+
|
|
20
|
+
Otherwise the command is denied with reason EPIC_WORKTREE_REMOVAL_BLOCKED. The gate
|
|
21
|
+
is fail-closed in every failure mode: neither checkpoint present, either checkpoint
|
|
22
|
+
unparseable, route_id absent or not "parallel", no matching worktree_path, a matched
|
|
23
|
+
record whose merge_status is not in the allowed set, or a matched record carrying no
|
|
24
|
+
merge_status key. The cascade is a disjunction of two positive predicates; no
|
|
25
|
+
negative path returns an allow. An envelope-level anomaly is checked first and denies
|
|
26
|
+
before either checkpoint is read.
|
|
27
|
+
|
|
28
|
+
Both branches key on the worktree PATH because the command names a path, and both
|
|
29
|
+
normalize separators and trim a trailing slash on each side of the comparison, so
|
|
30
|
+
Windows- and POSIX-style paths compare equal. Path is also why the two branches are
|
|
31
|
+
mutually exclusive in practice: an epic run and a parallel run allocate worktrees
|
|
32
|
+
under distinct per-run, per-item paths, so a path recorded in one checkpoint is not a
|
|
33
|
+
path recorded in the other, and adding branch 2 cannot widen what branch 1 authorizes.
|
|
34
|
+
The authorization is a property of the path rather than of the caller, and the safety
|
|
35
|
+
property this gate protects - do not destroy unmerged work - is likewise a property of
|
|
36
|
+
the path's recorded merge state.
|
|
37
|
+
|
|
38
|
+
Accepted residual: artifacts/ is gitignored, so the parallel checkpoint persists after
|
|
39
|
+
a run ends and a stale document remains readable here. For a stale document to
|
|
40
|
+
authorize a removal it should not, a worktree would have to exist at a path
|
|
41
|
+
byte-identical, after normalization, to one it records with merge_status merged or
|
|
42
|
+
worktree_removed. Worktree paths carry a session or timestamp component, so a
|
|
43
|
+
collision is implausible, and where the recorded status is worktree_removed the path
|
|
44
|
+
was already deleted. This is a documented accepted trade, not an unexamined gap; the
|
|
45
|
+
route_id check does not reduce it, because a stale parallel checkpoint legitimately
|
|
46
|
+
declares that route.
|
|
47
|
+
|
|
48
|
+
The sibling gate enforce-parallel-worktree-removal-gate.ps1 fires on the same command
|
|
49
|
+
and owns its own reason prefix. PreToolUse denials are conjunctive, so both gates must
|
|
50
|
+
allow for a removal to proceed; this gate keeps the EPIC_WORKTREE_REMOVAL_BLOCKED
|
|
51
|
+
prefix for both of its branches so transcript attribution stays unambiguous.
|
|
15
52
|
|
|
16
53
|
.NOTES
|
|
17
54
|
Compatible with PowerShell 7+. No external module dependencies. Filesystem reads go
|
|
@@ -24,6 +61,7 @@ param()
|
|
|
24
61
|
|
|
25
62
|
Import-Module (Join-Path $PSScriptRoot '../lib/hook-payload/HookPayload.psm1') -Force
|
|
26
63
|
$script:EpicCheckpointPath = 'artifacts/orchestration/epic-orchestrator-state.json'
|
|
64
|
+
$script:ParallelCheckpointPath = 'artifacts/orchestration/parallel-orchestrator-state.json'
|
|
27
65
|
$script:AllowedMergeStatuses = @('merged', 'worktree_removed')
|
|
28
66
|
|
|
29
67
|
function Get-EpicWorktreeGateCheckpointContent {
|
|
@@ -44,6 +82,52 @@ function Get-EpicWorktreeGateCheckpointContent {
|
|
|
44
82
|
return (Get-Content -LiteralPath $script:EpicCheckpointPath -Raw)
|
|
45
83
|
}
|
|
46
84
|
|
|
85
|
+
function Get-EpicWorktreeGateParallelCheckpointContent {
|
|
86
|
+
<#
|
|
87
|
+
.SYNOPSIS
|
|
88
|
+
Read the raw JSON text of the parallel-orchestrator checkpoint. Tests mock
|
|
89
|
+
this function (read seam).
|
|
90
|
+
.OUTPUTS
|
|
91
|
+
System.String or $null
|
|
92
|
+
#>
|
|
93
|
+
[CmdletBinding()]
|
|
94
|
+
[OutputType([string])]
|
|
95
|
+
param()
|
|
96
|
+
|
|
97
|
+
if (-not (Test-Path -LiteralPath $script:ParallelCheckpointPath -PathType Leaf)) {
|
|
98
|
+
return $null
|
|
99
|
+
}
|
|
100
|
+
return (Get-Content -LiteralPath $script:ParallelCheckpointPath -Raw)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function ConvertFrom-EpicWorktreeGateJson {
|
|
104
|
+
<#
|
|
105
|
+
.SYNOPSIS
|
|
106
|
+
Parse checkpoint JSON text, returning $null on unreadable/invalid content.
|
|
107
|
+
.DESCRIPTION
|
|
108
|
+
Shared by both authorization branches so the two checkpoints are parsed by
|
|
109
|
+
one implementation rather than by duplicated inline logic.
|
|
110
|
+
.PARAMETER Raw
|
|
111
|
+
Raw checkpoint text, or $null when the file does not exist.
|
|
112
|
+
.OUTPUTS
|
|
113
|
+
System.Object or $null
|
|
114
|
+
#>
|
|
115
|
+
[CmdletBinding()]
|
|
116
|
+
param(
|
|
117
|
+
[AllowNull()]
|
|
118
|
+
[string] $Raw
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
if ([string]::IsNullOrWhiteSpace($Raw)) {
|
|
122
|
+
return $null
|
|
123
|
+
}
|
|
124
|
+
try {
|
|
125
|
+
return ($Raw | ConvertFrom-Json -ErrorAction Stop)
|
|
126
|
+
} catch {
|
|
127
|
+
return $null
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
47
131
|
function Get-EpicWorktreeRemovalCommandPath {
|
|
48
132
|
<#
|
|
49
133
|
.SYNOPSIS
|
|
@@ -138,6 +222,70 @@ function Test-EpicWorktreeRemovalAllowed {
|
|
|
138
222
|
return $script:AllowedMergeStatuses -contains ([string]$FeatureRecord.merge_status)
|
|
139
223
|
}
|
|
140
224
|
|
|
225
|
+
function Test-ParallelCheckpointAllowsWorktreeRemoval {
|
|
226
|
+
<#
|
|
227
|
+
.SYNOPSIS
|
|
228
|
+
Decision logic for the parallel-orchestrator checkpoint path (branch 2).
|
|
229
|
+
.DESCRIPTION
|
|
230
|
+
Allows only when the checkpoint declares the parallel route identity and
|
|
231
|
+
carries an items[] entry whose worktree_path matches the removal target and
|
|
232
|
+
whose merge_status is in the allowed set. Every other shape returns $false,
|
|
233
|
+
so the branch is a positive predicate with no negative path to an allow.
|
|
234
|
+
.PARAMETER Checkpoint
|
|
235
|
+
Parsed parallel-orchestrator checkpoint, or $null when absent/unreadable.
|
|
236
|
+
.PARAMETER WorktreePath
|
|
237
|
+
The target worktree path extracted from the command text.
|
|
238
|
+
.OUTPUTS
|
|
239
|
+
System.Boolean
|
|
240
|
+
#>
|
|
241
|
+
[CmdletBinding()]
|
|
242
|
+
[OutputType([bool])]
|
|
243
|
+
param(
|
|
244
|
+
[AllowNull()]
|
|
245
|
+
$Checkpoint,
|
|
246
|
+
|
|
247
|
+
[AllowNull()]
|
|
248
|
+
[string] $WorktreePath
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
if ($null -eq $Checkpoint -or [string]::IsNullOrWhiteSpace($WorktreePath)) {
|
|
252
|
+
return $false
|
|
253
|
+
}
|
|
254
|
+
$props = @($Checkpoint.PSObject.Properties.Name)
|
|
255
|
+
# Route identity is orchestrator invariant 2, so a document at the parallel path
|
|
256
|
+
# that does not declare it is malformed by the rule's own definition and inert here.
|
|
257
|
+
if ($props -notcontains 'route_id' -or ([string]$Checkpoint.route_id) -ne 'parallel') {
|
|
258
|
+
return $false
|
|
259
|
+
}
|
|
260
|
+
if ($props -notcontains 'items' -or $null -eq $Checkpoint.items) {
|
|
261
|
+
return $false
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
$normalizedTarget = ($WorktreePath -replace '\\', '/').TrimEnd('/')
|
|
265
|
+
|
|
266
|
+
# Scan every recorded item for a worktree_path that matches the removal target;
|
|
267
|
+
# path separators are normalized exactly as the epic branch normalizes them.
|
|
268
|
+
foreach ($item in @($Checkpoint.items)) {
|
|
269
|
+
if ($null -eq $item) {
|
|
270
|
+
continue
|
|
271
|
+
}
|
|
272
|
+
$itemProps = @($item.PSObject.Properties.Name)
|
|
273
|
+
if ($itemProps -notcontains 'worktree_path') {
|
|
274
|
+
continue
|
|
275
|
+
}
|
|
276
|
+
$normalizedItemPath = (([string]$item.worktree_path) -replace '\\', '/').TrimEnd('/')
|
|
277
|
+
if ($normalizedItemPath -ne $normalizedTarget) {
|
|
278
|
+
continue
|
|
279
|
+
}
|
|
280
|
+
if ($itemProps -notcontains 'merge_status') {
|
|
281
|
+
return $false
|
|
282
|
+
}
|
|
283
|
+
return $script:AllowedMergeStatuses -contains ([string]$item.merge_status)
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return $false
|
|
287
|
+
}
|
|
288
|
+
|
|
141
289
|
function Get-EpicWorktreeGateAllowDecision {
|
|
142
290
|
[CmdletBinding()]
|
|
143
291
|
[OutputType([System.Collections.Specialized.OrderedDictionary])]
|
|
@@ -202,22 +350,19 @@ function Invoke-EpicWorktreeRemovalGateDecision {
|
|
|
202
350
|
|
|
203
351
|
$worktreePath = Get-EpicWorktreeRemovalCommandPath -CommandText $commandText
|
|
204
352
|
|
|
205
|
-
$
|
|
206
|
-
$checkpoint = $null
|
|
207
|
-
if (-not [string]::IsNullOrWhiteSpace($checkpointRaw)) {
|
|
208
|
-
try {
|
|
209
|
-
$checkpoint = $checkpointRaw | ConvertFrom-Json -ErrorAction Stop
|
|
210
|
-
} catch {
|
|
211
|
-
$checkpoint = $null
|
|
212
|
-
}
|
|
213
|
-
}
|
|
353
|
+
$checkpoint = ConvertFrom-EpicWorktreeGateJson -Raw (Get-EpicWorktreeGateCheckpointContent)
|
|
214
354
|
|
|
215
355
|
$featureRecord = Find-EpicWorktreeFeatureRecord -Checkpoint $checkpoint -WorktreePath $worktreePath
|
|
216
356
|
if (Test-EpicWorktreeRemovalAllowed -FeatureRecord $featureRecord) {
|
|
217
357
|
return Get-EpicWorktreeGateAllowDecision
|
|
218
358
|
}
|
|
219
359
|
|
|
220
|
-
|
|
360
|
+
$parallelCheckpoint = ConvertFrom-EpicWorktreeGateJson -Raw (Get-EpicWorktreeGateParallelCheckpointContent)
|
|
361
|
+
if (Test-ParallelCheckpointAllowsWorktreeRemoval -Checkpoint $parallelCheckpoint -WorktreePath $worktreePath) {
|
|
362
|
+
return Get-EpicWorktreeGateAllowDecision
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
return Get-EpicWorktreeGateBlockDecision -Reason "EPIC_WORKTREE_REMOVAL_BLOCKED: git worktree remove for '$worktreePath' requires either an epic checkpoint features[] record with merge_status in {merged, worktree_removed}, or a parallel-orchestrator checkpoint with route_id == ""parallel"" whose matching items[] record (matched by worktree_path) has merge_status in {merged, worktree_removed}. No checkpoint authorized this removal."
|
|
221
366
|
}
|
|
222
367
|
|
|
223
368
|
function Invoke-EpicWorktreeRemovalGateEntryPoint {
|
|
@@ -428,6 +428,17 @@ function Test-BlastRadiusConflict {
|
|
|
428
428
|
.OUTPUTS
|
|
429
429
|
System.Collections.Hashtable. Keys conflict (a boolean) and reasons (an
|
|
430
430
|
array of hashtables with keys kind and detail).
|
|
431
|
+
|
|
432
|
+
Read the verdict from the conflict key of the returned hashtable.
|
|
433
|
+
Do not test the returned object itself: the hashtable is
|
|
434
|
+
unconditionally truthy under PowerShell boolean coercion, so
|
|
435
|
+
'if ($result)' treats every pair as contending.
|
|
436
|
+
System.Collections.Hashtable implements IDictionary and ICollection but
|
|
437
|
+
not IList, and the count-based truthiness rule applies only to IList
|
|
438
|
+
implementations, so a hashtable falls under the rule for any other
|
|
439
|
+
non-collection type and is always $true. The Python port agrees with its
|
|
440
|
+
own verdict; this mirror provably cannot, because PowerShell exposes no
|
|
441
|
+
hook by which a type can decline or change the conversion.
|
|
431
442
|
#>
|
|
432
443
|
[CmdletBinding()]
|
|
433
444
|
[OutputType([hashtable])]
|
|
@@ -409,3 +409,4 @@ an unclassified key or a key present in only one copy fails loudly and names its
|
|
|
409
409
|
- Enforcement is therefore Python validator logic, plus the TypeScript parity port, plus this prose file. It is NEVER an imported JSON Schema. No schema file is read at validation time.
|
|
410
410
|
- The `parallel` route entry lives in `config/orchestration-routing.json` with `requires_pr_gate: false` (there is no run-level pull request to gate; each child's own route checkpoint enforces its per-item pull-request gate) and is mirrored byte-for-byte in `extensions/drm-copilot/resources/config/orchestration-routing.json`.
|
|
411
411
|
- The `PreToolUse` merge gate `.claude/hooks/enforce-epic-merge-gate.ps1` carries a parallel allow-branch that authorizes a per-item `gh pr merge --merge` from the parallel-orchestrator checkpoint when `route_id == "parallel"`, the target item's `merge_status == "ci_green"`, and the command's PR number matches that item's `pr_number`; any other case fails closed with `EPIC_MERGE_GATE_BLOCKED`.
|
|
412
|
+
- The `PreToolUse` worktree-removal gate `.claude/hooks/enforce-epic-worktree-removal-gate.ps1` likewise carries a parallel allow-branch that authorizes a per-item worktree removal from the parallel-orchestrator checkpoint when `route_id == "parallel"` and the `items[]` entry whose `worktree_path` matches the normalized removal target has `merge_status` in `{merged, worktree_removed}`; any other case — neither checkpoint present, either checkpoint unparseable, `route_id` absent or not `"parallel"`, no matching `worktree_path`, or a matched entry whose `merge_status` is outside that set or absent — fails closed with `EPIC_WORKTREE_REMOVAL_BLOCKED`. Removal is keyed on the worktree path rather than on `pr_number`, because the command names a path. This gate and the sibling gate `.claude/hooks/enforce-parallel-worktree-removal-gate.ps1`, which owns `PARALLEL_WORKTREE_REMOVAL_BLOCKED`, both fire on the same command, and `PreToolUse` denials are conjunctive, so both must allow for a removal to proceed.
|
|
@@ -139,6 +139,20 @@ For command-bearing tasks in approved plans (especially Phase 2 final-QC tasks):
|
|
|
139
139
|
|
|
140
140
|
Any regression test task expected to fail must be tagged with `[expect-fail]` and include an auditable evidence artifact per `evidence-and-timestamp-conventions`.
|
|
141
141
|
|
|
142
|
+
## Planner Adversarial Self-Review (Mandatory)
|
|
143
|
+
|
|
144
|
+
Before any plan handoff, `atomic-planner` MUST complete one explicit adversarial self-review pass over every fact, assumption, and line or file citation the plan relies on. The pass is required on initial authoring and on every revision-delta round. A revision round is not exempt because it changed only part of the plan: the citations the revision touched describe the tree as it stands after the revision, and no earlier pass observed that state.
|
|
145
|
+
|
|
146
|
+
Rules:
|
|
147
|
+
|
|
148
|
+
- **Re-derive every citation in this pass.** Any line, file, test, or assertion that the planner's own edit touched, added, or removed in the current authoring or revision pass MUST be re-derived directly against current repository state in that same pass. The prohibited source is a citation carried forward from an earlier round, including one the planner itself verified in a prior round: that earlier verification observed the tree before the intervening edits, so it is evidence about a superseded state rather than about the state the plan now asserts.
|
|
149
|
+
- **Re-check the sibling region.** The self-review MUST re-check the sibling lines, tests, and assertions that sit in the same file or region as any edited citation. The failure mechanism is sibling invalidation: a fix to one line can invalidate an assumption baked into a sibling line or test that a prior round's citation did not cover, so a pass that verifies only the edited line leaves the invalidated sibling unreported and it surfaces as a defect on a later round.
|
|
150
|
+
|
|
151
|
+
Declaration requirement. Every plan handoff MUST carry exactly one of these two signal lines, written in the directive-line form already used elsewhere in this contract:
|
|
152
|
+
|
|
153
|
+
- `SELF-REVIEW: RE-DERIVED THIS PASS` — the adversarial self-review pass completed in this pass. This signal MUST be followed by an enumeration of the citations re-derived in that pass, one entry per citation, each naming the file and the line, test, or identifier that was re-derived. A signal carrying no enumeration is not a completed declaration.
|
|
154
|
+
- `SELF-REVIEW: BLOCKED` — the pass could not be completed. This signal halts the handoff. It does not permit a self-approved plan: the planner reports the blocking reason and waits for the caller rather than proceeding to hand off an unverified plan.
|
|
155
|
+
|
|
142
156
|
## Preflight Validation (Planner ↔ Executor)
|
|
143
157
|
|
|
144
158
|
When validating or handing off plans for execution:
|
|
@@ -149,6 +163,20 @@ When validating or handing off plans for execution:
|
|
|
149
163
|
- If revisions are required, provide a precise plan delta and repeat validation until all clear.
|
|
150
164
|
- If the required planner ↔ executor handoff cannot be started or completed, stop and report blocked state; do not self-approve the plan.
|
|
151
165
|
|
|
166
|
+
Review depth and reporting rules:
|
|
167
|
+
|
|
168
|
+
- **Review the entire plan in one pass.** Under `DIRECTIVE: PREFLIGHT VALIDATION ONLY`, `atomic-executor` MUST continue checking every remaining phase, task, and prose region after finding an initial defect. Stopping at the first defect is prohibited: the unchecked remainder holds defects that the same pass could have reported, and each one that is left unreported becomes an additional round.
|
|
169
|
+
- **Enumerate every defect found.** `PREFLIGHT: REVISIONS REQUIRED` output MUST list every defect found in that pass, not only the first. The failure mechanism is round inflation: a single-defect report causes the next round to rediscover a defect the same pass could have reported, so the round count rises without the review having covered more of the plan.
|
|
170
|
+
- **Check the delta against its own rule.** Before returning either signal, `atomic-executor` MUST check its proposed fix or delta text against every rule the plan enforces, including that delta's own prose against the same violation class it is remediating. Worked example: the delta prose of a tonality-compliance fix must not itself contain the hyperbole or humor that `.claude/rules/tonality.md` prohibits, because a delta that violates the rule it is written to enforce reintroduces the finding it closes.
|
|
171
|
+
- **Two-round target.** The quality bar is a target of at most two preflight rounds per plan. Exhaustive first-pass review is the mechanism that holds the round count to that target: a pass that reports every defect it can find leaves at most a revision round and a confirming round, whereas a pass that reports one defect at a time cannot reach the target however correct each individual report is.
|
|
172
|
+
|
|
173
|
+
Convergence signal. Every preflight return, whether it carries `PREFLIGHT: ALL CLEAR` or `PREFLIGHT: REVISIONS REQUIRED`, MUST additionally carry exactly one of these two forward-looking lines:
|
|
174
|
+
|
|
175
|
+
- `CONVERGENCE: NO FURTHER ROUNDS EXPECTED` — the reviewer expects the plan to clear without a further round.
|
|
176
|
+
- `CONVERGENCE: FURTHER ROUNDS LIKELY` — the reviewer expects at least one further round, and states why.
|
|
177
|
+
|
|
178
|
+
The convergence line is a required signal rather than free prose. It is a second required line accompanying the preflight signal, not a third value of the signal set that the `Require one of the exact signals:` bullet above enumerates: that bullet's two-value set is unchanged, and every return carries one value from it together with one convergence line.
|
|
179
|
+
|
|
152
180
|
## Validator Gate (Mandatory)
|
|
153
181
|
|
|
154
182
|
Before a plan can be treated as approved:
|
|
@@ -3,11 +3,23 @@ name: cleanup-merged-worktrees
|
|
|
3
3
|
description: 'Detect, consolidate, and delete git worktrees/branches that are fully merged into main; use after an epic or feature''s PRs have merged and stale drm-copilot-wt-* branches/worktrees remain, driving the detect -> report -> consolidate -> pr-author handoff -> post-merge deletion workflow.'
|
|
4
4
|
allowed-tools:
|
|
5
5
|
- Read
|
|
6
|
+
- Grep
|
|
7
|
+
- Glob
|
|
8
|
+
- Agent
|
|
6
9
|
- "Bash(bash scripts/bash/cleanup-worktrees.sh *)"
|
|
7
10
|
- "Bash(git fetch *)"
|
|
8
11
|
- "Bash(git merge-base *)"
|
|
9
12
|
- "Bash(git push *)"
|
|
10
13
|
- "Bash(git rev-parse *)"
|
|
14
|
+
- "Bash(git status *)"
|
|
15
|
+
- "Bash(git log *)"
|
|
16
|
+
- "Bash(git show *)"
|
|
17
|
+
- "Bash(git diff *)"
|
|
18
|
+
- "Bash(git branch -r*)"
|
|
19
|
+
- "Bash(git worktree list*)"
|
|
20
|
+
- "Bash(gh issue view *)"
|
|
21
|
+
- mcp__drm-copilot__new_potential_bug_entry
|
|
22
|
+
- mcp__drm-copilot__potential_to_issue
|
|
11
23
|
---
|
|
12
24
|
|
|
13
25
|
# Cleanup Merged Worktrees
|
|
@@ -35,8 +47,12 @@ and is out of the script's scope.
|
|
|
35
47
|
which carry unmerged or unique work (`NOT_MERGED`, `HAS_UNIQUE_RESIDUALS`).
|
|
36
48
|
- When stranded documentation/agent-memory commits were appended to a worktree branch
|
|
37
49
|
after its feature content already merged and must be preserved before deletion.
|
|
50
|
+
- When a worktree is reported `BLOCKED-DIRTY`, or its branch is classified `NOT_MERGED`
|
|
51
|
+
or `HAS_UNIQUE_RESIDUALS`, and the uncommitted or unmerged content it holds must be
|
|
52
|
+
triaged into disposable versus must-preserve before the worktree can ever be deleted.
|
|
38
53
|
- Do not use this skill to manage remote branches; its scope is local branches and
|
|
39
|
-
local worktree registrations only
|
|
54
|
+
local worktree registrations only, except for the explicitly confirmed origin-branch
|
|
55
|
+
offer in the Dirty Worktree Triage Procedure's final step.
|
|
40
56
|
|
|
41
57
|
## Report Line Contract
|
|
42
58
|
|
|
@@ -99,7 +115,9 @@ The script emits pipe-delimited, `LC_ALL=C`-ordered records, one per line:
|
|
|
99
115
|
(without force; a dirty worktree is reported via `DIRTY|` lines and skipped), then
|
|
100
116
|
deletes branches with `git branch -D`. The now-merged `documentationandmemories`
|
|
101
117
|
branch and its worktree become `MERGED_CLEAN` instances and are cleaned up by the same
|
|
102
|
-
mechanics.
|
|
118
|
+
mechanics. Any worktree left standing afterward — reported `BLOCKED-DIRTY`, or whose
|
|
119
|
+
branch classified `NOT_MERGED` or `HAS_UNIQUE_RESIDUALS` — is not abandoned; it moves
|
|
120
|
+
to the Dirty Worktree Triage Procedure below.
|
|
103
121
|
|
|
104
122
|
## Nothing to Consolidate (Short Path)
|
|
105
123
|
|
|
@@ -108,6 +126,108 @@ with an empty cherry-pick-candidate list, skip steps 3-5 entirely: proceed direc
|
|
|
108
126
|
the report to `bash scripts/bash/cleanup-worktrees.sh --apply`. Cleanup completes in a
|
|
109
127
|
single session with no PR.
|
|
110
128
|
|
|
129
|
+
## Dirty Worktree Triage Procedure
|
|
130
|
+
|
|
131
|
+
**Trigger.** A worktree reported `ACTION|worktree-remove|<path>|BLOCKED-DIRTY` (with
|
|
132
|
+
accompanying `DIRTY|<path>|<status-porcelain-line>` records), or a branch classified
|
|
133
|
+
`NOT_MERGED` or `HAS_UNIQUE_RESIDUALS`, carries uncommitted or unmerged content the
|
|
134
|
+
script correctly refuses to discard. That refusal is correct and this procedure never
|
|
135
|
+
overrides it — a dirty worktree is never force-removed. This procedure is the systematic
|
|
136
|
+
follow-up: deciding, per worktree, whether that content is disposable or must be
|
|
137
|
+
preserved before the worktree can ever be deleted.
|
|
138
|
+
|
|
139
|
+
Steps 1-7 are read-only investigation. Run them per worktree, or fan out one
|
|
140
|
+
`Agent(general-purpose)` investigation per worktree (or small batch) concurrently per
|
|
141
|
+
step 8, each returning a `SAFE_TO_DELETE` / `PRESERVE` verdict with justification citing
|
|
142
|
+
specific files or commit SHAs, before step 9 acts on any finding.
|
|
143
|
+
|
|
144
|
+
1. **Re-verify current state before analyzing.** Worktrees can be actively in use by
|
|
145
|
+
another concurrent session. Re-run `git status --porcelain` in the worktree and
|
|
146
|
+
re-check the branch's merge status fresh — do not reuse the original scan's
|
|
147
|
+
snapshot. If the worktree's `.git`/index/HEAD mtimes show activity in the last few
|
|
148
|
+
minutes, treat it as possibly live and pause rather than analyze it as abandoned.
|
|
149
|
+
|
|
150
|
+
2. **Check committed-but-unmerged commits, not only the working tree.** Run
|
|
151
|
+
`git log main..<branch> --oneline`. Some worktrees carry real commits that never
|
|
152
|
+
merged, separate from uncommitted working-tree changes. Both need the classification
|
|
153
|
+
in step 5.
|
|
154
|
+
|
|
155
|
+
3. **Check for equivalent content already on `main`, by topic, not only by path.** For
|
|
156
|
+
every dirty, untracked, or unmerged file, check `git show main:<path>` at the same
|
|
157
|
+
path, and also grep broadly across the relevant shared namespace (for example
|
|
158
|
+
`.claude/agent-memory/**` for lesson files, `docs/features/**` for feature docs)
|
|
159
|
+
since the same fact is often re-recorded under a different filename on `main`.
|
|
160
|
+
|
|
161
|
+
4. **For feature-folder doc snapshots** (`issue.md`, `plan.md`, `spec.md`,
|
|
162
|
+
`research/*`), check whether the feature is fully closed on `main` — acceptance
|
|
163
|
+
criteria all checked, code-review/feature-audit/policy-audit artifacts present, an
|
|
164
|
+
evidence trail present. An earlier draft of an already-closed feature is almost
|
|
165
|
+
always fully superseded; diff it against the closed feature's final artifacts to
|
|
166
|
+
confirm rather than assume.
|
|
167
|
+
|
|
168
|
+
5. **Classify any content that is not obviously superseded** into exactly one of:
|
|
169
|
+
- `DEAD_ONE_OFF` — real, but tied to an already-executed, closed plan with no reuse
|
|
170
|
+
elsewhere (check whether the same pattern appears in shared `.claude/skills/**`
|
|
171
|
+
templates or in other feature plans). Low value; safe to discard even though it is
|
|
172
|
+
not technically duplicated.
|
|
173
|
+
- `ALREADY_SOLVED_ELSEWHERE` — the underlying problem it documents is fixed a
|
|
174
|
+
different way on `main` (check `main`'s current code/config/script, not only its
|
|
175
|
+
memory files — a memory file can describe a bug that no longer exists).
|
|
176
|
+
- `STALE_OR_CONTRADICTED` — `main`'s current version of the same lesson has since
|
|
177
|
+
been corrected to state something different or opposite. This is not merely
|
|
178
|
+
redundant; it is actively wrong, and discarding is the right call.
|
|
179
|
+
- `GENUINELY_NEW` / `STILL_RELEVANT` — not found anywhere else, or it corrects
|
|
180
|
+
something `main` currently gets wrong, or it documents unresolved scope on a
|
|
181
|
+
still-open issue (verify open/closed with `gh issue view <n>`; never assume). Must
|
|
182
|
+
be preserved before the worktree is deleted.
|
|
183
|
+
|
|
184
|
+
6. **Handle non-memory dirty content on its own terms.** Some worktrees carry stale
|
|
185
|
+
build artifacts (a modified `.csproj`/`packages.config`/`app.config` from a build run
|
|
186
|
+
in that worktree) rather than documentation. Diff a representative sample against
|
|
187
|
+
`main` (`git diff main -- <path>`) to characterize the change before deciding it is
|
|
188
|
+
disposable.
|
|
189
|
+
|
|
190
|
+
7. **Recognize orphaned non-worktree directories.** A path can still exist on disk
|
|
191
|
+
under a worktree-tracking folder after `git worktree remove` partially ran or
|
|
192
|
+
failed, with no `.git` file inside and no entry in `git worktree list`. These are no
|
|
193
|
+
longer worktrees — flag them for plain filesystem removal, not `git worktree
|
|
194
|
+
remove`, which will misfire or no-op on them. Filesystem removal of an orphaned
|
|
195
|
+
directory is a destructive action outside this skill's pre-approved tool surface; it
|
|
196
|
+
requires explicit user confirmation each time, the same as any other irreversible
|
|
197
|
+
delete.
|
|
198
|
+
|
|
199
|
+
8. **Parallelize the triage.** Steps 1-7 are pure read-only investigation. Fan out one
|
|
200
|
+
`Agent(general-purpose)` investigation per worktree (or a small batch) concurrently,
|
|
201
|
+
each following steps 1-7 and returning a structured `SAFE_TO_DELETE` / `PRESERVE`
|
|
202
|
+
verdict with justification. This scales far better than triaging serially.
|
|
203
|
+
|
|
204
|
+
9. **Route `PRESERVE` findings through the existing consolidation flow** (the
|
|
205
|
+
`documentationandmemories` branch/PR mechanism in steps 3-4 of the End-to-End
|
|
206
|
+
Workflow above) before that worktree's dirty content is discarded. If a finding
|
|
207
|
+
describes unresolved product scope rather than a process lesson, promote it to a
|
|
208
|
+
real follow-up issue instead of folding it into the docs/memory PR: file it with
|
|
209
|
+
`mcp__drm-copilot__new_potential_bug_entry` and promote with
|
|
210
|
+
`mcp__drm-copilot__potential_to_issue` per
|
|
211
|
+
`.claude/skills/feature-promotion-lifecycle/SKILL.md`. For a `SAFE_TO_DELETE`
|
|
212
|
+
verdict, discard the content as a distinct, individually confirmed manual action —
|
|
213
|
+
clear the dirty working tree, or delete a disposable `NOT_MERGED`/
|
|
214
|
+
`HAS_UNIQUE_RESIDUALS` branch directly. This is never automated: the script's
|
|
215
|
+
classification ladder and apply-mode allowlist are never changed to accept these
|
|
216
|
+
states, so a `--apply` run never deletes them on its own, before or after triage. If
|
|
217
|
+
discarding the working-tree content changes the branch's classification (for example
|
|
218
|
+
to content-neutral against `main`), a follow-up report/apply pass then picks it up
|
|
219
|
+
through the normal deterministic path.
|
|
220
|
+
|
|
221
|
+
10. **After local branch deletion, check origin too.** This skill is local-only by
|
|
222
|
+
design (see "When to Use This Skill"), which leaves stale branches on the remote for
|
|
223
|
+
anything already merged. After `--apply` finishes, diff the deleted-local-branch
|
|
224
|
+
list against `git branch -r` (post-prune) to find remote branches whose local
|
|
225
|
+
counterpart is gone, and offer to delete the remainder on origin. Because this
|
|
226
|
+
mutates shared, visible remote state, each deletion requires explicit user
|
|
227
|
+
confirmation — never delete an origin branch as an automatic consequence of local
|
|
228
|
+
cleanup, and never rely on this skill's general `Bash(git push *)` allowance to
|
|
229
|
+
perform it silently.
|
|
230
|
+
|
|
111
231
|
## Prohibited Shortcuts
|
|
112
232
|
|
|
113
233
|
- Never invoke `gh pr create` or `gh pr edit --body*` from this skill or the scripts. PR
|
|
@@ -116,10 +236,19 @@ single session with no PR.
|
|
|
116
236
|
- Never pass a force flag to `git worktree remove`. A dirty worktree blocks deletion and
|
|
117
237
|
is reported for manual handling; it is never force-removed.
|
|
118
238
|
- Never execute `git worktree prune`. Prunable registrations are report-only.
|
|
119
|
-
- Never act on `NOT_MERGED`, `HAS_UNIQUE_RESIDUALS`, or `PROTECTED_CURRENT` candidates
|
|
120
|
-
the
|
|
239
|
+
- Never act on `NOT_MERGED`, `HAS_UNIQUE_RESIDUALS`, or `PROTECTED_CURRENT` candidates
|
|
240
|
+
through the script or its apply-mode allowlist; `--apply` never mutates them, and the
|
|
241
|
+
caller's worktree and branch, and the main worktree, are never mutated under any
|
|
242
|
+
disposition. The Dirty Worktree Triage Procedure's `SAFE_TO_DELETE` verdict authorizes
|
|
243
|
+
only a distinct, individually confirmed manual action outside that automated path for
|
|
244
|
+
`NOT_MERGED`/`HAS_UNIQUE_RESIDUALS` — never a change to the classification ladder
|
|
245
|
+
itself, and never for `PROTECTED_CURRENT`.
|
|
121
246
|
- Never use commit-message text matching as a classification input, and never
|
|
122
247
|
auto-resolve cherry-pick conflicts.
|
|
248
|
+
- Never delete an origin branch, or run plain filesystem removal on an orphaned
|
|
249
|
+
worktree-tracking directory, without explicit per-item user confirmation — both are
|
|
250
|
+
outside this skill's pre-approved tool surface regardless of how the triage verdict
|
|
251
|
+
came out.
|
|
123
252
|
|
|
124
253
|
## Cross-References
|
|
125
254
|
|
|
@@ -130,3 +259,6 @@ single session with no PR.
|
|
|
130
259
|
- `.claude/rules/shell.md` — the bash toolchain (shfmt/shellcheck/bats/kcov), the
|
|
131
260
|
500-line cap, the no-temp-files test policy, and the `CLEANUP_WT_GIT_BIN` seam
|
|
132
261
|
convention.
|
|
262
|
+
- `.claude/skills/feature-promotion-lifecycle/SKILL.md` — the potential-entry-to-issue
|
|
263
|
+
promotion path used by the Dirty Worktree Triage Procedure's step 9 for `PRESERVE`
|
|
264
|
+
findings that describe unresolved product scope.
|
|
@@ -64,7 +64,9 @@ re-derivation is mandatory and is not an optimization to skip when the checkpoin
|
|
|
64
64
|
required parsed `config/blast-radius.json` mapping, which push-down publishes into the
|
|
65
65
|
destination workspace. `conflicts(a, b, config)` in `scripts/dev_tools/compute_blast_radius.py`
|
|
66
66
|
(defined in `scripts/dev_tools/_blast_radius_conflicts.py`) remains the repository authority and
|
|
67
|
-
the parity reference.
|
|
67
|
+
the parity reference. Read the verdict from the conflict key of the returned hashtable.
|
|
68
|
+
The hashtable itself is always truthy, so a bare boolean test on the result treats every pair as
|
|
69
|
+
conflicting. Map each conflicting pair onto an `(int, int)` conflict edge
|
|
68
70
|
of `items[].issue_num` values, normalized so `a < b`. Do not reimplement the relation and do not
|
|
69
71
|
compute edges over the unstarted subset only: an in-flight conflict is precisely what the
|
|
70
72
|
admission decision turns on.
|
|
@@ -387,15 +387,21 @@ checkout, never from inside a child worktree — issues `git worktree remove <wo
|
|
|
387
387
|
success it records `merge_status: worktree_removed` and `worktree_removed_at`, then regenerates
|
|
388
388
|
`docs/features/parallel/<slug>/parallel-status.md`.
|
|
389
389
|
|
|
390
|
-
Mechanical gating of this command for parallel worktrees is
|
|
391
|
-
`.claude/hooks/enforce-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
`
|
|
397
|
-
|
|
398
|
-
|
|
390
|
+
Mechanical gating of this command for parallel worktrees is delivered. Both halves have landed.
|
|
391
|
+
`.claude/hooks/enforce-parallel-worktree-removal-gate.ps1` exists and is registered in
|
|
392
|
+
`.claude/settings.json` on the `Bash` matcher; it authorizes a removal from the
|
|
393
|
+
parallel-orchestrator checkpoint and owns the block reason
|
|
394
|
+
`PARALLEL_WORKTREE_REMOVAL_BLOCKED`. `.claude/hooks/enforce-epic-worktree-removal-gate.ps1` is a
|
|
395
|
+
project-wide `PreToolUse` Bash-matcher hook registered alongside it whose block reason is
|
|
396
|
+
`EPIC_WORKTREE_REMOVAL_BLOCKED`; it now carries a second, parallel allow-branch, so it authorizes a
|
|
397
|
+
removal either from a matching epic checkpoint `features[]` record or from a parallel-orchestrator
|
|
398
|
+
checkpoint whose `route_id` is `parallel` and whose matching `items[].worktree_path` record has
|
|
399
|
+
`merge_status` in `{merged, worktree_removed}`. An unreadable checkpoint, an absent record, or a
|
|
400
|
+
non-authorizing `merge_status` still denies on both branches. The coordination matters because
|
|
401
|
+
`PreToolUse` denials are conjunctive: a new allow-hook alone could not have overridden the epic
|
|
402
|
+
gate's independent deny, which is why the epic gate itself had to gain the parallel branch.
|
|
403
|
+
The parallel-orchestrator-surface feature (F7) shipped no hook file and made no
|
|
404
|
+
`.claude/settings.json` change of its own.
|
|
399
405
|
|
|
400
406
|
## Documentation Maintenance Boundaries
|
|
401
407
|
|
|
@@ -736,6 +742,9 @@ against `items[].worktree_path`. Removal is allowed only when that item's `merge
|
|
|
736
742
|
or `worktree_removed`; anything else — including an unreadable checkpoint or no matching record —
|
|
737
743
|
denies with a reason prefixed `PARALLEL_WORKTREE_REMOVAL_BLOCKED`. Commands that are not
|
|
738
744
|
`git worktree remove` always allow. This is the mechanical counterpart to `## Worktree Cleanup`.
|
|
745
|
+
`.claude/hooks/enforce-epic-worktree-removal-gate.ps1` fires on the same command and now carries a
|
|
746
|
+
matching parallel allow-branch keyed on the same checkpoint, so both gates must allow for a removal
|
|
747
|
+
to proceed.
|
|
739
748
|
|
|
740
749
|
**Invocation-origin extension.** `.claude/hooks/enforce-epic-invocation-origin.ps1` was extended
|
|
741
750
|
additively so `$script:GatedSubagentTypes` lists `epic-planner`, `epic-orchestrator`,
|
|
@@ -214,7 +214,9 @@ as-is and never reimplemented here:
|
|
|
214
214
|
`compute_blast_radius.py`. The signature takes three arguments; the third is the parsed
|
|
215
215
|
`config/blast-radius.json`. Reasons come from the fixed vocabulary
|
|
216
216
|
`{path_overlap, module_overlap, shared_surface_overlap, contract_dependency}`, and the relation
|
|
217
|
-
fails closed.
|
|
217
|
+
fails closed. Read the verdict from the conflict field of the returned ConflictResult.
|
|
218
|
+
The result's boolean projection now agrees with that field, so `if conflicts(a, b, config):`
|
|
219
|
+
yields the verdict rather than the unconditional truth a bare object test gave before issue #576.
|
|
218
220
|
|
|
219
221
|
**The F1a corrections (issue #452, merged PR #453) are load-bearing.** Derivation now reaches
|
|
220
222
|
separator-free repository-root shared surfaces from plan and spec text, admitting such a token only
|
|
@@ -303,6 +305,11 @@ The library returns the partition; the planner supplies the record fields.
|
|
|
303
305
|
item is `prepared` and radius-validated. Derive the conflict edge set by applying
|
|
304
306
|
`Test-BlastRadiusConflict` to every unordered pair of `declared` radii, then pass the pairs as
|
|
305
307
|
`--edges "<a>:<b> ..."` and the item keys as `--keys "<k1> <k2> ..."`.
|
|
308
|
+
Read the verdict from the conflict key of the returned hashtable.
|
|
309
|
+
The hashtable itself is always truthy, so a bare boolean test on the result treats every pair as
|
|
310
|
+
conflicting and serializes the whole run. This is the sibling hazard to the `@(...)` warning
|
|
311
|
+
above for `Test-BlastRadius`: that function writes an `IList`-shaped pipeline result whose
|
|
312
|
+
emptiness is falsy, while this one returns a hashtable whose emptiness is not expressible at all.
|
|
306
313
|
2. Immediately after the conflict-edge set is derived and before anything consumes it, run the
|
|
307
314
|
lane-assertion diagnostic:
|
|
308
315
|
`poetry run python -m scripts.dev_tools.parallel_lane_assertion --manifest docs/features/parallel/<slug>/parallel.md --edges "<a>:<b> ..."`
|
|
@@ -28,3 +28,25 @@ If the artifacts are missing or stale relative to the current branch state, re-g
|
|
|
28
28
|
- Do not infer the refresh base from the repository default branch unless merge-base resolution fails for all candidates.
|
|
29
29
|
- Treat an already-fresh artifact pair as authoritative; do not refresh solely because no explicit `PRBaseBranch` input was provided.
|
|
30
30
|
|
|
31
|
+
### Freshness Cross-Check
|
|
32
|
+
|
|
33
|
+
Both artifacts open with a `Context generated` section carrying the generation timestamp and a
|
|
34
|
+
`Head SHA:` line. Decide freshness from those two values in two steps, and from nothing else.
|
|
35
|
+
|
|
36
|
+
1. **Pair identity.** The generated-context timestamp must be byte-identical in the summary and in
|
|
37
|
+
the appendix. A mismatch proves the two files came from different invocations — a summary
|
|
38
|
+
refreshed while a stale appendix persists, or the reverse — so the pair does not describe one
|
|
39
|
+
run and must be regenerated.
|
|
40
|
+
2. **Head binding.** The head SHA recorded in both files must equal the current head of the branch
|
|
41
|
+
under review. A mismatch proves the pair predates the current head, so it describes a different
|
|
42
|
+
diff than the one being reviewed and must be regenerated.
|
|
43
|
+
|
|
44
|
+
File existence and file modification time are not freshness signals. A file left at the expected
|
|
45
|
+
path by a previous invocation satisfies an existence check, and a stale file that was copied or
|
|
46
|
+
touched satisfies a modification-time check. Both operands of the cross-check above are read from
|
|
47
|
+
the artifacts themselves and from git, so the check is deterministic and does not depend on a wall
|
|
48
|
+
clock.
|
|
49
|
+
|
|
50
|
+
When the head SHA renders the `(unknown)` token, the collected context carried no head SHA. Head
|
|
51
|
+
binding cannot be established in that case, so treat the pair as unverified and regenerate it.
|
|
52
|
+
|
package/resources/claude-customizations/.claude/skills/remediation-handoff-atomic-planner/SKILL.md
CHANGED
|
@@ -81,6 +81,10 @@ Timestamp rule:
|
|
|
81
81
|
|
|
82
82
|
A cycle with fewer than five artifacts is malformed. A cycle that uses the same timestamp value for both its `remediation/<ts>/` and `audit/<ts>/` folders is malformed unless entry and exit genuinely ran within the same minute — the two folders remain distinct either way, since one is named `remediation/` and the other `audit/`.
|
|
83
83
|
|
|
84
|
+
### Cycle-Document Sweep Scope
|
|
85
|
+
|
|
86
|
+
A comprehensive or final sweep in a remediation cycle covers that cycle's own plan and audit documents — `remediation-plan.md`, `code-review.md`, `feature-audit.md`, and `policy-audit.md` — in addition to production and test code. The failure mechanism a code-only sweep leaves open is self-referential rule violation: a policy-compliance fix whose own descriptive text violates the policy it enforces is written into one of those four documents rather than into code, so a sweep scoped to code only reports no finding and the violation ships with the cycle.
|
|
87
|
+
|
|
84
88
|
## Plan Shape
|
|
85
89
|
|
|
86
90
|
`remediation/<entry-ts>/remediation-plan.md` MUST conform to `.claude/skills/atomic-plan-contract/SKILL.md`. In particular:
|
|
@@ -102,6 +106,12 @@ After the plan is authored, `atomic-executor` runs preflight under the directive
|
|
|
102
106
|
|
|
103
107
|
The orchestrator records the preflight outcome in `remediation_loop.cycles[current_cycle].preflight` with `iterations` (counter) and `final_status` (`clear|changes_requested|pending`).
|
|
104
108
|
|
|
109
|
+
The exhaustive-pass, defect-enumeration, and delta-self-check rules that govern how `atomic-executor` conducts preflight are defined in the `## Preflight Validation (Planner ↔ Executor)` section of `.claude/skills/atomic-plan-contract/SKILL.md` and are not restated here.
|
|
110
|
+
|
|
111
|
+
Alongside `iterations` and `final_status`, the orchestrator also records in `remediation_loop.cycles[current_cycle].preflight` the convergence line `atomic-executor` returned on that round, which is one of `CONVERGENCE: NO FURTHER ROUNDS EXPECTED` or `CONVERGENCE: FURTHER ROUNDS LIKELY`. This convergence field extends the field set already recorded at `remediation_loop.cycles[current_cycle].preflight` rather than replacing it: `iterations` and `final_status` continue to be recorded exactly as stated above, and the convergence field is written in addition to them.
|
|
112
|
+
|
|
113
|
+
Iteration ceiling. When a cycle's `iterations` would exceed 2, the orchestrator records `final_status: "blocked_preflight_iteration_limit"`, halts the preflight sub-loop, and escalates to the caller, rather than continuing the sub-loop indefinitely. `blocked_preflight_iteration_limit` is a fourth `final_status` value extending the `clear|changes_requested|pending` enumeration stated above. This ceiling bounds the repeat-until-clear behavior stated above it: the sub-loop still repeats until `PREFLIGHT: ALL CLEAR` is returned, and the ceiling supplies the terminating condition for the case where that signal is not reached within two iterations.
|
|
114
|
+
|
|
105
115
|
## Execution and Reaudit
|
|
106
116
|
|
|
107
117
|
When preflight is clear, `atomic-executor` executes the plan task-by-task. The executor invokes workers (`python-typed-engineer`, `typescript-engineer`, `csharp-typed-engineer`, `powershell-typed-engineer`) internally as needed. The orchestrator does not call workers.
|
package/resources/codex-and-agents-customizations/.agents/skills/pr-context-artifacts/SKILL.md
CHANGED
|
@@ -27,3 +27,25 @@ If the artifacts are missing or stale relative to the current branch state, re-g
|
|
|
27
27
|
- When `PRBaseBranch` is missing or ambiguous, resolve it first with `pr-base-branch-merge-base` before running the collector.
|
|
28
28
|
- Do not infer the refresh base from the repository default branch unless merge-base resolution fails for all candidates.
|
|
29
29
|
- Treat an already-fresh artifact pair as authoritative; do not refresh solely because no explicit `PRBaseBranch` input was provided.
|
|
30
|
+
|
|
31
|
+
### Freshness Cross-Check
|
|
32
|
+
|
|
33
|
+
Both artifacts open with a `Context generated` section carrying the generation timestamp and a
|
|
34
|
+
`Head SHA:` line. Decide freshness from those two values in two steps, and from nothing else.
|
|
35
|
+
|
|
36
|
+
1. **Pair identity.** The generated-context timestamp must be byte-identical in the summary and in
|
|
37
|
+
the appendix. A mismatch proves the two files came from different invocations — a summary
|
|
38
|
+
refreshed while a stale appendix persists, or the reverse — so the pair does not describe one
|
|
39
|
+
run and must be regenerated.
|
|
40
|
+
2. **Head binding.** The head SHA recorded in both files must equal the current head of the branch
|
|
41
|
+
under review. A mismatch proves the pair predates the current head, so it describes a different
|
|
42
|
+
diff than the one being reviewed and must be regenerated.
|
|
43
|
+
|
|
44
|
+
File existence and file modification time are not freshness signals. A file left at the expected
|
|
45
|
+
path by a previous invocation satisfies an existence check, and a stale file that was copied or
|
|
46
|
+
touched satisfies a modification-time check. Both operands of the cross-check above are read from
|
|
47
|
+
the artifacts themselves and from git, so the check is deterministic and does not depend on a wall
|
|
48
|
+
clock.
|
|
49
|
+
|
|
50
|
+
When the head SHA renders the `(unknown)` token, the collected context carried no head SHA. Head
|
|
51
|
+
binding cannot be established in that case, so treat the pair as unverified and regenerate it.
|
|
@@ -27,3 +27,25 @@ If the artifacts are missing or stale relative to the current branch state, re-g
|
|
|
27
27
|
- When `PRBaseBranch` is missing or ambiguous, resolve it first with `pr-base-branch-merge-base` before running the collector.
|
|
28
28
|
- Do not infer the refresh base from the repository default branch unless merge-base resolution fails for all candidates.
|
|
29
29
|
- Treat an already-fresh artifact pair as authoritative; do not refresh solely because no explicit `PRBaseBranch` input was provided.
|
|
30
|
+
|
|
31
|
+
### Freshness Cross-Check
|
|
32
|
+
|
|
33
|
+
Both artifacts open with a `Context generated` section carrying the generation timestamp and a
|
|
34
|
+
`Head SHA:` line. Decide freshness from those two values in two steps, and from nothing else.
|
|
35
|
+
|
|
36
|
+
1. **Pair identity.** The generated-context timestamp must be byte-identical in the summary and in
|
|
37
|
+
the appendix. A mismatch proves the two files came from different invocations — a summary
|
|
38
|
+
refreshed while a stale appendix persists, or the reverse — so the pair does not describe one
|
|
39
|
+
run and must be regenerated.
|
|
40
|
+
2. **Head binding.** The head SHA recorded in both files must equal the current head of the branch
|
|
41
|
+
under review. A mismatch proves the pair predates the current head, so it describes a different
|
|
42
|
+
diff than the one being reviewed and must be regenerated.
|
|
43
|
+
|
|
44
|
+
File existence and file modification time are not freshness signals. A file left at the expected
|
|
45
|
+
path by a previous invocation satisfies an existence check, and a stale file that was copied or
|
|
46
|
+
touched satisfies a modification-time check. Both operands of the cross-check above are read from
|
|
47
|
+
the artifacts themselves and from git, so the check is deterministic and does not depend on a wall
|
|
48
|
+
clock.
|
|
49
|
+
|
|
50
|
+
When the head SHA renders the `(unknown)` token, the collected context carried no head SHA. Head
|
|
51
|
+
binding cannot be established in that case, so treat the pair as unverified and regenerate it.
|