@cloverleaf/reference-impl 0.8.1 → 0.8.2

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/VERSION CHANGED
@@ -1 +1 @@
1
- 0.8.1
1
+ 0.8.2
package/dist/cli.mjs CHANGED
@@ -44,7 +44,7 @@
44
44
  */
45
45
  import { readFileSync, mkdirSync, copyFileSync, appendFileSync } from 'node:fs';
46
46
  import { dirname, join } from 'node:path';
47
- import { execSync } from 'node:child_process';
47
+ import { execSync, execFileSync } from 'node:child_process';
48
48
  import { loadTask, saveTask } from './task.mjs';
49
49
  import { advanceStatus } from './task.mjs';
50
50
  import { emitGateDecision } from './events.mjs';
@@ -66,7 +66,7 @@ import { buildBaselinePath } from './visual-diff.mjs';
66
66
  import { computeReadyTasks, detectCycle } from './dag-walker.mjs';
67
67
  import { readWalkState, writeWalkState, walkStatePath } from './walk-state.mjs';
68
68
  import { loadWalkerConfig } from './walker-config.mjs';
69
- import { classifyFiles } from './scope-check.mjs';
69
+ import { classifyFiles, normalizePath } from './scope-check.mjs';
70
70
  import { computeRfcTasksView } from './rfc-tasks.mjs';
71
71
  import { loadSecretPatternsConfig, scanSecrets } from './secret-scan.mjs';
72
72
  import { classifyTaskSecurity } from './security-classify.mjs';
@@ -652,8 +652,29 @@ try {
652
652
  catch {
653
653
  // No diff is fine (empty branch)
654
654
  }
655
- // 4. Classify and output
656
- const result = classifyFiles(taskDoc, modifiedFiles, siblingScopes);
655
+ // 4. Compute sharedFiles via git check-attr (merge=union honors user's multi-writer intent)
656
+ let sharedFiles = new Set();
657
+ if (modifiedFiles.length > 0) {
658
+ try {
659
+ const out = execFileSync('git', ['-C', repoRoot, 'check-attr', '-z', 'merge', '--', ...modifiedFiles], { encoding: 'utf-8' });
660
+ // -z output: NUL-separated triplets: path\0attr\0value\0
661
+ const parts = out.split('\0');
662
+ for (let i = 0; i + 2 < parts.length; i += 3) {
663
+ const path = parts[i];
664
+ const attr = parts[i + 1];
665
+ const value = parts[i + 2];
666
+ if (attr === 'merge' && value === 'union') {
667
+ sharedFiles.add(normalizePath(path));
668
+ }
669
+ }
670
+ }
671
+ catch (err) {
672
+ process.stderr.write(`cloverleaf-cli check-scope: git check-attr failed (${err instanceof Error ? err.message : String(err)}); proceeding with no shared-file annotations.\n`);
673
+ sharedFiles = new Set();
674
+ }
675
+ }
676
+ // 5. Classify and output
677
+ const result = classifyFiles(taskDoc, modifiedFiles, siblingScopes, sharedFiles);
657
678
  process.stdout.write(JSON.stringify(result) + '\n');
658
679
  process.exit(0);
659
680
  }
@@ -37,7 +37,7 @@
37
37
  * - Strip trailing `/` (preserving a non-empty result)
38
38
  * - Return the resulting string (may be empty for degenerate inputs; caller filters)
39
39
  */
40
- function normalizePath(p) {
40
+ export function normalizePath(p) {
41
41
  let s = p.trim().replace(/\\/g, '/');
42
42
  // Strip leading `./` repeatedly
43
43
  while (s.startsWith('./')) {
@@ -71,7 +71,7 @@ function normalizePaths(paths) {
71
71
  * @param siblingScopes - Other tasks in the same Plan with their declared files.
72
72
  * @returns - { contested, own, extension } with arrays sorted by file path.
73
73
  */
74
- export function classifyFiles(taskDoc, modifiedFiles, siblingScopes) {
74
+ export function classifyFiles(taskDoc, modifiedFiles, siblingScopes, sharedFiles) {
75
75
  // 1. Normalize own files from taskDoc.scope.files_touched
76
76
  const scope = taskDoc['scope'];
77
77
  const rawOwn = Array.isArray(scope?.['files_touched'])
@@ -117,6 +117,11 @@ export function classifyFiles(taskDoc, modifiedFiles, siblingScopes) {
117
117
  if (ownSet.has(f)) {
118
118
  own.push(f);
119
119
  }
120
+ else if (sharedFiles?.has(f)) {
121
+ // merge=union (or other shared-intent annotation): never contested.
122
+ // Falls into extension so post-merge auto-extend picks it up.
123
+ extension.push(f);
124
+ }
120
125
  else if (siblingMap.has(f)) {
121
126
  const owners = siblingMap.get(f);
122
127
  contested.push({ file: f, owner: owners[0] }); // lex-smallest wins
package/lib/cli.ts CHANGED
@@ -45,7 +45,7 @@
45
45
 
46
46
  import { readFileSync, mkdirSync, copyFileSync, appendFileSync, existsSync } from 'node:fs';
47
47
  import { dirname, join } from 'node:path';
48
- import { execSync } from 'node:child_process';
48
+ import { execSync, execFileSync } from 'node:child_process';
49
49
  import { loadTask, saveTask } from './task.js';
50
50
  import { advanceStatus } from './task.js';
51
51
  import { emitGateDecision } from './events.js';
@@ -68,7 +68,7 @@ import { buildBaselinePath } from './visual-diff.js';
68
68
  import { computeReadyTasks, detectCycle } from './dag-walker.js';
69
69
  import { readWalkState, writeWalkState, walkStatePath } from './walk-state.js';
70
70
  import { loadWalkerConfig } from './walker-config.js';
71
- import { classifyFiles } from './scope-check.js';
71
+ import { classifyFiles, normalizePath } from './scope-check.js';
72
72
  import type { SiblingScope } from './scope-check.js';
73
73
  import { computeRfcTasksView, type RfcTasksView } from './rfc-tasks.js';
74
74
  import { loadSecretPatternsConfig, scanSecrets } from './secret-scan.js';
@@ -663,8 +663,35 @@ try {
663
663
  // No diff is fine (empty branch)
664
664
  }
665
665
 
666
- // 4. Classify and output
667
- const result = classifyFiles(taskDoc, modifiedFiles, siblingScopes);
666
+ // 4. Compute sharedFiles via git check-attr (merge=union honors user's multi-writer intent)
667
+ let sharedFiles = new Set<string>();
668
+ if (modifiedFiles.length > 0) {
669
+ try {
670
+ const out = execFileSync(
671
+ 'git',
672
+ ['-C', repoRoot, 'check-attr', '-z', 'merge', '--', ...modifiedFiles],
673
+ { encoding: 'utf-8' },
674
+ );
675
+ // -z output: NUL-separated triplets: path\0attr\0value\0
676
+ const parts = out.split('\0');
677
+ for (let i = 0; i + 2 < parts.length; i += 3) {
678
+ const path = parts[i];
679
+ const attr = parts[i + 1];
680
+ const value = parts[i + 2];
681
+ if (attr === 'merge' && value === 'union') {
682
+ sharedFiles.add(normalizePath(path));
683
+ }
684
+ }
685
+ } catch (err) {
686
+ process.stderr.write(
687
+ `cloverleaf-cli check-scope: git check-attr failed (${err instanceof Error ? err.message : String(err)}); proceeding with no shared-file annotations.\n`,
688
+ );
689
+ sharedFiles = new Set();
690
+ }
691
+ }
692
+
693
+ // 5. Classify and output
694
+ const result = classifyFiles(taskDoc, modifiedFiles, siblingScopes, sharedFiles);
668
695
  process.stdout.write(JSON.stringify(result) + '\n');
669
696
  process.exit(0);
670
697
  }
@@ -56,7 +56,7 @@ export interface SiblingScope {
56
56
  * - Strip trailing `/` (preserving a non-empty result)
57
57
  * - Return the resulting string (may be empty for degenerate inputs; caller filters)
58
58
  */
59
- function normalizePath(p: string): string {
59
+ export function normalizePath(p: string): string {
60
60
  let s = p.trim().replace(/\\/g, '/');
61
61
  // Strip leading `./` repeatedly
62
62
  while (s.startsWith('./')) {
@@ -93,7 +93,8 @@ function normalizePaths(paths: string[]): string[] {
93
93
  export function classifyFiles(
94
94
  taskDoc: TaskDoc,
95
95
  modifiedFiles: string[],
96
- siblingScopes: SiblingScope[]
96
+ siblingScopes: SiblingScope[],
97
+ sharedFiles?: Set<string>
97
98
  ): ClassifyResult {
98
99
  // 1. Normalize own files from taskDoc.scope.files_touched
99
100
  const scope = taskDoc['scope'] as Record<string, unknown> | undefined;
@@ -138,6 +139,10 @@ export function classifyFiles(
138
139
  for (const f of filteredModified) {
139
140
  if (ownSet.has(f)) {
140
141
  own.push(f);
142
+ } else if (sharedFiles?.has(f)) {
143
+ // merge=union (or other shared-intent annotation): never contested.
144
+ // Falls into extension so post-merge auto-extend picks it up.
145
+ extension.push(f);
141
146
  } else if (siblingMap.has(f)) {
142
147
  const owners = siblingMap.get(f)!;
143
148
  contested.push({ file: f, owner: owners[0] }); // lex-smallest wins
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cloverleaf/reference-impl",
3
- "version": "0.8.1",
3
+ "version": "0.8.2",
4
4
  "description": "Reference implementation of the Cloverleaf methodology as Claude Code skills. Implements the Tight Loop (Implementer + Reviewer).",
5
5
  "type": "module",
6
6
  "license": "MIT",