dev-flow-codex 0.1.0

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/lib/paths.mjs ADDED
@@ -0,0 +1,170 @@
1
+ import { chmod, lstat, mkdir, realpath, stat } from "node:fs/promises";
2
+ import { homedir } from "node:os";
3
+ import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+
6
+ export const DATA_DIRECTORY_ENVIRONMENT = "DEV_FLOW_DATA_DIR";
7
+ export const SUPPORTED_RUNTIME_KEY = "darwin-arm64";
8
+
9
+ export function packageRootFromModule(moduleUrl = import.meta.url) {
10
+ return dirname(dirname(fileURLToPath(moduleUrl)));
11
+ }
12
+
13
+ export async function resolveProductPaths({
14
+ packageRoot = packageRootFromModule(),
15
+ homeDirectory = homedir(),
16
+ platform = process.platform,
17
+ arch = process.arch,
18
+ environment = process.env,
19
+ } = {}) {
20
+ const runtimeKey = `${platform}-${arch}`;
21
+ if (runtimeKey !== SUPPORTED_RUNTIME_KEY) {
22
+ throw new Error(`unsupported platform ${runtimeKey}; Feature 003 supports ${SUPPORTED_RUNTIME_KEY}`);
23
+ }
24
+
25
+ const canonicalPackageRoot = await canonicalExistingDirectory(packageRoot, "package root");
26
+ const canonicalHome = await canonicalExistingDirectory(homeDirectory, "home directory");
27
+ const productSupportRoot = join(
28
+ canonicalHome,
29
+ "Library",
30
+ "Application Support",
31
+ "dev-flow",
32
+ );
33
+ await assertNoSymlinkComponents(canonicalHome, productSupportRoot);
34
+
35
+ const runtimePath = containedPath(
36
+ canonicalPackageRoot,
37
+ join(canonicalPackageRoot, "runtime", runtimeKey, "dev-flow"),
38
+ "runtime",
39
+ );
40
+ const pluginRoot = containedPath(
41
+ canonicalPackageRoot,
42
+ join(canonicalPackageRoot, "plugin"),
43
+ "plugin",
44
+ );
45
+ const registrationsDirectory = containedPath(
46
+ productSupportRoot,
47
+ join(productSupportRoot, "registrations"),
48
+ "registration directory",
49
+ );
50
+ const receiptPath = containedPath(
51
+ productSupportRoot,
52
+ join(registrationsDirectory, "codex.json"),
53
+ "receipt",
54
+ );
55
+
56
+ const explicitDataDirectory = environment?.[DATA_DIRECTORY_ENVIRONMENT] ?? "";
57
+ let dataDirectory;
58
+ let usesDefaultDataDirectory;
59
+ if (explicitDataDirectory !== "") {
60
+ if (!isAbsolute(explicitDataDirectory)) {
61
+ throw new Error(`${DATA_DIRECTORY_ENVIRONMENT} must be an absolute path`);
62
+ }
63
+ const normalized = resolve(explicitDataDirectory);
64
+ let canonical;
65
+ try {
66
+ canonical = await realpath(explicitDataDirectory);
67
+ const info = await stat(canonical);
68
+ if (!info.isDirectory()) {
69
+ throw new Error("not a directory");
70
+ }
71
+ } catch (error) {
72
+ throw new Error(`${DATA_DIRECTORY_ENVIRONMENT} must name an existing directory`, {
73
+ cause: error,
74
+ });
75
+ }
76
+ if (canonical !== normalized) {
77
+ throw new Error(`${DATA_DIRECTORY_ENVIRONMENT} must be canonical and may not use a symbolic link`);
78
+ }
79
+ dataDirectory = canonical;
80
+ usesDefaultDataDirectory = false;
81
+ } else {
82
+ dataDirectory = containedPath(
83
+ productSupportRoot,
84
+ join(productSupportRoot, "data"),
85
+ "default data directory",
86
+ );
87
+ usesDefaultDataDirectory = true;
88
+ }
89
+
90
+ return Object.freeze({
91
+ packageRoot: canonicalPackageRoot,
92
+ marketplaceRoot: canonicalPackageRoot,
93
+ pluginRoot,
94
+ runtimePath,
95
+ homeDirectory: canonicalHome,
96
+ productSupportRoot,
97
+ registrationsDirectory,
98
+ receiptPath,
99
+ dataDirectory,
100
+ usesDefaultDataDirectory,
101
+ runtimeKey,
102
+ });
103
+ }
104
+
105
+ export async function ensureDefaultDataDirectory(paths) {
106
+ if (!paths?.usesDefaultDataDirectory) {
107
+ throw new Error("refusing to create an explicit data directory");
108
+ }
109
+ const expected = containedPath(
110
+ paths.productSupportRoot,
111
+ join(paths.productSupportRoot, "data"),
112
+ "default data directory",
113
+ );
114
+ if (paths.dataDirectory !== expected) {
115
+ throw new Error("default data directory does not match the product-owned path");
116
+ }
117
+ await assertNoSymlinkComponents(paths.homeDirectory, paths.productSupportRoot);
118
+ await mkdir(paths.dataDirectory, { recursive: true, mode: 0o700 });
119
+ await assertNoSymlinkComponents(paths.homeDirectory, paths.dataDirectory);
120
+ await chmod(paths.dataDirectory, 0o700);
121
+ return paths.dataDirectory;
122
+ }
123
+
124
+ export function containedPath(root, candidate, label = "path") {
125
+ const canonicalRoot = resolve(root);
126
+ const canonicalCandidate = resolve(candidate);
127
+ const offset = relative(canonicalRoot, canonicalCandidate);
128
+ if (offset === ".." || offset.startsWith(`..${sep}`) || isAbsolute(offset)) {
129
+ throw new Error(`${label} escapes its owned root`);
130
+ }
131
+ return canonicalCandidate;
132
+ }
133
+
134
+ async function canonicalExistingDirectory(path, label) {
135
+ if (!isAbsolute(path)) {
136
+ throw new Error(`${label} must be absolute`);
137
+ }
138
+ let canonical;
139
+ try {
140
+ canonical = await realpath(path);
141
+ const info = await stat(canonical);
142
+ if (!info.isDirectory()) {
143
+ throw new Error("not a directory");
144
+ }
145
+ } catch (error) {
146
+ throw new Error(`${label} must name an existing directory`, { cause: error });
147
+ }
148
+ return canonical;
149
+ }
150
+
151
+ async function assertNoSymlinkComponents(root, candidate) {
152
+ const canonicalRoot = resolve(root);
153
+ const canonicalCandidate = containedPath(canonicalRoot, candidate, "product path");
154
+ const components = relative(canonicalRoot, canonicalCandidate).split(sep).filter(Boolean);
155
+ let current = canonicalRoot;
156
+ for (const component of components) {
157
+ current = join(current, component);
158
+ try {
159
+ const info = await lstat(current);
160
+ if (info.isSymbolicLink()) {
161
+ throw new Error(`product path contains a symbolic link: ${current}`);
162
+ }
163
+ } catch (error) {
164
+ if (error?.code === "ENOENT") {
165
+ return;
166
+ }
167
+ throw error;
168
+ }
169
+ }
170
+ }
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "dev-flow-codex",
3
+ "version": "0.1.0",
4
+ "private": false,
5
+ "description": "Explicit-only Dev Flow integration for Codex CLI.",
6
+ "license": "Apache-2.0",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/Innocent-children/dev-flow.git",
10
+ "directory": "packages/codex"
11
+ },
12
+ "type": "module",
13
+ "os": [
14
+ "darwin"
15
+ ],
16
+ "cpu": [
17
+ "arm64"
18
+ ],
19
+ "publishConfig": {
20
+ "access": "public",
21
+ "registry": "https://registry.npmjs.org/"
22
+ },
23
+ "engines": {
24
+ "node": ">=24"
25
+ },
26
+ "bin": {
27
+ "dev-flow-codex": "bin/dev-flow-codex.mjs"
28
+ },
29
+ "files": [
30
+ ".agents/plugins/marketplace.json",
31
+ "LICENSE",
32
+ "bin/dev-flow-codex.mjs",
33
+ "lib/lifecycle.mjs",
34
+ "lib/paths.mjs",
35
+ "plugin/.codex-plugin/plugin.json",
36
+ "plugin/.mcp.json",
37
+ "plugin/skills/dev-flow/SKILL.md",
38
+ "plugin/skills/dev-flow/agents/openai.yaml",
39
+ "runtime/darwin-arm64/dev-flow"
40
+ ],
41
+ "scripts": {
42
+ "test": "node --test tests/*.test.mjs",
43
+ "test:package": "node --test tests/package-contract.test.mjs",
44
+ "test:lifecycle": "node --test tests/lifecycle.test.mjs",
45
+ "test:parser": "node --test tests/journey-evidence.test.mjs",
46
+ "test:native-smoke": "node --test tests/journey-harness.test.mjs",
47
+ "pack:dry": "pnpm pack --dry-run --json",
48
+ "build:local": "../../scripts/build-codex-local.sh",
49
+ "smoke:fixture": "../../scripts/run-codex-real-journey.sh --fixture success"
50
+ }
51
+ }
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "dev-flow-codex",
3
+ "version": "0.1.0",
4
+ "description": "Explicit-only Dev Flow integration for Codex CLI.",
5
+ "author": {
6
+ "name": "Dev Flow"
7
+ },
8
+ "license": "Apache-2.0",
9
+ "skills": "./skills/",
10
+ "mcpServers": "./.mcp.json",
11
+ "interface": {
12
+ "displayName": "Dev Flow for Codex",
13
+ "shortDescription": "Explicit Dev Flow workflow for Codex",
14
+ "longDescription": "Start or resume one Core-governed repository task only through an explicit $dev-flow-codex:dev-flow invocation.",
15
+ "developerName": "Dev Flow",
16
+ "category": "Productivity",
17
+ "capabilities": [
18
+ "Read",
19
+ "Write"
20
+ ],
21
+ "defaultPrompt": [
22
+ "$dev-flow-codex:dev-flow implement the requested change in this repository."
23
+ ]
24
+ }
25
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "$schema": "https://agent-plugins.org/schemas/1.0.0/mcp.schema.json",
3
+ "mcpServers": {
4
+ "dev-flow": {
5
+ "type": "stdio",
6
+ "command": "dev-flow-codex",
7
+ "args": [
8
+ "mcp"
9
+ ]
10
+ }
11
+ }
12
+ }
@@ -0,0 +1,243 @@
1
+ ---
2
+ name: dev-flow
3
+ description: "Explicit-only Dev Flow entry point for Codex. Use only when the current user turn contains $dev-flow-codex:dev-flow; never select this Skill implicitly."
4
+ ---
5
+
6
+ # Dev Flow
7
+
8
+ This Skill is a thin admission layer for the shared Dev Flow Core. It does not own task state,
9
+ workflow transitions, recovery decisions, verification budgets, or completion.
10
+
11
+ ## Admission gate
12
+
13
+ Perform every check below locally and in order before any Core or Dev Flow tool call.
14
+
15
+ The Skill resource/base name is `dev-flow`; the installed Skill full name is `dev-flow-codex:dev-flow`.
16
+ The only exact explicit selector is `$dev-flow-codex:dev-flow`.
17
+ Bare `$dev-flow` is not an alias and does not select this installed Skill. A wrong plugin namespace,
18
+ a wrong Skill base name, or a missing selector also does not select it.
19
+ Codex 0.147 may expose this plugin's MCP tools independently from Skill injection. This Skill does
20
+ not claim selector-bound tool visibility or authorization.
21
+
22
+ 1. Require the exact standalone `$dev-flow-codex:dev-flow` selector in the current user turn. Do not infer the
23
+ selector from earlier turns, repository contents, or a request that merely discusses Dev Flow.
24
+ If it is absent, do not treat the turn as Skill activation and do not make a task-bearing Dev Flow
25
+ call. Never activate this Skill implicitly.
26
+ 2. After removing the selector, accept either one substantive, bounded requirement for the current
27
+ repository or an explicit request to resume its compatible active Codex task. Reject an empty or
28
+ conversational invocation before any Core call.
29
+ 3. Use read-only Git inspection to resolve one current Git worktree and its canonical root. Preserve
30
+ spaces, Unicode, symlinks, and subdirectory invocation as one path value; do not concatenate a
31
+ shell command.
32
+ 4. Reject work that needs another repository, multiple repositories, or a repository that cannot be
33
+ resolved. Preserve repository instructions and current user authority when checking whether the
34
+ requested work is permitted.
35
+
36
+ If any admission check fails, explain the missing precondition and stop before Skill-owned task
37
+ discovery. Do not complete a task-bearing call or create adapter state. Host-exposed read-only or
38
+ Core-rejected calls are not activation and must be reported honestly.
39
+
40
+ ## Compatibility handshake
41
+
42
+ Only after every admission check passes, call `dev_flow_server_info({})`. It must be the first Dev
43
+ Flow tool call. Require the complete structured result to establish all of the following:
44
+
45
+ - product is exactly `dev-flow`;
46
+ - Core version equals the packaged product version;
47
+ - schema identifies Core Contract `0.1`;
48
+ - transport is exactly `stdio` and health is exactly `ready`;
49
+ - the supported host set contains `codex`;
50
+ - the reported tool catalog contains exactly these six raw names, in this order:
51
+
52
+ 1. `dev_flow_server_info`
53
+ 2. `dev_flow_open_task`
54
+ 3. `dev_flow_get_task`
55
+ 4. `dev_flow_get_next_action`
56
+ 5. `dev_flow_apply_action`
57
+ 6. `dev_flow_cancel_task`
58
+
59
+ An incomplete, truncated, malformed, incompatible, missing, additional, or reordered catalog is a
60
+ failed handshake. Stop without probing an undocumented tool or continuing to task discovery.
61
+
62
+ ## Task discovery
63
+
64
+ After the handshake, call `dev_flow_open_task` with `host=codex` and the canonical current
65
+ worktree.
66
+
67
+ - For an explicit resume request, omit `new_task` and let Core select the unique compatible active
68
+ Codex task.
69
+ - For a new substantive request, provide a bounded contract derived only from the current user
70
+ request, repository instructions, stated exclusions, observable acceptance criteria, and the
71
+ granted verification authority.
72
+ - Forward `new_task` with exactly the members `goal`, `scope`, `out_of_scope`,
73
+ `acceptance_criteria`, and `verification_budget`, with no additional members. Forward
74
+ `verification_budget` with exactly `level`, `max_automatic_commands`, `allow_full_suite`, and
75
+ `allow_manual_handoff`; do not invent aliases for any of these Core-declared fields.
76
+ - Ask before opening a new task if a material goal, scope, acceptance, or verification choice
77
+ cannot be derived without changing user intent.
78
+ - Let Core decide whether an exactly compatible contract creates or resumes a task. Never choose,
79
+ merge, or take over task records locally.
80
+
81
+ The complete open result is authoritative. Report a Core ownership or contract conflict unchanged
82
+ in meaning and stop.
83
+
84
+ ## Governed action loop
85
+
86
+ For an active task, perform each iteration in this order:
87
+
88
+ 1. Obtain one complete, fresh action from the open result or `dev_flow_get_next_action`.
89
+ 2. Treat the returned task ID, revision, action ID, action kind, repository-binding digest, allowed
90
+ effects, required evidence, payload schema, guidance, blocker, and outcome as one inseparable
91
+ Core result.
92
+ 3. Stop when that result reports a blocker or terminal outcome.
93
+ 4. Perform only the current action's allowed effects, under the repository instructions and
94
+ current user authority.
95
+ 5. Count verification commands and label evidence by how it actually ran.
96
+ 6. Build only the closed payload requested by the returned payload schema.
97
+ 7. Before dispatch, generate and retain an opaque request ID with the exact identity and payload.
98
+ 8. Submit exactly one mutation through `dev_flow_apply_action` using that retained material.
99
+ 9. After a complete successful mutation, continue only from the returned authoritative next action
100
+ or outcome, or make one fresh Core read before doing more work.
101
+
102
+ Do not infer a transition, reinterpret an error, or decide completion from repository contents or
103
+ host judgment.
104
+
105
+ ## Closed forwarding contract
106
+
107
+ For every mutation, forward `host=codex`; the task ID and revision from the same fresh result; the
108
+ exact action ID, action kind, and repository-binding digest; one caller-generated request ID; and a
109
+ closed payload containing only fields allowed by Core's returned schema. Do not add unknown fields,
110
+ aliases, command logs, environment dumps, inferred status, or locally invented recovery flags.
111
+
112
+ Use the fresh action's `payload_contract` as the discriminator for the corresponding closed schema
113
+ branch in the `dev_flow_apply_action` input. `required_evidence` names describe obligations; they are
114
+ not payload field names. If you cannot identify and read that exact schema branch, stop before
115
+ calling `dev_flow_apply_action` instead of guessing or deriving keys from evidence names.
116
+
117
+ Before calling, read the `dev_flow_apply_action` tool's supplied `inputSchema`: under `allOf`, choose
118
+ the `oneOf` branch whose `action_kind.const` equals the fresh action kind, resolve the payload `$ref`
119
+ through the same schema's `$defs`, and send exactly its `required` members. Do not search the
120
+ repository or installed package, inspect a binary or log, or start another MCP server to recover the
121
+ schema.
122
+
123
+ At the top-level, forward `request_id`, `host`, `task_id`, `revision`, `action_id`, `action_kind`,
124
+ and `repository_binding_digest`. The `payload` object contains only phase schema fields; never nest
125
+ the enclosing request inside `payload`.
126
+
127
+ Bind a source-neutral `fresh_action` from `result.task.current_action` when Core returns a task, or
128
+ from `result.action` when `dev_flow_get_next_action` returns the action directly. Map that same
129
+ fresh action into the tool input exactly:
130
+
131
+ - caller-generated opaque identity -> top-level `request_id`;
132
+ - exact value `codex` -> top-level `host`;
133
+ - `fresh_action.task_id` -> top-level `task_id`;
134
+ - `fresh_action.revision` -> top-level `revision`;
135
+ - `fresh_action.action_id` -> top-level `action_id`;
136
+ - `fresh_action.kind` -> top-level `action_kind`;
137
+ - `fresh_action.repository_binding_digest` -> top-level `repository_binding_digest`;
138
+ - the payload object built from the selected schema branch -> top-level `payload`.
139
+
140
+ Use this type-preserving structure for the `dev_flow_apply_action` arguments:
141
+
142
+ ```text
143
+ apply_arguments = {
144
+ "request_id": caller_request_id,
145
+ "host": "codex",
146
+ "task_id": fresh_action.task_id,
147
+ "revision": fresh_action.revision,
148
+ "action_id": fresh_action.action_id,
149
+ "action_kind": fresh_action.kind,
150
+ "repository_binding_digest": fresh_action.repository_binding_digest,
151
+ "payload": payload_for_selected_schema_branch
152
+ }
153
+ ```
154
+
155
+ `revision` remains an integer, not a string. `payload` remains an object, not a string, containing
156
+ exactly the selected branch's required members.
157
+
158
+ Do not wrap that request inside an outer `payload` object.
159
+
160
+ Use `recovery_apply` only when a fresh Core recovery assessment explicitly requires the exact
161
+ Core-defined form. Resolve context ambiguity through an ordinary Core read, never by guessing.
162
+
163
+ ## Recovery-before-retry contract
164
+
165
+ A mutation result is uncertain when it is missing, malformed, cancelled, truncated, or
166
+ transport-failed instead of returning one complete structured result. All five shapes use the same
167
+ read-before-retry procedure.
168
+
169
+ Before calling `dev_flow_apply_action`, retain the original `request_id`, `task_id`, `source_phase`,
170
+ `revision`, `action_id`, `action_kind`, `repository_binding_digest`, and exact closed `payload` from
171
+ the same fresh action and the same apply dispatch. Never derive or reconstruct any of them from an
172
+ incomplete response or partial output.
173
+
174
+ When all required non-payload original identity values are retained, construct the operation probe
175
+ as exactly this closed `operation_probe`:
176
+
177
+ ```json
178
+ {
179
+ "operation_id": "<original apply request_id>",
180
+ "source_phase": "<original source phase>",
181
+ "expected_revision": 3,
182
+ "action_id": "<original action id>",
183
+ "action_kind": "<original action kind>",
184
+ "repository_binding_digest": "<original issuance binding digest>",
185
+ "payload": {}
186
+ }
187
+ ```
188
+
189
+ `operation_id` is the original apply `request_id`, never the current read request ID.
190
+ `expected_revision` is the original action `revision`. `repository_binding_digest` is the original
191
+ issuance binding. `payload` is the exact original closed payload. If the payload was not completely
192
+ retained, `payload` must be JSON `null`; never reconstruct it from partial output, repository text,
193
+ or model memory. Do not add a caller-supplied payload digest or any other member.
194
+
195
+ 1. The Skill does not immediately repeat `dev_flow_apply_action` or automatically retry.
196
+ 2. Use the original `task_id` to call `dev_flow_get_task` with the exact `operation_probe`.
197
+ 3. Call `dev_flow_get_next_action` only when a current action or outcome is needed. If both reads
198
+ carry a probe, both reads use the same original `operation_probe`.
199
+ 4. A stale pre-dispatch Task snapshot is not an authoritative read-back. Obey only a complete fresh
200
+ Core result and obey the complete Core recovery assessment and advice.
201
+ 5. Permit retry or recovery only when Core explicitly says it is safe to retry or recover. Do not
202
+ branch on, decide, or interpret any recovery classification in the Skill.
203
+ 6. Otherwise stop and report the authoritative blocker or recovery condition.
204
+
205
+ If any required identity is missing or incomplete, do not construct or send an `operation_probe`;
206
+ send no fabricated probe and no half probe. Do not complete missing values from a partial response,
207
+ do not assume `not_started`, and do not automatically retry. Stop and report that the Skill cannot
208
+ prove the mutation state.
209
+
210
+ A complete structured `ok=false` result is a domain error, not transport uncertainty. Never convert
211
+ that domain error to missing or transport-failed. When it reports `retry_safe=false` and
212
+ `action=none`, stop. Do not call `dev_flow_get_next_action` or `dev_flow_apply_action` to repair or
213
+ retry that rejected mutation.
214
+
215
+ ## Evidence and verification budget
216
+
217
+ - Count verification commands exactly against Core's budget.
218
+ - Do not run a prohibited full suite. When automatic capacity is exhausted, present the remaining
219
+ allowed work as a manual handoff.
220
+ - Preserve repository instructions and explicit user authority even when broader work is possible.
221
+ - Keep static inspection, simulated Core execution, user-performed evidence, and native automated
222
+ evidence distinctly labelled.
223
+ - Submit actual evidence sources and outcomes. Never relabel a failed, skipped, or unavailable
224
+ check as passed.
225
+
226
+ ## Blocked and terminal behavior
227
+
228
+ Stop repository work when Core returns an authoritative blocker, ownership or contract conflict,
229
+ `DONE`, or `CANCELLED`. Report Core's blocker and unblock condition, exact terminal outcome and
230
+ evidence summary, cancellation, or conflict without replacing or merging a task. Use
231
+ `dev_flow_cancel_task` only after explicit user authority and a fresh current Core identity.
232
+
233
+ Codex's belief that source work is complete does not override Core, and a blocker is not success.
234
+
235
+ ## Presentation contract
236
+
237
+ Use complete structured Core results for every decision. A concise user summary must still preserve
238
+ task identity, current revision, whether a mutation committed, any blocker or recovery condition,
239
+ verification evidence and limits, and the terminal outcome. Never request or display private
240
+ database locations.
241
+
242
+ If only a truncated preview is available, treat the operation as uncertain and follow the recovery
243
+ contract. Do not fill missing data from a local catalog or silently discard outcome-bearing fields.
@@ -0,0 +1,2 @@
1
+ policy:
2
+ allow_implicit_invocation: false
Binary file