@danmoisan/drm-copilot-mcp 1.1.12 → 1.1.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (23) hide show
  1. package/out/mcp-server.js +30 -78
  2. package/package.json +2 -2
  3. package/resources/claude-customizations/.claude/agents/parallel-planner.md +18 -1
  4. package/resources/claude-customizations/.claude/hooks/enforce-completion-consistency.ps1 +16 -8
  5. package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate-helpers.ps1 +27 -27
  6. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadius.psm1 +43 -6
  7. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusScheduling.psm1 +486 -0
  8. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusValidation.psm1 +5 -2
  9. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusWriteIntent.psm1 +446 -0
  10. package/resources/claude-customizations/.claude/rules/parallel-orchestration.md +150 -2
  11. package/resources/claude-customizations/.claude/rules/plan-acceptance-gates.md +5 -5
  12. package/resources/claude-customizations/.claude/skills/epic-plan/SKILL.md +11 -0
  13. package/resources/claude-customizations/.claude/skills/parallel-add/SKILL.md +32 -17
  14. package/resources/claude-customizations/.claude/skills/parallel-plan/SKILL.md +39 -10
  15. package/resources/claude-customizations/config/blast-radius.json +14 -1
  16. package/resources/claude-customizations/pack-manifests/core.json +2 -0
  17. package/resources/codex-and-agents-customizations/.codex/config.toml +1 -1
  18. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate-epic-resolution.ps1 +493 -0
  19. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate-epic-scope.ps1 +189 -0
  20. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate-helpers.ps1 +27 -27
  21. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate.ps1 +14 -27
  22. package/resources/codex-and-agents-customizations/pack-manifests/core.json +2 -0
  23. package/resources/powershell/PoshQC/settings/pester.runsettings.psd1 +11 -0
package/out/mcp-server.js CHANGED
@@ -19728,7 +19728,10 @@ var CARRIED_KEYS = [
19728
19728
  "shared_surface_globs",
19729
19729
  "over_breadth_fraction",
19730
19730
  "mandate_reads",
19731
- "mergeable_paths"
19731
+ "mergeable_paths",
19732
+ "conflict_tolerance",
19733
+ "write_intent_extraction",
19734
+ "path_roots"
19732
19735
  ];
19733
19736
  var BlastRadiusDeriveError = class extends Error {
19734
19737
  /** Destination-relative path of the document that failed to parse. */
@@ -19835,6 +19838,9 @@ function deriveDestinationModuleMap(observations, sourceDocumentText) {
19835
19838
  shared_surface_globs: source[CARRIED_KEYS[2]],
19836
19839
  mandate_reads: source[CARRIED_KEYS[4]],
19837
19840
  mergeable_paths: source[CARRIED_KEYS[5]],
19841
+ conflict_tolerance: source[CARRIED_KEYS[6]],
19842
+ write_intent_extraction: source[CARRIED_KEYS[7]],
19843
+ path_roots: source[CARRIED_KEYS[8]],
19838
19844
  modules,
19839
19845
  over_breadth_fraction: source[CARRIED_KEYS[3]]
19840
19846
  };
@@ -32067,6 +32073,15 @@ function formatList(values, emptyText) {
32067
32073
  }
32068
32074
  return valuesList.map((item) => `- ${item}`).join("\n");
32069
32075
  }
32076
+ function compareCodePoint(left, right) {
32077
+ if (left < right) {
32078
+ return -1;
32079
+ }
32080
+ if (left > right) {
32081
+ return 1;
32082
+ }
32083
+ return 0;
32084
+ }
32070
32085
 
32071
32086
  // ../../extensions/drm-copilot/src/lib/pr-context/git-client.ts
32072
32087
  var GitClient = class {
@@ -32261,15 +32276,6 @@ function extractClosingNumbers(raw) {
32261
32276
  function sortedSet(values) {
32262
32277
  return [...new Set(values)].sort(compareCodePoint);
32263
32278
  }
32264
- function compareCodePoint(left, right) {
32265
- if (left < right) {
32266
- return -1;
32267
- }
32268
- if (left > right) {
32269
- return 1;
32270
- }
32271
- return 0;
32272
- }
32273
32279
  function authorLogin(record3, key) {
32274
32280
  const raw = record3[key];
32275
32281
  if (isRecord5(raw)) {
@@ -32836,9 +32842,9 @@ function selectPendingPrimary(options) {
32836
32842
  }
32837
32843
  function buildCloseCandidatesSection(params) {
32838
32844
  const { verified, authorAsserted, referenced, verifiedReason, authorReason } = params;
32839
- const authorAutoClose = [...new Set(authorAsserted)].sort(compareCodePoint2);
32845
+ const authorAutoClose = [...new Set(authorAsserted)].sort(compareCodePoint);
32840
32846
  const closing = /* @__PURE__ */ new Set([...verified, ...authorAsserted]);
32841
- const referencedOnly = [...new Set(referenced)].filter((ref) => !closing.has(ref)).sort(compareCodePoint2);
32847
+ const referencedOnly = [...new Set(referenced)].filter((ref) => !closing.has(ref)).sort(compareCodePoint);
32842
32848
  return [
32843
32849
  section("Close candidates"),
32844
32850
  "Auto-close issues (verified from GitHub PR metadata):",
@@ -32885,15 +32891,6 @@ ${AUTOCLOSE_UNVERIFIED_ANNOTATION}`;
32885
32891
  "\n"
32886
32892
  );
32887
32893
  }
32888
- function compareCodePoint2(left, right) {
32889
- if (left < right) {
32890
- return -1;
32891
- }
32892
- if (left > right) {
32893
- return 1;
32894
- }
32895
- return 0;
32896
- }
32897
32894
 
32898
32895
  // ../../extensions/drm-copilot/src/lib/pr-context/render-pr-helpers.ts
32899
32896
  var DEFAULT_BASE_CANDIDATES = [
@@ -32961,7 +32958,7 @@ function extensionSummary(files) {
32961
32958
  const ext = suffix2 ? suffix2 : "(noext)";
32962
32959
  counts.set(ext, (counts.get(ext) ?? 0) + 1);
32963
32960
  }
32964
- const lines = [...counts.keys()].sort(compareCodePoint3).map((key) => `${padCount(counts.get(key))} ${key}`);
32961
+ const lines = [...counts.keys()].sort(compareCodePoint).map((key) => `${padCount(counts.get(key))} ${key}`);
32965
32962
  return lines.join("\n");
32966
32963
  }
32967
32964
  function extractIssueReferences(text2) {
@@ -32988,7 +32985,7 @@ function extractMergePrNumbers(subjects) {
32988
32985
  numbers.add(`#${match[1]}`);
32989
32986
  }
32990
32987
  }
32991
- return [...numbers].sort(compareCodePoint3);
32988
+ return [...numbers].sort(compareCodePoint);
32992
32989
  }
32993
32990
  function summarizeConventionalCommits(subjects) {
32994
32991
  const counts = /* @__PURE__ */ new Map();
@@ -33052,15 +33049,6 @@ function padName(name) {
33052
33049
  function isDigits(value) {
33053
33050
  return value.length > 0 && /^\d+$/u.test(value);
33054
33051
  }
33055
- function compareCodePoint3(left, right) {
33056
- if (left < right) {
33057
- return -1;
33058
- }
33059
- if (left > right) {
33060
- return 1;
33061
- }
33062
- return 0;
33063
- }
33064
33052
  function splitLines4(value) {
33065
33053
  if (value === "") {
33066
33054
  return [];
@@ -33287,16 +33275,7 @@ function errorMessage(exc) {
33287
33275
  return exc instanceof Error ? exc.message : String(exc);
33288
33276
  }
33289
33277
  function sortedSet2(values) {
33290
- return [...new Set(values)].sort(compareCodePoint4);
33291
- }
33292
- function compareCodePoint4(left, right) {
33293
- if (left < right) {
33294
- return -1;
33295
- }
33296
- if (left > right) {
33297
- return 1;
33298
- }
33299
- return 0;
33278
+ return [...new Set(values)].sort(compareCodePoint);
33300
33279
  }
33301
33280
  function splitLines5(value) {
33302
33281
  if (value === "") {
@@ -33384,7 +33363,7 @@ function readText(fs12, path16) {
33384
33363
  return fs12.exists(path16) ? fs12.readTextFile(path16) : "";
33385
33364
  }
33386
33365
  function latestGlobPath(fs12, directory, pattern) {
33387
- const matches = [...fs12.glob(directory, pattern)].sort(compareCodePoint5);
33366
+ const matches = [...fs12.glob(directory, pattern)].sort(compareCodePoint);
33388
33367
  return matches.length > 0 ? matches[matches.length - 1] : null;
33389
33368
  }
33390
33369
  function verificationText(planText) {
@@ -33428,7 +33407,7 @@ function parseReadinessValue(text2) {
33428
33407
  }
33429
33408
  function resolveReadinessSignal(fs12, featureDir) {
33430
33409
  const auditFiles = [...fs12.glob(featureDir, "feature-audit.*.md")].sort(
33431
- compareCodePoint5
33410
+ compareCodePoint
33432
33411
  );
33433
33412
  for (let index = auditFiles.length - 1; index >= 0; index -= 1) {
33434
33413
  const auditPath = auditFiles[index];
@@ -33447,15 +33426,6 @@ function relativeToPosix6(root, path16) {
33447
33426
  }
33448
33427
  return normalized.replace(/^\/+/u, "");
33449
33428
  }
33450
- function compareCodePoint5(left, right) {
33451
- if (left < right) {
33452
- return -1;
33453
- }
33454
- if (left > right) {
33455
- return 1;
33456
- }
33457
- return 0;
33458
- }
33459
33429
  function escapeRegExp4(value) {
33460
33430
  return value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
33461
33431
  }
@@ -33485,7 +33455,7 @@ function discoverCanonicalEvidenceFiles(fs12, root, feature) {
33485
33455
  }
33486
33456
  }
33487
33457
  }
33488
- return [...discovered].sort(compareCodePoint6);
33458
+ return [...discovered].sort(compareCodePoint);
33489
33459
  }
33490
33460
  function parseVerificationEvidenceMarkdown(params) {
33491
33461
  const { feature, sourceFile, markdown } = params;
@@ -33581,15 +33551,6 @@ function relativeToPosix7(root, absolute) {
33581
33551
  }
33582
33552
  return normalized.replace(/^\/+/u, "");
33583
33553
  }
33584
- function compareCodePoint6(left, right) {
33585
- if (left < right) {
33586
- return -1;
33587
- }
33588
- if (left > right) {
33589
- return 1;
33590
- }
33591
- return 0;
33592
- }
33593
33554
  function splitLines6(value) {
33594
33555
  if (value === "") {
33595
33556
  return [];
@@ -33629,7 +33590,7 @@ function gatherFeatureExcerpts2(fs12, root, changedFiles) {
33629
33590
  const excerpts = [];
33630
33591
  const baseDir = `${normalizedRoot}/docs/features/active`;
33631
33592
  const promotedDir = `${normalizedRoot}/docs/features/potential/promoted`;
33632
- for (const feature of [...features].sort(compareCodePoint5)) {
33593
+ for (const feature of [...features].sort(compareCodePoint)) {
33633
33594
  const featureDir = resolveFeatureDir2(fs12, baseDir, feature);
33634
33595
  const promotedFeatureDir = resolveFeatureDir2(fs12, promotedDir, feature);
33635
33596
  if (featureDir === null && promotedFeatureDir === null) {
@@ -33783,7 +33744,7 @@ function buildContextFiles(params) {
33783
33744
  feature
33784
33745
  );
33785
33746
  return [.../* @__PURE__ */ new Set([...contextFiles, ...evidenceContextFiles])].sort(
33786
- compareCodePoint5
33747
+ compareCodePoint
33787
33748
  );
33788
33749
  }
33789
33750
 
@@ -34219,9 +34180,9 @@ function collectPrContext(options) {
34219
34180
  referencedPrsSet,
34220
34181
  invalidRefsSet
34221
34182
  });
34222
- const referencedIssues = [...referencedIssuesSet].sort(compareCodePoint7);
34223
- const referencedPrs = [...referencedPrsSet].sort(compareCodePoint7);
34224
- const invalidRefs = [...invalidRefsSet].sort(compareCodePoint7);
34183
+ const referencedIssues = [...referencedIssuesSet].sort(compareCodePoint);
34184
+ const referencedPrs = [...referencedPrsSet].sort(compareCodePoint);
34185
+ const invalidRefs = [...invalidRefsSet].sort(compareCodePoint);
34225
34186
  const authorAsserted = [];
34226
34187
  const authorReason = "None (author has not asserted autoclose issues)";
34227
34188
  const verified = ghAvailable ? contextResult.verifiedClosing : [];
@@ -34368,16 +34329,7 @@ function errorMessage2(exc) {
34368
34329
  return exc instanceof Error ? exc.message : String(exc);
34369
34330
  }
34370
34331
  function sortedSet3(values) {
34371
- return [...new Set(values)].sort(compareCodePoint7);
34372
- }
34373
- function compareCodePoint7(left, right) {
34374
- if (left < right) {
34375
- return -1;
34376
- }
34377
- if (left > right) {
34378
- return 1;
34379
- }
34380
- return 0;
34332
+ return [...new Set(values)].sort(compareCodePoint);
34381
34333
  }
34382
34334
 
34383
34335
  // ../../extensions/drm-copilot/src/lib/pr-context/collector-output.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danmoisan/drm-copilot-mcp",
3
- "version": "1.1.12",
3
+ "version": "1.1.14",
4
4
  "description": "Stdio MCP server exposing drm-copilot repo-automation tools.",
5
5
  "license": "MIT",
6
6
  "type": "commonjs",
@@ -40,7 +40,7 @@
40
40
  "overrides": {
41
41
  "fast-uri": "^3.1.4",
42
42
  "hono": "^4.12.27",
43
- "ip-address": "^10.2.0",
43
+ "ip-address": "^10.7.2",
44
44
  "qs": "^6.15.2",
45
45
  "@hono/node-server": "^2.0.5"
46
46
  },
@@ -110,7 +110,8 @@ your own initiative; withdrawal is a caller decision made through F6's remove op
110
110
  Update `artifacts/orchestration/parallel-planner-state.json` after every completed step with:
111
111
  `objective`, `parallel_slug`, `parallel_manifest_path`, `mode`, `max_concurrency`,
112
112
  `plan_home_branch`, `items[]`, `cohorts[]`, `conflict_edges[]`, `recolor_generation`,
113
- `kickoff_prompt_path`, `completed_steps`, `next_step`, and `last_updated`.
113
+ `kickoff_prompt_path`, `completed_steps`, `next_step`, and `last_updated`. Also record the
114
+ `tolerated_overlaps` list returned by Get-BlastRadiusConflictEdge (tolerated-not-validated).
114
115
 
115
116
  Each `items[]` entry carries `issue_num`, `feature_folder`, `kind`, `state`, `complexity_band`,
116
117
  `preparation_status`, `research_path`, `plan_path`, `preflight_status`, `branch_name`,
@@ -163,6 +164,22 @@ The facade exports `Get-PlanPaths`, `Get-BlastRadius`, `Get-BlastRadiusFromObser
163
164
  `Test-BlastRadiusConflict` reads that table's optional `mergeable_paths` list and contributes no
164
165
  `path_overlap` edge for a path matching it, while the path stays in the declared radius.
165
166
 
167
+ The facade also exports the scheduling entry point Get-BlastRadiusConflictEdge (with its per-pair
168
+ helper `Get-BlastRadiusPairDecision`), the PowerShell port of `schedule_conflict_edges` in
169
+ `scripts/dev_tools/_blast_radius_scheduling.py`. Build the conflict edges with one call,
170
+ `Get-BlastRadiusConflictEdge -Item <records carrying key, radius, band> -Config <parsed truth table> -Relation ${function:Test-BlastRadiusConflict}`,
171
+ rather than applying `Test-BlastRadiusConflict` to each pair by hand. The -Relation argument is
172
+ required: the scheduling module does not resolve the relation itself, and an omitted relation fails
173
+ fast. It applies the detection
174
+ relation to every unordered pair and then the integration-cost edge rule of
175
+ `.claude/rules/parallel-orchestration.md` under the table's configured `conflict_tolerance`, and
176
+ returns `edges` and `tolerated_overlaps`. Record the edges in `conflict_edges[]` (each entry may
177
+ carry the tolerated extra fields `hard`, `cost`, and `benefit`) and record every returned tolerated
178
+ overlap in a `tolerated_overlaps` list on the planner checkpoint; tolerated overlaps are required
179
+ checkpoint content, never discarded. A tolerated pair runs in the same or adjacent cohorts without a
180
+ barrier, and the later-merging item merges `origin/main` and re-passes CI. The configured tolerance
181
+ is the only sanctioned relaxation: never hand-narrow a radius to suppress an edge.
182
+
166
183
  **Cohort seeding and concurrency batching — bash entry points.** The bash library is granted as
167
184
  four entry-point-specific allowlist entries — `"Bash(bash .claude/lib/bash/compute-cohorts.sh*)"`,
168
185
  `"Bash(bash .claude/lib/bash/compute-concurrency-batches.sh*)"`,
@@ -29,9 +29,13 @@
29
29
  completion is not asserted, the write is allowed via
30
30
  hookSpecificOutput.permissionDecision='allow' (backward compatibility).
31
31
 
32
- Edit tool calls supply only old_string/new_string (a partial patch) and
33
- cannot be reliably validated without the full target file content, so they
34
- are allowed by this hook, matching enforce-checkpoint-monotonic.ps1. For
32
+ Edit tool calls supply only old_string/new_string (a partial patch). An Edit
33
+ call is validated by reading the checkpoint at the Edit's
34
+ targeted file_path through the injectable CheckpointReader seam and
35
+ applying the old_string -> new_string replacement in memory; the resulting
36
+ content is then evaluated like a Write. When the patch cannot be resolved
37
+ (the targeted file is missing or empty, the Edit has no old_string, or the
38
+ old_string is absent from the targeted content), the call is allowed. For
35
39
  Write calls whose content is not valid JSON, the hook allows the operation
36
40
  and defers to downstream tools to surface the error.
37
41
 
@@ -266,10 +270,11 @@ function Resolve-EditedCheckpointContent {
266
270
  when the patch cannot be applied against the on-disk checkpoint.
267
271
  .DESCRIPTION
268
272
  Implements the read-then-validate Edit path. When the tool input carries
269
- an old_string (an Edit patch), the on-disk checkpoint is read through the
273
+ an old_string (an Edit patch), the checkpoint at the caller-supplied
274
+ CheckpointPath (the Edit's targeted file_path) is read through the
270
275
  injectable CheckpointReader seam and the old_string -> new_string
271
276
  replacement is applied in memory (no on-disk mutation). Returns $null
272
- when there is no old_string, the on-disk file does not exist, or the
277
+ when there is no old_string, the targeted file does not exist or is empty, or the
273
278
  old_string is not present in the on-disk content, signalling the caller
274
279
  to allow (defer).
275
280
  #>
@@ -281,7 +286,10 @@ function Resolve-EditedCheckpointContent {
281
286
  $ToolInput,
282
287
 
283
288
  [Parameter(Mandatory)]
284
- [scriptblock] $CheckpointReader
289
+ [scriptblock] $CheckpointReader,
290
+
291
+ [Parameter(Mandatory)]
292
+ [string] $CheckpointPath
285
293
  )
286
294
 
287
295
  $oldString = $null
@@ -297,7 +305,7 @@ function Resolve-EditedCheckpointContent {
297
305
  $newString = [string]$ToolInput.new_string
298
306
  }
299
307
 
300
- $onDisk = & $CheckpointReader 'artifacts/orchestration/orchestrator-state.json'
308
+ $onDisk = & $CheckpointReader $CheckpointPath
301
309
  if ([string]::IsNullOrEmpty([string]$onDisk)) {
302
310
  # The on-disk checkpoint does not exist (or is empty); cannot patch.
303
311
  return $null
@@ -363,7 +371,7 @@ function Invoke-CompletionConsistencyDecision {
363
371
  # apply the old_string -> new_string patch in memory (read-then-validate).
364
372
  $content = Get-ClaudeHookToolInputString -ToolInput $toolInput -Name 'content'
365
373
  if (-not $content) {
366
- $content = Resolve-EditedCheckpointContent -ToolInput $toolInput -CheckpointReader $CheckpointReader
374
+ $content = Resolve-EditedCheckpointContent -ToolInput $toolInput -CheckpointReader $CheckpointReader -CheckpointPath $filePath
367
375
  if (-not $content) {
368
376
  # No content, and the Edit could not be resolved against on-disk
369
377
  # state (missing file or non-matching patch): defer and allow.
@@ -28,12 +28,12 @@ $script:OrchestrationBookkeepingTrees = @(
28
28
  )
29
29
 
30
30
  # Characters that make a command line statically unresolvable (D4 row 12, as narrowed by
31
- # issue #663). Interpolation characters are unresolvable in every quote state, because a
32
- # double-quoted span still expands them. Redirection characters are unresolvable only
33
- # outside a quoted span: inside single or double quotes the shell treats `<` and `>` as
34
- # literal text, so a quoted Co-Authored-By trailer is not a redirection.
31
+ # issues #663 and #713): interpolation characters outside single quotes; outside-quote
32
+ # characters (`<`, `>`, and the `#` comment introducer) outside any quote; and typographic
33
+ # quotes (U+2018 to U+201E) anywhere, because a PowerShell host reads them as quotes.
35
34
  $script:InterpolationCommandCharacters = [char[]]@('$', '`')
36
- $script:RedirectionCommandCharacters = [char[]]@('>', '<')
35
+ $script:OutsideQuoteCommandCharacters = [char[]]@('>', '<', '#')
36
+ $script:TypographicQuoteCharacters = [char[]]@(0x2018, 0x2019, 0x201A, 0x201B, 0x201C, 0x201D, 0x201E)
37
37
 
38
38
  # Wildcards that make an operand a glob (D4 row 15). Only the literal prefix before the
39
39
  # first of these is prefix-tested.
@@ -60,10 +60,9 @@ function Split-OrchestrationCommandLine {
60
60
  .SYNOPSIS
61
61
  Splits a command line into segments on chain operators outside quotes.
62
62
  .DESCRIPTION
63
- Realizes D4 row 13. Quote state is tracked while scanning so a chain operator
64
- inside a quoted span does not split. The returned `Balanced` flag reports whether
65
- the scan ended outside every quote; unbalanced text is not splittable and the
66
- caller denies (D4 rows 11 and 13). Empty and whitespace-only segments are dropped.
63
+ Realizes D4 row 13. Quote state and POSIX backslash escapes (issue #710) are tracked, so a quoted or
64
+ escaped chain operator does not split. `Balanced` reports whether the scan ended outside every quote;
65
+ the caller denies unbalanced text (D4 rows 11 and 13). Empty and whitespace-only segments are dropped.
67
66
  .OUTPUTS
68
67
  System.Collections.Hashtable with keys `Balanced` (bool) and `Segments` (string[]).
69
68
  #>
@@ -74,8 +73,9 @@ function Split-OrchestrationCommandLine {
74
73
  $segments = [System.Collections.Generic.List[string]]::new()
75
74
  $current = [System.Text.StringBuilder]::new()
76
75
  $openQuote = [char]0
77
-
76
+ $escaped = $false
78
77
  foreach ($character in $CommandText.ToCharArray()) {
78
+ if ($escaped -or ($character -eq '\' -and $openQuote -ne "'")) { $escaped = -not $escaped; [void]$current.Append($character); continue }
79
79
  if ($openQuote -ne [char]0) {
80
80
  if ($character -eq $openQuote) {
81
81
  $openQuote = [char]0
@@ -112,12 +112,12 @@ function Test-OrchestrationCommandTextUnresolvable {
112
112
  .SYNOPSIS
113
113
  Reports whether a command line carries a statically unresolvable character.
114
114
  .DESCRIPTION
115
- Realizes D4 row 12 as narrowed by issue #663. Quote state is tracked with the same
116
- state machine as Split-OrchestrationCommandLine. An interpolation character (`$` or
117
- backtick) answers true in any quote state; a redirection character (`<` or `>`)
118
- answers true only outside a quoted span. Backslash escapes are not modelled, so a
119
- backslash before any quote character, or anywhere inside a double-quoted span,
120
- answers true: the shell may end the span where this scan does not (fail closed).
115
+ Realizes D4 row 12 as narrowed by issues #663 and #713, with quote state tracked as in
116
+ Split-OrchestrationCommandLine. `$` or backtick answers true outside quotes or inside
117
+ double quotes (single quotes keep it literal); `<`, `>`, or a `#` comment answers true
118
+ only outside a quoted span. A typographic quote (U+2018 to U+201E), a backslash before
119
+ any quote character, or a backslash inside a double-quoted span answers true anywhere:
120
+ a shell may end the span where this scan does not (fail closed).
121
121
  .PARAMETER CommandText
122
122
  The full command line as the shell would receive it.
123
123
  .OUTPUTS
@@ -127,17 +127,17 @@ function Test-OrchestrationCommandTextUnresolvable {
127
127
  [OutputType([bool])]
128
128
  param([Parameter(Mandatory)][AllowEmptyString()][string] $CommandText)
129
129
 
130
- # An escaped quote moves a span boundary the scan cannot model (issue #663 remediation CR-1).
131
- if ($CommandText.Contains('\"') -or $CommandText.Contains("\'")) {
130
+ # An escaped or typographic quote moves a span boundary the scan cannot model (#663, #713).
131
+ if ($CommandText.Contains('\"') -or $CommandText.Contains("\'") -or $CommandText.IndexOfAny($script:TypographicQuoteCharacters) -ge 0) {
132
132
  return $true
133
133
  }
134
134
 
135
135
  $openQuote = [char]0
136
136
 
137
137
  # Scan every character once, tracking whether it sits inside a quoted span so that
138
- # redirection characters are judged only where the shell would honour them.
138
+ # outside-quote characters are judged only where the shell would honour them.
139
139
  foreach ($character in $CommandText.ToCharArray()) {
140
- if ($script:InterpolationCommandCharacters -contains $character) {
140
+ if ($openQuote -ne "'" -and $script:InterpolationCommandCharacters -contains $character) {
141
141
  return $true
142
142
  }
143
143
 
@@ -153,7 +153,7 @@ function Test-OrchestrationCommandTextUnresolvable {
153
153
  }
154
154
  } elseif ($character -eq '"' -or $character -eq "'") {
155
155
  $openQuote = $character
156
- } elseif ($script:RedirectionCommandCharacters -contains $character) {
156
+ } elseif ($script:OutsideQuoteCommandCharacters -contains $character) {
157
157
  return $true
158
158
  }
159
159
  }
@@ -393,21 +393,21 @@ function Test-ExemptOrchestrationSegmentToken {
393
393
  }
394
394
 
395
395
  if ($candidate.StartsWith('-')) {
396
- # The message option is the only modelled option on either subcommand. Rows
396
+ # Only the message and trailer options of commit are modelled (issue #713). Rows
397
397
  # 2, 5, 6, 8, and 10 all land here and deny, including a dash-leading
398
398
  # operand supplied without a preceding separator.
399
399
  if ($subcommand -cne 'commit') {
400
400
  return $false
401
401
  }
402
- if ($candidate -ceq '-m' -or $candidate -ceq '--message') {
403
- # The message value is the following token and is not a pathspec.
402
+ if ($candidate -ceq '-m' -or $candidate -ceq '--message' -or $candidate -ceq '--trailer') {
403
+ # The message or trailer value is the following token and is not a pathspec.
404
404
  $index += 2
405
405
  if ($index -gt $Token.Count) {
406
406
  return $false
407
407
  }
408
408
  continue
409
409
  }
410
- if ($candidate.StartsWith('--message=') -or
410
+ if ($candidate.StartsWith('--message=') -or $candidate.StartsWith('--trailer=') -or
411
411
  ($candidate.Length -gt 2 -and $candidate.StartsWith('-m'))) {
412
412
  $index++
413
413
  continue
@@ -464,8 +464,8 @@ function Test-ExemptOrchestrationStagingCommand {
464
464
  return $false
465
465
  }
466
466
 
467
- # Row 12: interpolation anywhere, redirection outside quotes, and unmodelled backslash
468
- # escapes are not statically resolvable, so the operand list cannot be trusted.
467
+ # Row 12: `$` or backtick outside single quotes, `<`, `>`, or `#` outside quotes, any
468
+ # typographic quote, and unmodelled backslash escapes make the operand list untrustworthy.
469
469
  if (Test-OrchestrationCommandTextUnresolvable -CommandText $CommandText) {
470
470
  return $false
471
471
  }
@@ -18,6 +18,17 @@
18
18
  - Test-BlastRadius port of validate_blast_radius
19
19
  - Test-BlastRadiusConflict port of conflicts
20
20
 
21
+ It also re-exports two scheduling functions of BlastRadiusScheduling.psm1
22
+ (issue #722); each takes the relation as -Relation, so a caller passes
23
+ ${function:Test-BlastRadiusConflict} from this facade:
24
+
25
+ - Get-BlastRadiusConflictEdge port of schedule_conflict_edges
26
+ - Get-BlastRadiusPairDecision port of decide_pair
27
+
28
+ It also re-exports the six functions of BlastRadiusWriteIntent.psm1 (issue
29
+ #722); Get-BlastRadius and Get-NormalizedDeclaredRadius delegate their
30
+ write_intent_extraction flag branch to that module.
31
+
21
32
  The Python modules remain the authoritative reference implementation. This
22
33
  module is one half of a two-language mirror; it never imports validator
23
34
  logic. Every function is pure: no filesystem, subprocess, network, or
@@ -60,6 +71,8 @@ Import-Module (Join-Path -Path $PSScriptRoot -ChildPath 'BlastRadiusConfig.psm1'
60
71
  Import-Module (Join-Path -Path $PSScriptRoot -ChildPath 'BlastRadiusNormalization.psm1') -Force -ErrorAction Stop
61
72
  Import-Module (Join-Path -Path $PSScriptRoot -ChildPath 'BlastRadiusValidation.psm1') -Force -ErrorAction Stop
62
73
  Import-Module (Join-Path -Path $PSScriptRoot -ChildPath 'BlastRadiusConflict.psm1') -Force -ErrorAction Stop
74
+ Import-Module (Join-Path -Path $PSScriptRoot -ChildPath 'BlastRadiusScheduling.psm1') -Force -ErrorAction Stop
75
+ Import-Module (Join-Path -Path $PSScriptRoot -ChildPath 'BlastRadiusWriteIntent.psm1') -Force -ErrorAction Stop
63
76
 
64
77
  # Feature-folder handling. Every radius contains its own feature folder, and a
65
78
  # caller may pass either a bare folder name or an already-qualified path.
@@ -169,10 +182,19 @@ function Get-BlastRadius {
169
182
  # derived radius always passes V1 and V2 against its own plan (issue #452).
170
183
  $rootSurface = [string[]]@(Get-ConfigRootSurface -Config $Config)
171
184
 
172
- $specLine = [string[]]@(ConvertTo-NormalizedLine -Text $SpecText)
185
+ # Write-intent mode (issue #722) delegates to BlastRadiusWriteIntent.psm1:
186
+ # plan paths come from the selector Test-BlastRadius also uses and the spec
187
+ # contributes contracts only.
173
188
  $entry = [System.Collections.Generic.List[string]]::new()
174
- $entry.AddRange([string[]]@(Get-PlanPaths -PlanText $PlanText -RootSurface $rootSurface))
175
- $entry.AddRange([string[]]@(Get-PathFromLine -Line $specLine -RootSurface $rootSurface))
189
+ if (Test-WriteIntentExtractionEnabled -Config $Config) {
190
+ $entry.AddRange([string[]]@(Get-PlanPathForConfig -PlanText $PlanText -Config $Config -RootSurface $rootSurface))
191
+ $contract = @(Get-WriteIntentSpecContract -SpecText $SpecText)
192
+ } else {
193
+ $specLine = [string[]]@(ConvertTo-NormalizedLine -Text $SpecText)
194
+ $entry.AddRange([string[]]@(Get-PlanPaths -PlanText $PlanText -RootSurface $rootSurface))
195
+ $entry.AddRange([string[]]@(Get-PathFromLine -Line $specLine -RootSurface $rootSurface))
196
+ $contract = @(Get-ContractIdentifier -SpecText $SpecText)
197
+ }
176
198
 
177
199
  # Read-by-mandate citations are dropped before the feature folder is added: a
178
200
  # plan cites the policy rules because its author was told to read them, not
@@ -193,7 +215,7 @@ function Get-BlastRadius {
193
215
  paths = $paths
194
216
  modules = @(Resolve-BlastRadiusModule -PathEntry $paths -Config $Config)
195
217
  shared_surfaces = @(Resolve-BlastRadiusSharedSurface -ConcretePath $concrete -Config $Config)
196
- contracts = @(Get-ContractIdentifier -SpecText $SpecText)
218
+ contracts = $contract
197
219
  source = $Source
198
220
  computed_at = $ComputedAt
199
221
  }
@@ -264,7 +286,14 @@ function Get-NormalizedDeclaredRadius {
264
286
  }
265
287
  }
266
288
 
267
- $paths = [string[]]@(Get-NonMandateReadEntry -Entry $accepted.ToArray() `
289
+ # Write-intent mode (issue #722) applies the token-level rules W1, W4, and W6
290
+ # before the mandate-read filter; the feature-folder glob is kept.
291
+ $acceptedEntry = [string[]]$accepted.ToArray()
292
+ if (Test-WriteIntentExtractionEnabled -Config $Config) {
293
+ $acceptedEntry = [string[]]@(Select-WriteIntentPathEntry -Entry $acceptedEntry -RootSurface $rootSurface `
294
+ -PathRoot ([string[]]@(Get-ConfigPathRoot -Config $Config)))
295
+ }
296
+ $paths = [string[]]@(Get-NonMandateReadEntry -Entry $acceptedEntry `
268
297
  -MandateRead ([string[]]@(Get-ConfigMandateRead -Config $Config)))
269
298
  $concrete = [string[]]@(Get-ConcreteEntry -Entry $paths)
270
299
 
@@ -435,4 +464,12 @@ Export-ModuleMember -Function `
435
464
  Get-NormalizedDeclaredRadius, `
436
465
  Get-BlastRadiusFromObservedPaths, `
437
466
  Test-BlastRadius, `
438
- Test-BlastRadiusConflict
467
+ Test-BlastRadiusConflict, `
468
+ Get-BlastRadiusConflictEdge, `
469
+ Get-BlastRadiusPairDecision, `
470
+ Test-WriteIntentExtractionEnabled, `
471
+ Get-ConfigPathRoot, `
472
+ Get-WriteIntentPlanPath, `
473
+ Get-WriteIntentSpecContract, `
474
+ Select-WriteIntentPathEntry, `
475
+ Get-PlanPathForConfig