@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/cli.mjs +2 -1
- package/commands/drop.mjs +45 -47
- package/commands/init.mjs +122 -125
- package/commands/nuke.mjs +30 -10
- package/commands/pull.mjs +57 -370
- package/commands/push.mjs +297 -287
- package/commands/status.mjs +50 -80
- package/config.mjs +2 -2
- package/constants.mjs +6 -0
- package/ecc.mjs +180 -0
- package/fmt.mjs +2 -0
- package/git.mjs +233 -1
- package/integrate.mjs +8 -6
- package/package.json +3 -2
- package/apply.mjs +0 -79
- package/manifest.mjs +0 -64
- package/plan.mjs +0 -147
package/integrate.mjs
CHANGED
|
@@ -56,11 +56,13 @@ function findFiles(dir, typeName) {
|
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
/**
|
|
59
|
-
* Copy
|
|
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 ['
|
|
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 =
|
|
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
|
-
> **
|
|
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.
|
|
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
|
-
}
|