@ghl-ai/aw 0.1.35 → 0.1.36-beta.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/integrate.mjs CHANGED
@@ -56,11 +56,13 @@ function findFiles(dir, typeName) {
56
56
  }
57
57
 
58
58
  /**
59
- * Copy CLAUDE.md and AGENTS.md to project root.
59
+ * Copy AGENTS.md to project root.
60
+ * CLAUDE.md is intentionally NOT generated — its routing rule hijacks plugin
61
+ * commands like /aw:plan, preventing proper agent dispatch.
60
62
  */
61
63
  export function copyInstructions(cwd, tempDir, namespace) {
62
64
  const createdFiles = [];
63
- for (const file of ['CLAUDE.md', 'AGENTS.md']) {
65
+ for (const file of ['AGENTS.md']) {
64
66
  const dest = join(cwd, file);
65
67
  if (existsSync(dest)) continue;
66
68
 
@@ -78,9 +80,7 @@ export function copyInstructions(cwd, tempDir, namespace) {
78
80
  }
79
81
  }
80
82
 
81
- const content = file === 'CLAUDE.md'
82
- ? generateClaudeMd(cwd, namespace)
83
- : generateAgentsMd(cwd, namespace);
83
+ const content = generateAgentsMd(cwd, namespace);
84
84
  if (content) {
85
85
  writeFileSync(dest, content);
86
86
  fmt.logSuccess(`Created ${file}`);
@@ -100,7 +100,9 @@ Team: ${team} | Local-first orchestration via \`.aw_docs/\` | MCPs: \`memory/*\`
100
100
 
101
101
  > **Every non-trivial task MUST call \`Skill(skill: "platform-ai-task-router")\` BEFORE any response.**
102
102
  >
103
- > **Trivial** (do directly): typo fixes, single-line edits, git ops, file exploration, factual code questions.
103
+ > **Exempt from routing (execute directly):**
104
+ > - **Plugin commands**: any \`/aw:*\` slash command — these have their own agent dispatch via the plugin system. Execute the plugin command as-is, do NOT re-route through the task router.
105
+ > - **Trivial tasks**: typo fixes, single-line edits, git ops, file exploration, factual code questions.
104
106
  >
105
107
  > Everything else — including tasks phrased as questions, suggestions, or discussions — routes first.
106
108
  > **No conversational responses first. No planning first. Route first.**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ghl-ai/aw",
3
- "version": "0.1.35",
3
+ "version": "0.1.36-beta.2",
4
4
  "description": "Agentic Workspace CLI — pull, push & manage agents, skills and commands from the registry",
5
5
  "type": "module",
6
6
  "bin": {
@@ -24,7 +24,8 @@
24
24
  "registry.mjs",
25
25
  "apply.mjs",
26
26
  "update.mjs",
27
- "hooks.mjs"
27
+ "hooks.mjs",
28
+ "ecc.mjs"
28
29
  ],
29
30
  "engines": {
30
31
  "node": ">=18.0.0"
package/apply.mjs DELETED
@@ -1,79 +0,0 @@
1
- // apply.mjs — Apply sync plan (file operations). Zero dependencies.
2
-
3
- import { readFileSync, writeFileSync, mkdirSync, cpSync, existsSync, unlinkSync, rmdirSync, readdirSync } from 'node:fs';
4
- import { dirname, basename } from 'node:path';
5
-
6
- function writeConflictMarkers(filePath, localContent, registryContent) {
7
- writeFileSync(filePath, [
8
- '<<<<<<< LOCAL',
9
- localContent.trimEnd(),
10
- '=======',
11
- registryContent.trimEnd(),
12
- '>>>>>>> REGISTRY',
13
- '',
14
- ].join('\n'));
15
- }
16
-
17
- /**
18
- * Apply actions to the workspace.
19
- * All actions are file-level (no directory-level operations).
20
- *
21
- * @param {Array} actions
22
- * @param {{ teamNS?: string }} opts
23
- * teamNS — when set, replaces all occurrences of `$TEAM_NS` in file content
24
- * with this value. Used when pulling [template] as a renamed team namespace.
25
- * @returns {number} count of files with conflicts
26
- */
27
- export function applyActions(actions, { teamNS } = {}) {
28
- let conflicts = 0;
29
-
30
- for (const act of actions) {
31
- switch (act.action) {
32
- case 'ADD':
33
- case 'UPDATE':
34
- mkdirSync(dirname(act.targetPath), { recursive: true });
35
- if (teamNS && act.sourcePath.endsWith('.md')) {
36
- const content = readFileSync(act.sourcePath, 'utf8').replaceAll('$TEAM_NS', teamNS);
37
- writeFileSync(act.targetPath, content);
38
- } else {
39
- cpSync(act.sourcePath, act.targetPath);
40
- }
41
- break;
42
-
43
- case 'CONFLICT': {
44
- const local = existsSync(act.targetPath) ? readFileSync(act.targetPath, 'utf8') : '';
45
- let registry = readFileSync(act.sourcePath, 'utf8');
46
- if (teamNS) registry = registry.replaceAll('$TEAM_NS', teamNS);
47
- mkdirSync(dirname(act.targetPath), { recursive: true });
48
- writeConflictMarkers(act.targetPath, local, registry);
49
- conflicts++;
50
- break;
51
- }
52
-
53
- case 'ORPHAN':
54
- // File was removed from registry — delete local copy
55
- if (existsSync(act.targetPath)) {
56
- try { unlinkSync(act.targetPath); } catch { /* best effort */ }
57
- // Clean up empty parent dirs, stop at .aw_registry/ boundary
58
- let dir = dirname(act.targetPath);
59
- while (basename(dir) !== '.aw_registry') {
60
- try {
61
- const entries = readdirSync(dir);
62
- if (entries.length === 0) {
63
- rmdirSync(dir);
64
- dir = dirname(dir);
65
- } else {
66
- break;
67
- }
68
- } catch { break; }
69
- }
70
- }
71
- break;
72
-
73
- case 'UNCHANGED':
74
- break;
75
- }
76
- }
77
-
78
- return conflicts;
79
- }
package/manifest.mjs DELETED
@@ -1,64 +0,0 @@
1
- // manifest.mjs — .sync-manifest.json management. Zero dependencies.
2
-
3
- import { readFileSync, writeFileSync, existsSync } from 'node:fs';
4
- import { join } from 'node:path';
5
- import { hashFile } from './registry.mjs';
6
-
7
- const MANIFEST_FILE = '.sync-manifest.json';
8
-
9
- export function manifestPath(workspaceDir) {
10
- return join(workspaceDir, MANIFEST_FILE);
11
- }
12
-
13
- export function load(workspaceDir) {
14
- const p = manifestPath(workspaceDir);
15
- if (!existsSync(p)) return { version: 3, synced_at: '', namespace: '', files: {} };
16
- try {
17
- return JSON.parse(readFileSync(p, 'utf8'));
18
- } catch {
19
- return { version: 3, synced_at: '', namespace: '', files: {} };
20
- }
21
- }
22
-
23
- export function save(workspaceDir, manifest) {
24
- writeFileSync(manifestPath(workspaceDir), JSON.stringify(manifest, null, 2) + '\n');
25
- }
26
-
27
- export function update(workspaceDir, actions, namespaceName, opts = {}) {
28
- const manifest = load(workspaceDir);
29
- manifest.synced_at = new Date().toISOString();
30
- manifest.namespace = namespaceName || manifest.namespace;
31
- manifest.version = 3;
32
-
33
- for (const act of actions) {
34
- const manifestKey = act.targetFilename;
35
-
36
- if (act.action === 'ADD' || act.action === 'UPDATE' || act.action === 'UNCHANGED') {
37
- if (existsSync(act.targetPath)) {
38
- manifest.files[manifestKey] = {
39
- sha256: hashFile(act.targetPath),
40
- source: act.source,
41
- // When pulled from template (renamed namespace), these files don't exist
42
- // in the remote registry yet — mark as null so push detects them as new.
43
- registry_sha256: opts.fromTemplate ? null : act.registryHash,
44
- };
45
- }
46
- } else if (act.action === 'CONFLICT') {
47
- // Update local hash but keep OLD registry_sha256
48
- // so next sync still detects the unacknowledged registry change
49
- if (existsSync(act.targetPath)) {
50
- const existing = manifest.files[manifestKey];
51
- manifest.files[manifestKey] = {
52
- sha256: hashFile(act.targetPath),
53
- source: act.source,
54
- registry_sha256: existing?.registry_sha256 ?? act.registryHash,
55
- };
56
- }
57
- } else if (act.action === 'ORPHAN') {
58
- delete manifest.files[manifestKey];
59
- }
60
- }
61
-
62
- save(workspaceDir, manifest);
63
- return manifest;
64
- }
package/plan.mjs DELETED
@@ -1,147 +0,0 @@
1
- // plan.mjs — Compute sync plan (what actions to take). Zero dependencies.
2
-
3
- import { existsSync } from 'node:fs';
4
- import { join } from 'node:path';
5
- import { walkRegistryTree, hashFile } from './registry.mjs';
6
- import { load as loadManifest } from './manifest.mjs';
7
- import { matchesAny } from './glob.mjs';
8
-
9
- /**
10
- * Compute sync actions from registry dirs → workspace.
11
- *
12
- * @param {Array<{name: string, path: string}>} registryDirs — sources to scan
13
- * @param {string} workspaceDir — .aw_registry path
14
- * @param {string[]} includePatterns — paths to filter (empty = all)
15
- * @returns {{ actions: Array }}
16
- */
17
- export function computePlan(registryDirs, workspaceDir, includePatterns = [], { skipOrphans = false } = {}) {
18
- const plan = new Map();
19
-
20
- for (const { name, path } of registryDirs) {
21
- for (const entry of walkRegistryTree(path, name)) {
22
- // Apply include filter
23
- if (includePatterns.length > 0 && !matchesAny(entry.registryPath, includePatterns)) {
24
- continue;
25
- }
26
-
27
- // Key must include namespacePath to avoid collisions when the same
28
- // slug exists under different domains (e.g. backend/agents/developer
29
- // vs frontend/agents/developer).
30
- const key = entry.skillRelPath
31
- ? `${entry.namespacePath}/${entry.type}/${entry.slug}/${entry.skillRelPath}`
32
- : `${entry.namespacePath}/${entry.type}/${entry.slug}`;
33
- plan.set(key, { ...entry, source: entry.namespacePath || name });
34
- }
35
- }
36
-
37
- const manifest = loadManifest(workspaceDir);
38
- const actions = [];
39
-
40
- for (const [key, entry] of plan) {
41
- // Mirror registry structure: namespace/type/slug
42
- // Skills: namespace/skills/slug/relPath
43
- // Others: namespace/agents/slug.md
44
- let targetFilename, targetPath;
45
- if (entry.skillRelPath) {
46
- targetFilename = `${entry.namespacePath}/${entry.type}/${entry.slug}/${entry.skillRelPath}`;
47
- targetPath = join(workspaceDir, entry.namespacePath, entry.type, entry.slug, entry.skillRelPath);
48
- } else {
49
- targetFilename = `${entry.namespacePath}/${entry.type}/${entry.slug}.md`;
50
- targetPath = join(workspaceDir, entry.namespacePath, entry.type, `${entry.slug}.md`);
51
- }
52
-
53
- const manifestKey = targetFilename;
54
- const registryHash = hashFile(entry.sourcePath);
55
-
56
- let action;
57
-
58
- if (!existsSync(targetPath)) {
59
- action = 'ADD';
60
- } else {
61
- const localHash = hashFile(targetPath);
62
-
63
- if (localHash === registryHash) {
64
- action = 'UNCHANGED';
65
- } else {
66
- const manifestEntry = manifest.files?.[manifestKey];
67
- if (!manifestEntry) {
68
- action = 'UPDATE';
69
- } else {
70
- const localChanged = localHash !== manifestEntry.sha256;
71
- // registry_sha256 is null for template-derived files (never pushed).
72
- // Treat null as "no known registry version" — can't conflict with unknown.
73
- const registryChanged = manifestEntry.registry_sha256 != null
74
- && registryHash !== manifestEntry.registry_sha256;
75
- if (localChanged && registryChanged) {
76
- action = 'CONFLICT';
77
- } else if (localChanged) {
78
- action = 'UNCHANGED';
79
- } else {
80
- action = 'UPDATE';
81
- }
82
- }
83
- }
84
- }
85
-
86
- actions.push({
87
- slug: entry.slug,
88
- source: entry.source,
89
- type: entry.type,
90
- action,
91
- sourcePath: entry.sourcePath,
92
- targetPath,
93
- targetFilename,
94
- isDirectory: false,
95
- registryHash,
96
- namespacePath: entry.namespacePath,
97
- registryPath: entry.registryPath,
98
- });
99
- }
100
-
101
- // Orphans: in manifest but not in plan.
102
- // Scoped to namespaces actually walked — only entries whose namespace was
103
- // part of this registry scan can be orphaned. This prevents a "platform" pull
104
- // from orphaning "platform/ai" entries (different pull, different sparse checkout).
105
- if (manifest.files && !skipOrphans) {
106
- const planTargets = new Set(actions.map(a => a.targetFilename));
107
- const walkedNamespaces = new Set(actions.map(a => a.namespacePath));
108
-
109
- for (const [manifestKey, manifestEntry] of Object.entries(manifest.files)) {
110
- if (planTargets.has(manifestKey)) continue;
111
-
112
- // Extract namespace from manifest key
113
- const parts = manifestKey.split('/');
114
- let typeIdx = -1;
115
- for (let i = 0; i < parts.length; i++) {
116
- if (['agents', 'skills', 'commands', 'evals'].includes(parts[i])) {
117
- typeIdx = i;
118
- break;
119
- }
120
- }
121
- if (typeIdx === -1) continue;
122
- const namespace = parts.slice(0, typeIdx).join('/');
123
-
124
- // Only orphan entries whose exact namespace was walked in this scan
125
- if (!walkedNamespaces.has(namespace)) continue;
126
-
127
- const type = parts[typeIdx];
128
- const slug = parts[typeIdx + 1]?.replace(/\.md$/, '');
129
-
130
- actions.push({
131
- slug,
132
- source: manifestEntry.source || 'unknown',
133
- type,
134
- action: 'ORPHAN',
135
- sourcePath: '',
136
- targetPath: join(workspaceDir, manifestKey),
137
- targetFilename: manifestKey,
138
- isDirectory: false,
139
- registryHash: '',
140
- namespacePath: namespace,
141
- registryPath: '',
142
- });
143
- }
144
- }
145
-
146
- return { actions };
147
- }