@urielsh/prodify 0.1.1 → 0.1.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/README.md CHANGED
@@ -54,7 +54,7 @@ Use `$prodify-execute --auto` to continue without pausing between stages, or `$p
54
54
  5. Tell the agent to read `.prodify/AGENTS.md`.
55
55
  6. Run `$prodify-init`, then continue with `$prodify-execute` or `$prodify-execute --auto`.
56
56
 
57
- Prodify keeps repository initialization agent-agnostic. The active runtime is resolved when `$prodify-init` runs inside the opened agent, not when `prodify init` creates `.prodify/`.
57
+ Repo initialization stays agent-agnostic. The active runtime is resolved when `$prodify-init` runs inside the opened agent, not when `prodify init` creates `.prodify/`.
58
58
 
59
59
  ## How It Works
60
60
 
@@ -106,7 +106,7 @@ Stage order:
106
106
  - Stage outputs live under `.prodify/artifacts/`.
107
107
  - Skill definitions live under `.prodify/skills/`.
108
108
  - Local baseline, final, and delta scoring artifacts live under `.prodify/metrics/`.
109
- - Fresh product users do not need root-level agent files.
109
+ - No root-level agent files are required in the default product flow.
110
110
 
111
111
  ## Contracts And Validation
112
112
 
@@ -136,6 +136,8 @@ Prodify users and Prodify contributors follow different entrypoints:
136
136
  - Product users start from `.prodify/AGENTS.md` inside the repository they want to improve.
137
137
  - Contributors working on the Prodify source repository follow the root [AGENTS.md](/Users/urielsh/projects/prodify/AGENTS.md).
138
138
 
139
+ In this repository, root `AGENTS.md` is repository-local contributor guidance. `.prodify/AGENTS.md` remains the runtime entrypoint for product users.
140
+
139
141
  For this self-hosting repository, the checked-in repo-root `.prodify/` directory is a development workspace for Prodify itself, not a byte-for-byte snapshot of fresh `prodify init` output.
140
142
 
141
143
  Run the source-repo test suite with:
@@ -17,6 +17,9 @@ export async function runSetupAgentCommand(args, context) {
17
17
  context.stdout.write(`Status: ${result.alreadyConfigured ? 'already configured globally; refreshed' : 'configured globally'}\n`);
18
18
  context.stdout.write(`Configured agents: ${result.configuredAgents.join(', ')}\n`);
19
19
  context.stdout.write(`Registry: ${result.statePath}\n`);
20
+ if (result.installedPaths.length > 0) {
21
+ context.stdout.write(`Installed runtime commands: ${result.installedPaths.join(', ')}\n`);
22
+ }
20
23
  context.stdout.write('Repo impact: none\n');
21
24
  context.stdout.write('Next step: run `prodify init` in a repository, then open that agent and use `$prodify-init`.\n');
22
25
  return 0;
@@ -6,6 +6,117 @@ import { pathExists, writeFileEnsuringDir } from './fs.js';
6
6
  import { getRuntimeProfile } from './targets.js';
7
7
  export const GLOBAL_AGENT_SETUP_SCHEMA_VERSION = '1';
8
8
  export const PRODIFY_RUNTIME_COMMANDS = ['$prodify-init', '$prodify-execute', '$prodify-resume'];
9
+ function resolveCodexHome(env = process.env) {
10
+ const explicit = env.CODEX_HOME?.trim();
11
+ if (explicit) {
12
+ return path.resolve(explicit);
13
+ }
14
+ return path.join(os.homedir(), '.codex');
15
+ }
16
+ function resolveCodexSkillsRoot(env = process.env) {
17
+ return path.join(resolveCodexHome(env), 'skills');
18
+ }
19
+ function renderCodexSkill(name) {
20
+ if (name === 'prodify-init') {
21
+ return `---
22
+ name: "prodify-init"
23
+ description: "Bootstrap Prodify inside the current repository."
24
+ metadata:
25
+ short-description: "Bootstrap Prodify inside the current repository."
26
+ ---
27
+
28
+ <codex_skill_adapter>
29
+ ## A. Skill Invocation
30
+ - This skill is invoked by mentioning \`$prodify-init\`.
31
+ - Ignore trailing arguments unless the repository-specific runtime instructions require them.
32
+ </codex_skill_adapter>
33
+
34
+ # prodify-init
35
+
36
+ Use this runtime bridge to bootstrap Prodify inside the current repository.
37
+
38
+ Load and follow, in this order:
39
+ - \`.prodify/AGENTS.md\`
40
+ - \`.prodify/runtime-commands.md\`
41
+ - \`.prodify/state.json\`
42
+
43
+ Keep the runtime anchored to \`.prodify/\`.
44
+ Do not substitute compatibility files for the canonical \`.prodify/AGENTS.md\` entrypoint.
45
+
46
+ Available runtime commands:
47
+ - \`$prodify-init\`
48
+ - \`$prodify-execute\`
49
+ - \`$prodify-execute --auto\`
50
+ - \`$prodify-resume\`
51
+ `;
52
+ }
53
+ if (name === 'prodify-execute') {
54
+ return `---
55
+ name: "prodify-execute"
56
+ description: "Execute the next Prodify workflow stage inside the current repository."
57
+ metadata:
58
+ short-description: "Execute the next Prodify workflow stage."
59
+ ---
60
+
61
+ <codex_skill_adapter>
62
+ ## A. Skill Invocation
63
+ - This skill is invoked by mentioning \`$prodify-execute\`.
64
+ - Treat all user text after \`$prodify-execute\` as \`{{PRODIFY_EXECUTE_ARGS}}\`.
65
+ - If no arguments are present, treat \`{{PRODIFY_EXECUTE_ARGS}}\` as empty.
66
+ </codex_skill_adapter>
67
+
68
+ # prodify-execute
69
+
70
+ Use this runtime bridge to execute the next Prodify workflow stage.
71
+
72
+ Load and follow, in this order:
73
+ - \`.prodify/runtime-commands.md\`
74
+ - \`.prodify/state.json\`
75
+ - \`.prodify/AGENTS.md\`
76
+
77
+ Interpret \`{{PRODIFY_EXECUTE_ARGS}}\` as the runtime command arguments.
78
+ - empty: run \`$prodify-execute\`
79
+ - \`--auto\`: run \`$prodify-execute --auto\`
80
+
81
+ Keep all execution state, artifacts, contracts, and validation anchored to \`.prodify/\`.
82
+ `;
83
+ }
84
+ return `---
85
+ name: "prodify-resume"
86
+ description: "Resume a paused Prodify run from saved runtime state."
87
+ metadata:
88
+ short-description: "Resume a paused Prodify run."
89
+ ---
90
+
91
+ <codex_skill_adapter>
92
+ ## A. Skill Invocation
93
+ - This skill is invoked by mentioning \`$prodify-resume\`.
94
+ - Treat trailing arguments as optional runtime-specific hints only when the repository guidance explicitly supports them.
95
+ </codex_skill_adapter>
96
+
97
+ # prodify-resume
98
+
99
+ Use this runtime bridge to resume Prodify from saved runtime state.
100
+
101
+ Load and follow, in this order:
102
+ - \`.prodify/runtime-commands.md\`
103
+ - \`.prodify/state.json\`
104
+ - \`.prodify/AGENTS.md\`
105
+
106
+ Resume from the current state recorded under \`.prodify/state.json\`.
107
+ Preserve validation checkpoints and stop clearly if the state is corrupt or non-resumable.
108
+ `;
109
+ }
110
+ async function installCodexRuntimeCommands(env = process.env) {
111
+ const skillsRoot = resolveCodexSkillsRoot(env);
112
+ const installedFiles = [];
113
+ for (const command of ['prodify-init', 'prodify-execute', 'prodify-resume']) {
114
+ const skillPath = path.join(skillsRoot, command, 'SKILL.md');
115
+ await writeFileEnsuringDir(skillPath, renderCodexSkill(command));
116
+ installedFiles.push(skillPath);
117
+ }
118
+ return installedFiles;
119
+ }
9
120
  function asRecord(value) {
10
121
  return typeof value === 'object' && value !== null ? value : {};
11
122
  }
@@ -102,10 +213,14 @@ export async function setupAgentIntegration(agent, { now = new Date().toISOStrin
102
213
  configured_at: now,
103
214
  commands: [...PRODIFY_RUNTIME_COMMANDS]
104
215
  };
216
+ const installedPaths = profile.name === 'codex'
217
+ ? await installCodexRuntimeCommands(env)
218
+ : [];
105
219
  const statePath = await writeGlobalAgentSetupState(nextState, { env });
106
220
  return {
107
221
  statePath,
108
222
  configuredAgents: listConfiguredAgents(nextState),
109
- alreadyConfigured
223
+ alreadyConfigured,
224
+ installedPaths
110
225
  };
111
226
  }
@@ -39,15 +39,24 @@ export async function resolveRepoRoot(options = {}) {
39
39
  }
40
40
  return explicitRepo;
41
41
  }
42
- const prodifyRoot = await searchUpwards(cwd, async (candidate) => directoryHas(candidate, '.prodify'));
43
- if (prodifyRoot) {
44
- return prodifyRoot;
45
- }
46
- if (allowBootstrap) {
47
- const gitRoot = await searchUpwards(cwd, async (candidate) => directoryHas(candidate, '.git'));
48
- if (gitRoot) {
49
- return gitRoot;
42
+ let current = cwd;
43
+ while (true) {
44
+ const hasProdify = await directoryHas(current, '.prodify');
45
+ if (hasProdify) {
46
+ return current;
47
+ }
48
+ const hasGit = await directoryHas(current, '.git');
49
+ if (hasGit) {
50
+ if (allowBootstrap) {
51
+ return current;
52
+ }
53
+ break;
50
54
  }
55
+ const parent = path.dirname(current);
56
+ if (parent === current) {
57
+ break;
58
+ }
59
+ current = parent;
51
60
  }
52
61
  throw new ProdifyError('Could not resolve repository root from the current working directory.', {
53
62
  code: 'REPO_ROOT_NOT_FOUND'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@urielsh/prodify",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "license": "Apache-2.0",
5
5
  "private": false,
6
6
  "type": "module",
@@ -22,6 +22,9 @@ export async function runSetupAgentCommand(args: string[], context: CommandConte
22
22
  context.stdout.write(`Status: ${result.alreadyConfigured ? 'already configured globally; refreshed' : 'configured globally'}\n`);
23
23
  context.stdout.write(`Configured agents: ${result.configuredAgents.join(', ')}\n`);
24
24
  context.stdout.write(`Registry: ${result.statePath}\n`);
25
+ if (result.installedPaths.length > 0) {
26
+ context.stdout.write(`Installed runtime commands: ${result.installedPaths.join(', ')}\n`);
27
+ }
25
28
  context.stdout.write('Repo impact: none\n');
26
29
  context.stdout.write('Next step: run `prodify init` in a repository, then open that agent and use `$prodify-init`.\n');
27
30
  return 0;
@@ -10,6 +10,126 @@ import type { GlobalAgentSetupState, RuntimeProfileName } from '../types.js';
10
10
  export const GLOBAL_AGENT_SETUP_SCHEMA_VERSION = '1';
11
11
  export const PRODIFY_RUNTIME_COMMANDS = ['$prodify-init', '$prodify-execute', '$prodify-resume'] as const;
12
12
 
13
+ function resolveCodexHome(env: NodeJS.ProcessEnv = process.env): string {
14
+ const explicit = env.CODEX_HOME?.trim();
15
+ if (explicit) {
16
+ return path.resolve(explicit);
17
+ }
18
+
19
+ return path.join(os.homedir(), '.codex');
20
+ }
21
+
22
+ function resolveCodexSkillsRoot(env: NodeJS.ProcessEnv = process.env): string {
23
+ return path.join(resolveCodexHome(env), 'skills');
24
+ }
25
+
26
+ function renderCodexSkill(name: 'prodify-init' | 'prodify-execute' | 'prodify-resume'): string {
27
+ if (name === 'prodify-init') {
28
+ return `---
29
+ name: "prodify-init"
30
+ description: "Bootstrap Prodify inside the current repository."
31
+ metadata:
32
+ short-description: "Bootstrap Prodify inside the current repository."
33
+ ---
34
+
35
+ <codex_skill_adapter>
36
+ ## A. Skill Invocation
37
+ - This skill is invoked by mentioning \`$prodify-init\`.
38
+ - Ignore trailing arguments unless the repository-specific runtime instructions require them.
39
+ </codex_skill_adapter>
40
+
41
+ # prodify-init
42
+
43
+ Use this runtime bridge to bootstrap Prodify inside the current repository.
44
+
45
+ Load and follow, in this order:
46
+ - \`.prodify/AGENTS.md\`
47
+ - \`.prodify/runtime-commands.md\`
48
+ - \`.prodify/state.json\`
49
+
50
+ Keep the runtime anchored to \`.prodify/\`.
51
+ Do not substitute compatibility files for the canonical \`.prodify/AGENTS.md\` entrypoint.
52
+
53
+ Available runtime commands:
54
+ - \`$prodify-init\`
55
+ - \`$prodify-execute\`
56
+ - \`$prodify-execute --auto\`
57
+ - \`$prodify-resume\`
58
+ `;
59
+ }
60
+
61
+ if (name === 'prodify-execute') {
62
+ return `---
63
+ name: "prodify-execute"
64
+ description: "Execute the next Prodify workflow stage inside the current repository."
65
+ metadata:
66
+ short-description: "Execute the next Prodify workflow stage."
67
+ ---
68
+
69
+ <codex_skill_adapter>
70
+ ## A. Skill Invocation
71
+ - This skill is invoked by mentioning \`$prodify-execute\`.
72
+ - Treat all user text after \`$prodify-execute\` as \`{{PRODIFY_EXECUTE_ARGS}}\`.
73
+ - If no arguments are present, treat \`{{PRODIFY_EXECUTE_ARGS}}\` as empty.
74
+ </codex_skill_adapter>
75
+
76
+ # prodify-execute
77
+
78
+ Use this runtime bridge to execute the next Prodify workflow stage.
79
+
80
+ Load and follow, in this order:
81
+ - \`.prodify/runtime-commands.md\`
82
+ - \`.prodify/state.json\`
83
+ - \`.prodify/AGENTS.md\`
84
+
85
+ Interpret \`{{PRODIFY_EXECUTE_ARGS}}\` as the runtime command arguments.
86
+ - empty: run \`$prodify-execute\`
87
+ - \`--auto\`: run \`$prodify-execute --auto\`
88
+
89
+ Keep all execution state, artifacts, contracts, and validation anchored to \`.prodify/\`.
90
+ `;
91
+ }
92
+
93
+ return `---
94
+ name: "prodify-resume"
95
+ description: "Resume a paused Prodify run from saved runtime state."
96
+ metadata:
97
+ short-description: "Resume a paused Prodify run."
98
+ ---
99
+
100
+ <codex_skill_adapter>
101
+ ## A. Skill Invocation
102
+ - This skill is invoked by mentioning \`$prodify-resume\`.
103
+ - Treat trailing arguments as optional runtime-specific hints only when the repository guidance explicitly supports them.
104
+ </codex_skill_adapter>
105
+
106
+ # prodify-resume
107
+
108
+ Use this runtime bridge to resume Prodify from saved runtime state.
109
+
110
+ Load and follow, in this order:
111
+ - \`.prodify/runtime-commands.md\`
112
+ - \`.prodify/state.json\`
113
+ - \`.prodify/AGENTS.md\`
114
+
115
+ Resume from the current state recorded under \`.prodify/state.json\`.
116
+ Preserve validation checkpoints and stop clearly if the state is corrupt or non-resumable.
117
+ `;
118
+ }
119
+
120
+ async function installCodexRuntimeCommands(env: NodeJS.ProcessEnv = process.env): Promise<string[]> {
121
+ const skillsRoot = resolveCodexSkillsRoot(env);
122
+ const installedFiles: string[] = [];
123
+
124
+ for (const command of ['prodify-init', 'prodify-execute', 'prodify-resume'] as const) {
125
+ const skillPath = path.join(skillsRoot, command, 'SKILL.md');
126
+ await writeFileEnsuringDir(skillPath, renderCodexSkill(command));
127
+ installedFiles.push(skillPath);
128
+ }
129
+
130
+ return installedFiles;
131
+ }
132
+
13
133
  function asRecord(value: unknown): Record<string, unknown> {
14
134
  return typeof value === 'object' && value !== null ? value as Record<string, unknown> : {};
15
135
  }
@@ -116,7 +236,7 @@ export async function setupAgentIntegration(
116
236
  now?: string;
117
237
  env?: NodeJS.ProcessEnv;
118
238
  } = {}
119
- ): Promise<{ statePath: string; configuredAgents: RuntimeProfileName[]; alreadyConfigured: boolean }> {
239
+ ): Promise<{ statePath: string; configuredAgents: RuntimeProfileName[]; alreadyConfigured: boolean; installedPaths: string[] }> {
120
240
  const profile = getRuntimeProfile(agent);
121
241
  if (!profile) {
122
242
  throw new ProdifyError('setup-agent requires <codex|claude|copilot|opencode>.', {
@@ -138,10 +258,14 @@ export async function setupAgentIntegration(
138
258
  commands: [...PRODIFY_RUNTIME_COMMANDS]
139
259
  };
140
260
 
261
+ const installedPaths = profile.name === 'codex'
262
+ ? await installCodexRuntimeCommands(env)
263
+ : [];
141
264
  const statePath = await writeGlobalAgentSetupState(nextState, { env });
142
265
  return {
143
266
  statePath,
144
267
  configuredAgents: listConfiguredAgents(nextState),
145
- alreadyConfigured
268
+ alreadyConfigured,
269
+ installedPaths
146
270
  };
147
271
  }
@@ -56,16 +56,29 @@ export async function resolveRepoRoot(options: ResolveRepoRootOptions = {}): Pro
56
56
  return explicitRepo;
57
57
  }
58
58
 
59
- const prodifyRoot = await searchUpwards(cwd, async (candidate) => directoryHas(candidate, '.prodify'));
60
- if (prodifyRoot) {
61
- return prodifyRoot;
62
- }
59
+ let current = cwd;
60
+
61
+ while (true) {
62
+ const hasProdify = await directoryHas(current, '.prodify');
63
+ if (hasProdify) {
64
+ return current;
65
+ }
66
+
67
+ const hasGit = await directoryHas(current, '.git');
68
+ if (hasGit) {
69
+ if (allowBootstrap) {
70
+ return current;
71
+ }
63
72
 
64
- if (allowBootstrap) {
65
- const gitRoot = await searchUpwards(cwd, async (candidate) => directoryHas(candidate, '.git'));
66
- if (gitRoot) {
67
- return gitRoot;
73
+ break;
68
74
  }
75
+
76
+ const parent = path.dirname(current);
77
+ if (parent === current) {
78
+ break;
79
+ }
80
+
81
+ current = parent;
69
82
  }
70
83
 
71
84
  throw new ProdifyError('Could not resolve repository root from the current working directory.', {
@@ -60,6 +60,19 @@ test('init creates only .prodify-owned runtime scaffolding', async () => {
60
60
  await assertMissing(repoRoot, '.opencode/AGENTS.md');
61
61
  });
62
62
 
63
+ test('init prefers the current git repo over an unrelated ancestor .prodify workspace', async () => {
64
+ const outerRepo = await createTempRepo('prodify-outer-');
65
+ await execCli(outerRepo, ['init']);
66
+
67
+ const nestedRepo = path.join(outerRepo, 'nested-app');
68
+ await fs.mkdir(path.join(nestedRepo, '.git'), { recursive: true });
69
+
70
+ const result = await execCli(nestedRepo, ['init']);
71
+
72
+ assert.equal(result.exitCode, 0);
73
+ await fs.access(path.join(nestedRepo, '.prodify', 'state.json'));
74
+ });
75
+
63
76
  test('status becomes the primary user-facing summary after init', async () => {
64
77
  const repoRoot = await createTempRepo();
65
78
  await execCli(repoRoot, ['init']);
@@ -85,14 +98,19 @@ test('status becomes the primary user-facing summary after init', async () => {
85
98
 
86
99
  test('setup-agent configures a supported agent globally without repo-local writes', async () => {
87
100
  const cwd = await createTempDir();
101
+ process.env.CODEX_HOME = path.join(cwd, '.codex-home');
88
102
 
89
103
  const result = await execCli(cwd, ['setup-agent', 'codex']);
90
104
 
91
105
  assert.equal(result.exitCode, 0);
92
106
  assert.match(result.stdout, /Prodify Agent Setup/);
93
107
  assert.match(result.stdout, /Agent: codex/);
108
+ assert.match(result.stdout, /Installed runtime commands:/);
94
109
  assert.match(result.stdout, /Repo impact: none/);
95
110
  await fs.access(path.join(cwd, '.prodify-home', 'agent-setup.json'));
111
+ await fs.access(path.join(cwd, '.codex-home', 'skills', 'prodify-init', 'SKILL.md'));
112
+ await fs.access(path.join(cwd, '.codex-home', 'skills', 'prodify-execute', 'SKILL.md'));
113
+ await fs.access(path.join(cwd, '.codex-home', 'skills', 'prodify-resume', 'SKILL.md'));
96
114
  await assertMissing(cwd, '.prodify');
97
115
  });
98
116
 
@@ -17,10 +17,11 @@ test('global agent setup registers multiple agents without repo-local state', as
17
17
  const root = await createTempDir();
18
18
  const env = {
19
19
  ...process.env,
20
- PRODIFY_HOME: path.join(root, '.prodify-home')
20
+ PRODIFY_HOME: path.join(root, '.prodify-home'),
21
+ CODEX_HOME: path.join(root, '.codex-home')
21
22
  };
22
23
 
23
- await setupAgentIntegration('codex', {
24
+ const codexResult = await setupAgentIntegration('codex', {
24
25
  now: '2026-04-04T00:00:00.000Z',
25
26
  env
26
27
  });
@@ -34,7 +35,11 @@ test('global agent setup registers multiple agents without repo-local state', as
34
35
  });
35
36
 
36
37
  assert.deepEqual(listConfiguredAgents(state), ['claude', 'codex']);
38
+ assert.equal(codexResult.installedPaths.length, 3);
37
39
  await fs.access(resolveGlobalAgentSetupStatePath(env));
40
+ assert.match(await fs.readFile(path.join(root, '.codex-home', 'skills', 'prodify-init', 'SKILL.md'), 'utf8'), /\$prodify-init/);
41
+ assert.match(await fs.readFile(path.join(root, '.codex-home', 'skills', 'prodify-execute', 'SKILL.md'), 'utf8'), /\$prodify-execute/);
42
+ assert.match(await fs.readFile(path.join(root, '.codex-home', 'skills', 'prodify-resume', 'SKILL.md'), 'utf8'), /\$prodify-resume/);
38
43
  await assert.rejects(fs.access(path.join(root, '.prodify')));
39
44
  });
40
45
 
@@ -67,7 +72,8 @@ test('runtime agent binding rejects ambiguous or missing global setup', async ()
67
72
  const root = await createTempDir();
68
73
  const env = {
69
74
  ...process.env,
70
- PRODIFY_HOME: path.join(root, '.prodify-home')
75
+ PRODIFY_HOME: path.join(root, '.prodify-home'),
76
+ CODEX_HOME: path.join(root, '.codex-home')
71
77
  };
72
78
 
73
79
  assert.deepEqual(listConfiguredAgents(createInitialGlobalAgentSetupState()), []);