cc-ship 0.0.7 → 0.0.8
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/agents/ship-architect.md +12 -5
- package/agents/ship-brainstormer-prd.md +14 -7
- package/agents/ship-brainstormer.md +256 -249
- package/agents/ship-executor.md +17 -4
- package/agents/ship-shaper.md +159 -146
- package/agents/ship-specifier.md +12 -5
- package/agents/ship-splitter.md +13 -6
- package/agents/ship-verifier.md +15 -2
- package/bin/install.js +29 -3
- package/commands/ship/architect.md +17 -8
- package/commands/ship/brainstorm.md +84 -75
- package/commands/ship/execute.md +16 -13
- package/commands/ship/help.md +111 -105
- package/commands/ship/init.md +73 -0
- package/commands/ship/next.md +35 -14
- package/commands/ship/prd.md +16 -7
- package/commands/ship/shape.md +112 -103
- package/commands/ship/specify.md +16 -7
- package/commands/ship/split.md +20 -11
- package/commands/ship/status.md +136 -129
- package/commands/ship/verify.md +13 -10
- package/hooks/check-agent-completion.js +64 -0
- package/hooks/lib/config.js +34 -0
- package/hooks/lib/find-packages.js +37 -0
- package/hooks/lib/parse-frontmatter.js +135 -0
- package/hooks/validate-frontmatter.js +109 -0
- package/hooks/validate-transition.js +136 -0
- package/hooks-settings.json +26 -0
- package/package.json +33 -31
- package/skills/ship-shaping/SKILL.md +151 -151
- package/skills/ship-writing/SKILL.md +152 -152
package/commands/ship/verify.md
CHANGED
|
@@ -9,11 +9,20 @@ Lance l'agent ship-verifier pour verifier l'implementation d'un scope contre ses
|
|
|
9
9
|
|
|
10
10
|
## Instructions
|
|
11
11
|
|
|
12
|
+
### Resolution du projet (PREMIERE ETAPE)
|
|
13
|
+
|
|
14
|
+
1. Lire `cc-ship.json` a la racine du repo
|
|
15
|
+
2. Resoudre le chemin : `{projectsDir}/{currentProject}/`
|
|
16
|
+
3. Si `cc-ship.json` n'existe pas OU `currentProject` est null → ERREUR : "Lance `/ship:init` ou `/ship:next` d'abord pour initialiser un projet."
|
|
17
|
+
4. Utiliser ce chemin partout au lieu de `.ship/`
|
|
18
|
+
|
|
19
|
+
### Lancement de l'agent
|
|
20
|
+
|
|
12
21
|
Tu dois lancer l'agent `ship-verifier` en utilisant le tool Task avec les parametres suivants:
|
|
13
22
|
|
|
14
23
|
```
|
|
15
24
|
subagent_type: ship-verifier
|
|
16
|
-
prompt: [Le package et/ou scope a verifier]
|
|
25
|
+
prompt: "Le chemin du projet est {projectPath}. [Le package et/ou scope a verifier]"
|
|
17
26
|
```
|
|
18
27
|
|
|
19
28
|
## Prerequis
|
|
@@ -22,8 +31,8 @@ Avant de lancer l'agent, verifie que ces fichiers existent :
|
|
|
22
31
|
|
|
23
32
|
| Fichier | Obligatoire | Cree par |
|
|
24
33
|
|---------|-------------|----------|
|
|
25
|
-
|
|
|
26
|
-
|
|
|
34
|
+
| `{projectPath}/packages/<nom>/package.md` | Oui | shaper |
|
|
35
|
+
| `{projectPath}/packages/<nom>/verification.md` | Oui | shaper |
|
|
27
36
|
|
|
28
37
|
### Verification du status
|
|
29
38
|
|
|
@@ -89,17 +98,11 @@ Quand l'agent te retourne une question pour l'utilisateur (verification manuelle
|
|
|
89
98
|
|
|
90
99
|
Verifie le scope courant du package actif (detecte automatiquement).
|
|
91
100
|
|
|
92
|
-
```
|
|
93
|
-
/ship:verify .ship/packages/auth/package.md
|
|
94
|
-
```
|
|
95
|
-
|
|
96
|
-
Verifie le scope courant du package specifie.
|
|
97
|
-
|
|
98
101
|
```
|
|
99
102
|
/ship:verify auth
|
|
100
103
|
```
|
|
101
104
|
|
|
102
|
-
|
|
105
|
+
Verifie le scope courant du package "auth".
|
|
103
106
|
|
|
104
107
|
## Output
|
|
105
108
|
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Hook: Verify agent completion state (SubagentStop).
|
|
5
|
+
* Checks that agents left packages in a consistent state.
|
|
6
|
+
*
|
|
7
|
+
* - ship-executor: at least one package should have status "executed"
|
|
8
|
+
* - ship-verifier: no package should still have status "verifying"
|
|
9
|
+
*
|
|
10
|
+
* Exit 0 = OK, Exit 2 = inconsistent state (warns the user).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const { findPackages } = require('./lib/find-packages');
|
|
14
|
+
|
|
15
|
+
async function main() {
|
|
16
|
+
// Read stdin (SubagentStop JSON)
|
|
17
|
+
let input = '';
|
|
18
|
+
for await (const chunk of process.stdin) {
|
|
19
|
+
input += chunk;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let data;
|
|
23
|
+
try {
|
|
24
|
+
data = JSON.parse(input);
|
|
25
|
+
} catch {
|
|
26
|
+
process.exit(0);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const agentName = data.agent_name || data.subagent_type || '';
|
|
30
|
+
|
|
31
|
+
const packages = findPackages();
|
|
32
|
+
if (!packages || packages.length === 0) {
|
|
33
|
+
process.exit(0); // No packages to check
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Check executor completion
|
|
37
|
+
if (agentName === 'ship-executor') {
|
|
38
|
+
const hasExecuted = packages.some(p => p.metadata.status === 'executed');
|
|
39
|
+
if (!hasExecuted) {
|
|
40
|
+
process.stderr.write(
|
|
41
|
+
"L'executor a termine sans mettre a jour le status d'aucun package a \"executed\".\n" +
|
|
42
|
+
"Verifiez que le frontmatter du package.md a ete mis a jour correctement.\n"
|
|
43
|
+
);
|
|
44
|
+
process.exit(2);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Check verifier completion
|
|
49
|
+
if (agentName === 'ship-verifier') {
|
|
50
|
+
const stillVerifying = packages.filter(p => p.metadata.status === 'verifying');
|
|
51
|
+
if (stillVerifying.length > 0) {
|
|
52
|
+
const names = stillVerifying.map(p => p.name).join(', ');
|
|
53
|
+
process.stderr.write(
|
|
54
|
+
`Le verifier a termine sans conclure pour: ${names}\n` +
|
|
55
|
+
'Le status est encore "verifying" au lieu de "done" ou "executing".\n'
|
|
56
|
+
);
|
|
57
|
+
process.exit(2);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
process.exit(0);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
main();
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Reads cc-ship.json and resolves the project path.
|
|
6
|
+
* Returns { projectsDir, currentProject, projectPath } or null if not configured.
|
|
7
|
+
*/
|
|
8
|
+
function getConfig() {
|
|
9
|
+
const configPath = path.join(process.cwd(), 'cc-ship.json');
|
|
10
|
+
|
|
11
|
+
if (!fs.existsSync(configPath)) {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
let config;
|
|
16
|
+
try {
|
|
17
|
+
config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
18
|
+
} catch {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const projectsDir = config.projectsDir || '.ship';
|
|
23
|
+
const currentProject = config.currentProject || null;
|
|
24
|
+
|
|
25
|
+
if (!currentProject) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const projectPath = path.join(process.cwd(), projectsDir, currentProject);
|
|
30
|
+
|
|
31
|
+
return { projectsDir, currentProject, projectPath };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
module.exports = { getConfig };
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { getConfig } = require('./config');
|
|
4
|
+
const { parseFrontmatter } = require('./parse-frontmatter');
|
|
5
|
+
|
|
6
|
+
// Scans {projectPath}/packages/*/package.md and returns parsed metadata.
|
|
7
|
+
// Returns null if config is not available, otherwise array of packages.
|
|
8
|
+
function findPackages() {
|
|
9
|
+
const config = getConfig();
|
|
10
|
+
if (!config) return null;
|
|
11
|
+
|
|
12
|
+
const packagesDir = path.join(config.projectPath, 'packages');
|
|
13
|
+
if (!fs.existsSync(packagesDir)) return [];
|
|
14
|
+
|
|
15
|
+
const entries = fs.readdirSync(packagesDir, { withFileTypes: true });
|
|
16
|
+
const packages = [];
|
|
17
|
+
|
|
18
|
+
for (const entry of entries) {
|
|
19
|
+
if (!entry.isDirectory()) continue;
|
|
20
|
+
|
|
21
|
+
const packageMdPath = path.join(packagesDir, entry.name, 'package.md');
|
|
22
|
+
if (!fs.existsSync(packageMdPath)) continue;
|
|
23
|
+
|
|
24
|
+
const content = fs.readFileSync(packageMdPath, 'utf-8');
|
|
25
|
+
const metadata = parseFrontmatter(content) || {};
|
|
26
|
+
|
|
27
|
+
packages.push({
|
|
28
|
+
path: packageMdPath,
|
|
29
|
+
name: entry.name,
|
|
30
|
+
metadata,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return packages;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
module.exports = { findPackages };
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal YAML-like frontmatter parser (no npm dependencies).
|
|
3
|
+
* Supports: key: value, key: ~, simple arrays, one level of nested objects.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Parse frontmatter from markdown content.
|
|
8
|
+
* Finds the first block between --- delimiters (may appear after a H1).
|
|
9
|
+
* @param {string} content - The markdown file content
|
|
10
|
+
* @returns {object|null} Parsed frontmatter object, or null if not found
|
|
11
|
+
*/
|
|
12
|
+
function parseFrontmatter(content) {
|
|
13
|
+
const lines = content.split('\n');
|
|
14
|
+
|
|
15
|
+
let startIdx = -1;
|
|
16
|
+
let endIdx = -1;
|
|
17
|
+
|
|
18
|
+
for (let i = 0; i < lines.length; i++) {
|
|
19
|
+
if (lines[i].trim() === '---') {
|
|
20
|
+
if (startIdx === -1) {
|
|
21
|
+
startIdx = i;
|
|
22
|
+
} else {
|
|
23
|
+
endIdx = i;
|
|
24
|
+
break;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (startIdx === -1 || endIdx === -1) {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const yamlLines = lines.slice(startIdx + 1, endIdx);
|
|
34
|
+
return parseYaml(yamlLines);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Minimal YAML parser supporting:
|
|
39
|
+
* - key: value (strings, numbers, booleans)
|
|
40
|
+
* - key: ~ (null)
|
|
41
|
+
* - arrays (- item)
|
|
42
|
+
* - one level of nested objects
|
|
43
|
+
*/
|
|
44
|
+
function parseYaml(lines) {
|
|
45
|
+
const result = {};
|
|
46
|
+
let currentKey = null;
|
|
47
|
+
let currentArray = null;
|
|
48
|
+
let currentObject = null;
|
|
49
|
+
let inNestedObject = false;
|
|
50
|
+
|
|
51
|
+
for (const line of lines) {
|
|
52
|
+
// Skip empty lines and comments
|
|
53
|
+
if (line.trim() === '' || line.trim().startsWith('#')) {
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const indent = line.length - line.trimStart().length;
|
|
58
|
+
const trimmed = line.trim();
|
|
59
|
+
|
|
60
|
+
// Array item at indent level 2+ (belongs to current key)
|
|
61
|
+
if (trimmed.startsWith('- ') && indent >= 2 && currentKey) {
|
|
62
|
+
if (!currentArray) {
|
|
63
|
+
currentArray = [];
|
|
64
|
+
}
|
|
65
|
+
if (inNestedObject) {
|
|
66
|
+
inNestedObject = false;
|
|
67
|
+
currentObject = null;
|
|
68
|
+
}
|
|
69
|
+
currentArray.push(parseValue(trimmed.slice(2).trim()));
|
|
70
|
+
result[currentKey] = currentArray;
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Nested object key: value at indent level 2+
|
|
75
|
+
if (indent >= 2 && trimmed.includes(':') && currentKey && !trimmed.startsWith('- ')) {
|
|
76
|
+
if (!currentObject) {
|
|
77
|
+
currentObject = {};
|
|
78
|
+
inNestedObject = true;
|
|
79
|
+
}
|
|
80
|
+
const colonIdx = trimmed.indexOf(':');
|
|
81
|
+
const nestedKey = trimmed.slice(0, colonIdx).trim();
|
|
82
|
+
const nestedVal = trimmed.slice(colonIdx + 1).trim();
|
|
83
|
+
if (nestedVal !== '') {
|
|
84
|
+
currentObject[nestedKey] = parseValue(nestedVal);
|
|
85
|
+
}
|
|
86
|
+
result[currentKey] = currentObject;
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Top-level key: value
|
|
91
|
+
if (indent === 0 && trimmed.includes(':')) {
|
|
92
|
+
// Flush previous array/object
|
|
93
|
+
currentArray = null;
|
|
94
|
+
currentObject = null;
|
|
95
|
+
inNestedObject = false;
|
|
96
|
+
|
|
97
|
+
const colonIdx = trimmed.indexOf(':');
|
|
98
|
+
const key = trimmed.slice(0, colonIdx).trim();
|
|
99
|
+
const val = trimmed.slice(colonIdx + 1).trim();
|
|
100
|
+
|
|
101
|
+
currentKey = key;
|
|
102
|
+
|
|
103
|
+
if (val === '') {
|
|
104
|
+
// Could be followed by array or nested object — will be set by next lines
|
|
105
|
+
result[key] = null;
|
|
106
|
+
} else {
|
|
107
|
+
result[key] = parseValue(val);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return result;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Parse a single YAML value.
|
|
117
|
+
*/
|
|
118
|
+
function parseValue(val) {
|
|
119
|
+
if (val === '~' || val === 'null') return null;
|
|
120
|
+
if (val === 'true') return true;
|
|
121
|
+
if (val === 'false') return false;
|
|
122
|
+
if (/^-?\d+$/.test(val)) return parseInt(val, 10);
|
|
123
|
+
if (/^-?\d+\.\d+$/.test(val)) return parseFloat(val);
|
|
124
|
+
// Remove surrounding quotes if present
|
|
125
|
+
if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
|
|
126
|
+
return val.slice(1, -1);
|
|
127
|
+
}
|
|
128
|
+
// Handle inline arrays like [item1, item2]
|
|
129
|
+
if (val.startsWith('[') && val.endsWith(']')) {
|
|
130
|
+
return val.slice(1, -1).split(',').map(s => parseValue(s.trim()));
|
|
131
|
+
}
|
|
132
|
+
return val;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
module.exports = { parseFrontmatter };
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Hook: Validate frontmatter of package.md files after Write/Edit.
|
|
5
|
+
* Checks that status and other fields are valid.
|
|
6
|
+
*
|
|
7
|
+
* Exit 0 = OK, Exit 2 = validation error (blocks the tool use).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
const path = require('path');
|
|
12
|
+
const { getConfig } = require('./lib/config');
|
|
13
|
+
const { parseFrontmatter } = require('./lib/parse-frontmatter');
|
|
14
|
+
|
|
15
|
+
const VALID_STATUSES = ['pending', 'shaping', 'shaped', 'executing', 'executed', 'verifying', 'done'];
|
|
16
|
+
|
|
17
|
+
async function main() {
|
|
18
|
+
// Read stdin (PostToolUse JSON)
|
|
19
|
+
let input = '';
|
|
20
|
+
for await (const chunk of process.stdin) {
|
|
21
|
+
input += chunk;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
let data;
|
|
25
|
+
try {
|
|
26
|
+
data = JSON.parse(input);
|
|
27
|
+
} catch {
|
|
28
|
+
process.exit(0); // Can't parse input, skip
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const filePath = data.tool_input?.file_path || data.tool_input?.path || null;
|
|
32
|
+
if (!filePath) {
|
|
33
|
+
process.exit(0);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Check if this is a package.md in the project
|
|
37
|
+
const config = getConfig();
|
|
38
|
+
if (!config) {
|
|
39
|
+
process.exit(0); // No config, skip
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const packagesDir = path.join(config.projectPath, 'packages');
|
|
43
|
+
const normalizedFile = path.resolve(filePath);
|
|
44
|
+
const normalizedPackages = path.resolve(packagesDir);
|
|
45
|
+
|
|
46
|
+
// Must be inside {projectPath}/packages/*/package.md
|
|
47
|
+
if (!normalizedFile.startsWith(normalizedPackages)) {
|
|
48
|
+
process.exit(0);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (path.basename(normalizedFile) !== 'package.md') {
|
|
52
|
+
process.exit(0);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Read and parse the file
|
|
56
|
+
if (!fs.existsSync(normalizedFile)) {
|
|
57
|
+
process.exit(0);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const content = fs.readFileSync(normalizedFile, 'utf-8');
|
|
61
|
+
const frontmatter = parseFrontmatter(content);
|
|
62
|
+
|
|
63
|
+
if (!frontmatter) {
|
|
64
|
+
process.stderr.write('Frontmatter invalide : aucun bloc --- trouve dans package.md\n');
|
|
65
|
+
process.exit(2);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const errors = [];
|
|
69
|
+
|
|
70
|
+
// Validate status
|
|
71
|
+
if (!frontmatter.status) {
|
|
72
|
+
errors.push('Le champ "status" est absent du frontmatter');
|
|
73
|
+
} else if (!VALID_STATUSES.includes(frontmatter.status)) {
|
|
74
|
+
errors.push(`Status invalide: "${frontmatter.status}". Valeurs acceptees: ${VALID_STATUSES.join(', ')}`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Validate current_scope (string or null)
|
|
78
|
+
if (frontmatter.current_scope !== undefined && frontmatter.current_scope !== null && typeof frontmatter.current_scope !== 'string') {
|
|
79
|
+
errors.push(`current_scope doit etre une string ou null, recu: ${typeof frontmatter.current_scope}`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Validate scopes_completed (array or absent)
|
|
83
|
+
if (frontmatter.scopes_completed !== undefined && frontmatter.scopes_completed !== null && !Array.isArray(frontmatter.scopes_completed)) {
|
|
84
|
+
errors.push('scopes_completed doit etre un tableau');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Validate last_verification (object with date/result/failed_criteria or absent)
|
|
88
|
+
if (frontmatter.last_verification !== undefined && frontmatter.last_verification !== null) {
|
|
89
|
+
if (typeof frontmatter.last_verification !== 'object') {
|
|
90
|
+
errors.push('last_verification doit etre un objet avec date/result/failed_criteria');
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Validate verification_attempts (integer >= 0 or absent)
|
|
95
|
+
if (frontmatter.verification_attempts !== undefined && frontmatter.verification_attempts !== null) {
|
|
96
|
+
if (typeof frontmatter.verification_attempts !== 'number' || frontmatter.verification_attempts < 0 || !Number.isInteger(frontmatter.verification_attempts)) {
|
|
97
|
+
errors.push('verification_attempts doit etre un entier >= 0');
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (errors.length > 0) {
|
|
102
|
+
process.stderr.write(`Frontmatter invalide dans package.md:\n${errors.map(e => ` - ${e}`).join('\n')}\n`);
|
|
103
|
+
process.exit(2);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
process.exit(0);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
main();
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Hook: Validate status transitions in package.md files.
|
|
5
|
+
* Each agent type has its own set of valid transitions.
|
|
6
|
+
*
|
|
7
|
+
* Usage: node validate-transition.js --agent=shaper|executor|verifier
|
|
8
|
+
*
|
|
9
|
+
* Exit 0 = OK, Exit 2 = invalid transition (blocks the tool use).
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
const { execSync } = require('child_process');
|
|
15
|
+
const { getConfig } = require('./lib/config');
|
|
16
|
+
const { parseFrontmatter } = require('./lib/parse-frontmatter');
|
|
17
|
+
|
|
18
|
+
const VALID_TRANSITIONS = {
|
|
19
|
+
shaper: [
|
|
20
|
+
[null, 'pending'],
|
|
21
|
+
[null, 'shaping'],
|
|
22
|
+
['pending', 'shaping'],
|
|
23
|
+
['shaping', 'shaped'],
|
|
24
|
+
],
|
|
25
|
+
executor: [
|
|
26
|
+
['shaped', 'executing'],
|
|
27
|
+
['executing', 'executed'],
|
|
28
|
+
],
|
|
29
|
+
verifier: [
|
|
30
|
+
['executed', 'verifying'],
|
|
31
|
+
['verifying', 'done'],
|
|
32
|
+
['verifying', 'executing'],
|
|
33
|
+
],
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
function getAgentArg() {
|
|
37
|
+
for (const arg of process.argv.slice(2)) {
|
|
38
|
+
if (arg.startsWith('--agent=')) {
|
|
39
|
+
return arg.split('=')[1];
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function getOldStatus(filePath) {
|
|
46
|
+
try {
|
|
47
|
+
// Get the file content from HEAD (last committed version)
|
|
48
|
+
const relativePath = path.relative(process.cwd(), filePath).replace(/\\/g, '/');
|
|
49
|
+
const oldContent = execSync(`git show HEAD:${relativePath}`, {
|
|
50
|
+
encoding: 'utf-8',
|
|
51
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
52
|
+
});
|
|
53
|
+
const fm = parseFrontmatter(oldContent);
|
|
54
|
+
return fm ? fm.status || null : null;
|
|
55
|
+
} catch {
|
|
56
|
+
// File is new (not in git yet)
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function main() {
|
|
62
|
+
const agent = getAgentArg();
|
|
63
|
+
if (!agent || !VALID_TRANSITIONS[agent]) {
|
|
64
|
+
process.exit(0); // Unknown agent, skip
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Read stdin (PostToolUse JSON)
|
|
68
|
+
let input = '';
|
|
69
|
+
for await (const chunk of process.stdin) {
|
|
70
|
+
input += chunk;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
let data;
|
|
74
|
+
try {
|
|
75
|
+
data = JSON.parse(input);
|
|
76
|
+
} catch {
|
|
77
|
+
process.exit(0);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const filePath = data.tool_input?.file_path || data.tool_input?.path || null;
|
|
81
|
+
if (!filePath) {
|
|
82
|
+
process.exit(0);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Check if this is a package.md in the project
|
|
86
|
+
const config = getConfig();
|
|
87
|
+
if (!config) {
|
|
88
|
+
process.exit(0);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const packagesDir = path.join(config.projectPath, 'packages');
|
|
92
|
+
const normalizedFile = path.resolve(filePath);
|
|
93
|
+
const normalizedPackages = path.resolve(packagesDir);
|
|
94
|
+
|
|
95
|
+
if (!normalizedFile.startsWith(normalizedPackages)) {
|
|
96
|
+
process.exit(0);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (path.basename(normalizedFile) !== 'package.md') {
|
|
100
|
+
process.exit(0);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (!fs.existsSync(normalizedFile)) {
|
|
104
|
+
process.exit(0);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Get old and new status
|
|
108
|
+
const oldStatus = getOldStatus(normalizedFile);
|
|
109
|
+
const content = fs.readFileSync(normalizedFile, 'utf-8');
|
|
110
|
+
const newFm = parseFrontmatter(content);
|
|
111
|
+
const newStatus = newFm ? newFm.status || null : null;
|
|
112
|
+
|
|
113
|
+
// If status hasn't changed, it's fine
|
|
114
|
+
if (oldStatus === newStatus) {
|
|
115
|
+
process.exit(0);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Check if transition is valid
|
|
119
|
+
const transitions = VALID_TRANSITIONS[agent];
|
|
120
|
+
const isValid = transitions.some(([from, to]) => from === oldStatus && to === newStatus);
|
|
121
|
+
|
|
122
|
+
if (!isValid) {
|
|
123
|
+
const fromLabel = oldStatus || 'null';
|
|
124
|
+
const toLabel = newStatus || 'null';
|
|
125
|
+
const allowed = transitions.map(([f, t]) => `${f || 'null'} → ${t}`).join(', ');
|
|
126
|
+
process.stderr.write(
|
|
127
|
+
`Transition de status invalide pour ${agent}: ${fromLabel} → ${toLabel}\n` +
|
|
128
|
+
`Transitions autorisees: ${allowed}\n`
|
|
129
|
+
);
|
|
130
|
+
process.exit(2);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
process.exit(0);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
main();
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"hooks": {
|
|
3
|
+
"PostToolUse": [
|
|
4
|
+
{
|
|
5
|
+
"matcher": "Write|Edit",
|
|
6
|
+
"hooks": [
|
|
7
|
+
{
|
|
8
|
+
"type": "command",
|
|
9
|
+
"command": "node .claude/hooks/validate-frontmatter.js"
|
|
10
|
+
}
|
|
11
|
+
]
|
|
12
|
+
}
|
|
13
|
+
],
|
|
14
|
+
"SubagentStop": [
|
|
15
|
+
{
|
|
16
|
+
"matcher": "ship-executor|ship-verifier",
|
|
17
|
+
"hooks": [
|
|
18
|
+
{
|
|
19
|
+
"type": "command",
|
|
20
|
+
"command": "node .claude/hooks/check-agent-completion.js"
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
}
|
|
24
|
+
]
|
|
25
|
+
}
|
|
26
|
+
}
|
package/package.json
CHANGED
|
@@ -1,31 +1,33 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "cc-ship",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"description": "Architecture 3 couches pour Claude Code: Commands -> Agents -> Skills",
|
|
5
|
-
"bin": {
|
|
6
|
-
"cc-ship": "bin/install.js"
|
|
7
|
-
},
|
|
8
|
-
"files": [
|
|
9
|
-
"bin",
|
|
10
|
-
"commands",
|
|
11
|
-
"agents",
|
|
12
|
-
"skills"
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
"
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
-
"
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
"
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "cc-ship",
|
|
3
|
+
"version": "0.0.8",
|
|
4
|
+
"description": "Architecture 3 couches pour Claude Code: Commands -> Agents -> Skills",
|
|
5
|
+
"bin": {
|
|
6
|
+
"cc-ship": "bin/install.js"
|
|
7
|
+
},
|
|
8
|
+
"files": [
|
|
9
|
+
"bin",
|
|
10
|
+
"commands",
|
|
11
|
+
"agents",
|
|
12
|
+
"skills",
|
|
13
|
+
"hooks",
|
|
14
|
+
"hooks-settings.json"
|
|
15
|
+
],
|
|
16
|
+
"keywords": [
|
|
17
|
+
"claude",
|
|
18
|
+
"claude-code",
|
|
19
|
+
"ai",
|
|
20
|
+
"brainstorming",
|
|
21
|
+
"shaping",
|
|
22
|
+
"productivity"
|
|
23
|
+
],
|
|
24
|
+
"author": "",
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"repository": {
|
|
27
|
+
"type": "git",
|
|
28
|
+
"url": "https://github.com/vdstack/cc-ship"
|
|
29
|
+
},
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": ">=14.0.0"
|
|
32
|
+
}
|
|
33
|
+
}
|