projectstore-codex 0.28.1 → 0.29.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/.codex-plugin/plugin.json +1 -1
- package/README.md +2 -2
- package/bin/projectstore-codex.mjs +14 -9
- package/node_modules/projectstore/.claude-plugin/marketplace.json +1 -1
- package/node_modules/projectstore/.claude-plugin/plugin.json +1 -1
- package/node_modules/projectstore/README.md +4 -4
- package/node_modules/projectstore/docs/harnesses.md +23 -10
- package/node_modules/projectstore/harnesses/claude-code.json +3 -0
- package/node_modules/projectstore/harnesses/codex.json +7 -3
- package/node_modules/projectstore/package.json +1 -1
- package/node_modules/projectstore/scripts/build-adapters.mjs +8 -5
- package/node_modules/projectstore/scripts/cli.mjs +173 -26
- package/node_modules/projectstore/scripts/install-harness.mjs +336 -93
- package/node_modules/projectstore/scripts/term.mjs +147 -0
- package/node_modules/projectstore/scripts/version-guard.mjs +8 -2
- package/package.json +2 -3
- package/skills/projectstore-agents/SKILL.md +5 -4
- package/skills/projectstore-doctor/SKILL.md +3 -1
- package/plugin.json +0 -53
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
// projectstore — term.mjs: terminal presentation for the write verbs, with no
|
|
2
|
+
// dependency (the zero-dependency property the MCP and link-graph ADRs keep).
|
|
3
|
+
//
|
|
4
|
+
// One question decides everything here: is this a person at a terminal? Then
|
|
5
|
+
// colour and in-place step lines. Otherwise — a pipe, an agent's tool call,
|
|
6
|
+
// CI — the same layout as plain text, so a reader that is not a terminal loses
|
|
7
|
+
// nothing but the escapes (install spec, contract 18).
|
|
8
|
+
//
|
|
9
|
+
// caps(stream, env) → { color, live, width, ascii }
|
|
10
|
+
// painter(caps) → paint(style, text)
|
|
11
|
+
// icon(caps, name) → the glyph for an action or a result
|
|
12
|
+
// duration(ms) → "0.4s", "12s", "1m 03s"
|
|
13
|
+
// wrap(text, width, indent) → prose broken at spaces, hanging indent
|
|
14
|
+
// stepReporter(stream, caps) → { start(label), end(ok, note), abort() }
|
|
15
|
+
// askLine(question, in, out) → the answer, or null on end of input
|
|
16
|
+
|
|
17
|
+
import { createInterface } from "node:readline/promises";
|
|
18
|
+
|
|
19
|
+
// FORCE_COLOR decides when it is set, as Node's own tty reads it: "", "1",
|
|
20
|
+
// "2", "3" or "true" turn colour on even without a terminal, anything else
|
|
21
|
+
// ("0", "false") turns it off. Otherwise NO_COLOR (https://no-color.org) turns
|
|
22
|
+
// colour off when set to anything non-empty, and TERM=dumb means a terminal
|
|
23
|
+
// that understands no escapes.
|
|
24
|
+
export function caps(stream = process.stdout, env = process.env) {
|
|
25
|
+
const tty = Boolean(stream && stream.isTTY);
|
|
26
|
+
const dumb = env.TERM === "dumb";
|
|
27
|
+
const off = env.NO_COLOR !== undefined && env.NO_COLOR !== "";
|
|
28
|
+
const color = env.FORCE_COLOR !== undefined ? ["", "1", "2", "3", "true"].includes(String(env.FORCE_COLOR)) : tty && !dumb && !off;
|
|
29
|
+
const columns = tty && Number(stream.columns) > 0 ? Number(stream.columns) : 100;
|
|
30
|
+
// `width` lays out prose (never below 40); `columns` is the terminal's
|
|
31
|
+
// real width, which a live line must not reach.
|
|
32
|
+
return { color, live: tty && !dumb, width: Math.max(40, Math.min(columns, 120)), columns, ascii: dumb };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const SGR = {
|
|
36
|
+
bold: [1, 22], dim: [2, 22], red: [31, 39], green: [32, 39], yellow: [33, 39],
|
|
37
|
+
blue: [34, 39], magenta: [35, 39], cyan: [36, 39], gray: [90, 39],
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// Raw SGR rather than util.styleText: that arrived in Node 20.12 and 21.7, and
|
|
41
|
+
// the package promises >=20.0.0.
|
|
42
|
+
export function painter(c) {
|
|
43
|
+
return (style, text) => {
|
|
44
|
+
const s = String(text);
|
|
45
|
+
if (!c.color || !SGR[style] || !s) return s;
|
|
46
|
+
return `\x1b[${SGR[style][0]}m${s}\x1b[${SGR[style][1]}m`;
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const ICONS = {
|
|
51
|
+
create: ["+", "+"], update: ["↻", "~"], migrate: ["↻", "~"], refresh: ["↻", "~"],
|
|
52
|
+
remove: ["✕", "x"], cleanup: ["✕", "x"], skip: ["·", "."], refuse: ["!", "!"],
|
|
53
|
+
ok: ["✓", "ok"], fail: ["✗", "FAIL"], running: ["…", "..."], note: ["!", "!"],
|
|
54
|
+
};
|
|
55
|
+
export function icon(c, name) {
|
|
56
|
+
const pair = ICONS[name] || ICONS.update;
|
|
57
|
+
return c.ascii ? pair[1] : pair[0];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function duration(ms) {
|
|
61
|
+
if (ms < 10_000) return `${(ms / 1000).toFixed(1)}s`;
|
|
62
|
+
if (ms < 60_000) return `${Math.round(ms / 1000)}s`;
|
|
63
|
+
const m = Math.floor(ms / 60_000), s = Math.round((ms % 60_000) / 1000);
|
|
64
|
+
return `${m}m ${String(s).padStart(2, "0")}s`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Strip SGR escapes — for measuring a painted string and for tests.
|
|
68
|
+
export const plain = (s) => String(s).replace(/\x1b\[[0-9;]*m/g, "");
|
|
69
|
+
|
|
70
|
+
// Prose only: broken at spaces to `width` columns, every line after the first
|
|
71
|
+
// indented by `indent`. A word longer than the line — a path, a command — is
|
|
72
|
+
// never broken: a path cut in two cannot be copied back. Width 0 is a reader
|
|
73
|
+
// that is not a terminal: nothing is broken, so a search never meets a split.
|
|
74
|
+
export function wrap(text, width, indent = "") {
|
|
75
|
+
if (!width) return String(text);
|
|
76
|
+
const room = Math.max(20, width - indent.length);
|
|
77
|
+
const lines = [];
|
|
78
|
+
let line = "";
|
|
79
|
+
for (const word of String(text).split(" ")) {
|
|
80
|
+
if (line && plain(line).length + 1 + plain(word).length > room) { lines.push(line); line = word; }
|
|
81
|
+
else line = line ? `${line} ${word}` : word;
|
|
82
|
+
}
|
|
83
|
+
lines.push(line);
|
|
84
|
+
return lines.map((l, k) => (k ? indent + l : l)).join("\n");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// A live line must fit one physical row: "\r\x1b[2K" clears only the row the
|
|
88
|
+
// cursor is on, so a label that wrapped would leave its first half behind.
|
|
89
|
+
const fit = (text, room) => (text.length <= room ? text : text.slice(0, Math.max(1, room - 1)) + "…");
|
|
90
|
+
|
|
91
|
+
// One line per step. On a live terminal the line appears when the step starts
|
|
92
|
+
// (ending in "…") and is rewritten in place with the result and duration when
|
|
93
|
+
// it ends; anything else gets the finished line only. Steps run synchronously
|
|
94
|
+
// (spawnSync), so nothing animates in between — the "…" is the honest state.
|
|
95
|
+
export function stepReporter(stream = process.stdout, c = caps(stream), { indent = 2 } = {}) {
|
|
96
|
+
const paint = painter(c);
|
|
97
|
+
let open = null;
|
|
98
|
+
// One column short of the terminal: a line that fills the last column
|
|
99
|
+
// leaves some terminals waiting to wrap.
|
|
100
|
+
const width = Math.min(c.width, c.columns || c.width, 100) - 1;
|
|
101
|
+
const line = (mark, label, right) => {
|
|
102
|
+
const room = width - indent - 2 - plain(right).length - 1;
|
|
103
|
+
const left = `${" ".repeat(indent)}${mark} ${c.live ? fit(label, room) : label}`;
|
|
104
|
+
const gap = Math.max(1, width - plain(left).length - plain(right).length);
|
|
105
|
+
return left + " ".repeat(gap) + right;
|
|
106
|
+
};
|
|
107
|
+
const self = {
|
|
108
|
+
start(label) {
|
|
109
|
+
// A step started while another is still open (a host command run by a
|
|
110
|
+
// rollback) closes the open line where it stands rather than overwrite it.
|
|
111
|
+
if (open && c.live) stream.write("\n");
|
|
112
|
+
open = { label, t0: Date.now() };
|
|
113
|
+
if (c.live) stream.write(line(paint("gray", icon(c, "running")), label, ""));
|
|
114
|
+
},
|
|
115
|
+
end(ok = true, note = "", { kept = false } = {}) {
|
|
116
|
+
if (!open) return;
|
|
117
|
+
const ms = Date.now() - open.t0;
|
|
118
|
+
// A step that ran and left its target in place is neither ✓ nor ✗.
|
|
119
|
+
const mark = !ok ? paint("red", icon(c, "fail")) : kept ? paint("gray", icon(c, "skip")) : paint("green", icon(c, "ok"));
|
|
120
|
+
const right = paint("gray", [note, duration(ms)].filter(Boolean).join(" "));
|
|
121
|
+
const text = line(mark, open.label, right);
|
|
122
|
+
stream.write((c.live ? "\r\x1b[2K" : "") + text + "\n");
|
|
123
|
+
open = null;
|
|
124
|
+
},
|
|
125
|
+
// An exception left a step open: end its line as failed, so whatever is
|
|
126
|
+
// printed next starts on a line of its own.
|
|
127
|
+
abort() { if (open) self.end(false); },
|
|
128
|
+
};
|
|
129
|
+
return self;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// One line of input, read the way a shell's own prompt is: in the terminal's
|
|
133
|
+
// line mode, so readline writes nothing but the question (no cursor escapes),
|
|
134
|
+
// the terminal does the editing, and Ctrl+C is a real SIGINT (exit 130, before
|
|
135
|
+
// anything is written). End of input — Ctrl+D, a closed pipe — is null, which
|
|
136
|
+
// every caller reads as no; it never leaves the question pending (an unsettled
|
|
137
|
+
// promise is exit 13 with a warning). The promise API rejects on close in
|
|
138
|
+
// some Node versions and stays pending in others; both settle here.
|
|
139
|
+
export async function askLine(question, input, output) {
|
|
140
|
+
const rl = createInterface({ input, output, terminal: false });
|
|
141
|
+
return await new Promise((settle) => {
|
|
142
|
+
let done = false;
|
|
143
|
+
const finish = (answer) => { if (done) return; done = true; settle(answer); rl.close(); };
|
|
144
|
+
rl.on("close", () => finish(null));
|
|
145
|
+
rl.question(question).then((a) => finish(a), () => finish(null));
|
|
146
|
+
});
|
|
147
|
+
}
|
|
@@ -68,10 +68,16 @@ export function collectShells(root = ROOT) {
|
|
|
68
68
|
if (typeof json.version !== "string" || !json.version) return { error: `${rel}: no version found where one is required` };
|
|
69
69
|
const pluginVersions = [];
|
|
70
70
|
if (name === "projectstore-codex") {
|
|
71
|
-
|
|
71
|
+
// A root plugin.json is an Agent Plugins manifest Codex picks first, and
|
|
72
|
+
// Codex loads no hooks from that format (openai/codex#37027, merged
|
|
73
|
+
// 2026-08-05): every hook goes missing.
|
|
74
|
+
if (existsSync(resolve(root, SHELLS_DIR, name, "plugin.json"))) {
|
|
75
|
+
return { error: `${SHELLS_DIR}/${name}/plugin.json: refused — Codex picks a root Agent Plugins manifest first and loads no hooks from it (openai/codex#37027; codex-rs/core-plugins/src/loader.rs); the shell's manifest is .codex-plugin/plugin.json alone` };
|
|
76
|
+
}
|
|
77
|
+
for (const manifest of [".codex-plugin/plugin.json"]) {
|
|
72
78
|
const manifestRel = `${SHELLS_DIR}/${name}/${manifest}`;
|
|
73
79
|
const manifestAbs = resolve(root, manifestRel);
|
|
74
|
-
if (!existsSync(manifestAbs)) return { error: `${manifestRel}: missing — the Codex shell
|
|
80
|
+
if (!existsSync(manifestAbs)) return { error: `${manifestRel}: missing — the Codex shell's one manifest` };
|
|
75
81
|
let parsed;
|
|
76
82
|
try { parsed = JSON.parse(readFileSync(manifestAbs, "utf8")); } catch (e) { return { error: `${manifestRel}: ${e.message}` }; }
|
|
77
83
|
if (typeof parsed.version !== "string" || !parsed.version) return { error: `${manifestRel}: no version found where one is required` };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "projectstore-codex",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.29.0",
|
|
4
4
|
"description": "ProjectStore for Codex: portable project memory, rendered workflow skills and lifecycle hooks, with the core pinned and bundled.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"projectstore",
|
|
@@ -35,13 +35,12 @@
|
|
|
35
35
|
"files": [
|
|
36
36
|
"bin/",
|
|
37
37
|
"README.md",
|
|
38
|
-
"plugin.json",
|
|
39
38
|
".codex-plugin/",
|
|
40
39
|
"skills/",
|
|
41
40
|
"hooks/"
|
|
42
41
|
],
|
|
43
42
|
"dependencies": {
|
|
44
|
-
"projectstore": "=0.
|
|
43
|
+
"projectstore": "=0.29.0"
|
|
45
44
|
},
|
|
46
45
|
"bundleDependencies": [
|
|
47
46
|
"projectstore"
|
|
@@ -26,10 +26,11 @@ You are managing ProjectStore's Codex agent integration. Require a bound project
|
|
|
26
26
|
|
|
27
27
|
## register / unregister
|
|
28
28
|
|
|
29
|
-
Preview the requested change
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
29
|
+
Preview the requested change with `plan --harness codex --surface agents_block
|
|
30
|
+
--project "$PWD"` and ask for explicit approval. On approval, run the core's
|
|
31
|
+
`install` or `uninstall` verb with `--harness codex --surface agents_block
|
|
32
|
+
--project "$PWD" --json` — `--json` never waits on a terminal's question — and
|
|
33
|
+
report the envelope's result. Never edit the managed block by hand.
|
|
33
34
|
|
|
34
35
|
## status
|
|
35
36
|
|
|
@@ -28,6 +28,8 @@ arguments and `--json`. Summarize every finding without re-deriving it.
|
|
|
28
28
|
When `--fix` is absent, remain read-only. When it is present, separate fixes
|
|
29
29
|
by owner: derived vault views use `$projectstore-reconcile`; Codex plugin or
|
|
30
30
|
agents-block drift uses the core's `upgrade --harness codex` path. Preview
|
|
31
|
-
each mutation and ask for explicit approval before
|
|
31
|
+
each mutation with `plan --harness codex` and ask for explicit approval before
|
|
32
|
+
running it; then run `upgrade --harness codex --json`, whose envelope is the
|
|
33
|
+
result — `--json` never waits on a terminal's question. Unsupported
|
|
32
34
|
surfaces remain unsupported; do not create host configuration by hand. Never
|
|
33
35
|
claim a fix after a non-zero exit.
|
package/plugin.json
DELETED
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
|
|
3
|
-
"name": "projectstore",
|
|
4
|
-
"version": "0.28.1",
|
|
5
|
-
"description": "Your agent runs the project through a verified loop: task → artifact (ADR / spec / epic / story) → adversarial critic → backlog → planner → reviewer → done. Plain markdown in git — any model can pick the project up tomorrow.",
|
|
6
|
-
"author": {
|
|
7
|
-
"name": "Evgenii Konev",
|
|
8
|
-
"email": "ekonev@smartandpoint.com",
|
|
9
|
-
"url": "https://github.com/SmartAndPoint"
|
|
10
|
-
},
|
|
11
|
-
"homepage": "https://github.com/SmartAndPoint/ProjectStore#readme",
|
|
12
|
-
"repository": "git+https://github.com/SmartAndPoint/ProjectStore.git",
|
|
13
|
-
"license": "MIT",
|
|
14
|
-
"keywords": [
|
|
15
|
-
"project-management",
|
|
16
|
-
"adr",
|
|
17
|
-
"epics",
|
|
18
|
-
"stories",
|
|
19
|
-
"kanban",
|
|
20
|
-
"obsidian",
|
|
21
|
-
"markdown",
|
|
22
|
-
"knowledge-base",
|
|
23
|
-
"engineering-process",
|
|
24
|
-
"claude-code",
|
|
25
|
-
"codex",
|
|
26
|
-
"agentic",
|
|
27
|
-
"ai-agents"
|
|
28
|
-
],
|
|
29
|
-
"extensions": {
|
|
30
|
-
"com.openai": {
|
|
31
|
-
"hooks": "./hooks/hooks.json",
|
|
32
|
-
"interface": {
|
|
33
|
-
"displayName": "ProjectStore",
|
|
34
|
-
"shortDescription": "Versioned project memory for developer teams and agents.",
|
|
35
|
-
"longDescription": "Your agent runs the project through a verified loop: task → artifact (ADR / spec / epic / story) → adversarial critic → backlog → planner → reviewer → done. Plain markdown in git — any model can pick the project up tomorrow.",
|
|
36
|
-
"developerName": "SmartAndPoint",
|
|
37
|
-
"category": "Developer Tools",
|
|
38
|
-
"capabilities": [
|
|
39
|
-
"Project memory",
|
|
40
|
-
"Architecture decisions",
|
|
41
|
-
"Planning",
|
|
42
|
-
"Peer review"
|
|
43
|
-
],
|
|
44
|
-
"websiteURL": "https://github.com/SmartAndPoint/ProjectStore#readme",
|
|
45
|
-
"defaultPrompt": [
|
|
46
|
-
"Show the current ProjectStore status and work in progress.",
|
|
47
|
-
"Capture this technical decision as a ProjectStore ADR.",
|
|
48
|
-
"Plan the next ProjectStore story, then review the implementation."
|
|
49
|
-
]
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
}
|