@codyswann/lisa 2.107.2 → 2.109.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.
Files changed (25) hide show
  1. package/package.json +1 -1
  2. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  3. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  4. package/plugins/lisa/scripts/project-ideation-idempotency-harness.mjs +319 -0
  5. package/plugins/lisa/skills/project-ideation/SKILL.md +27 -0
  6. package/plugins/lisa/skills/project-ideation/examples/idempotency-verification-harness.md +57 -0
  7. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  8. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  9. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  10. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  11. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  12. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  13. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  14. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  15. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  16. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  17. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  18. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  19. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  20. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  21. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  22. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  23. package/plugins/src/base/scripts/project-ideation-idempotency-harness.mjs +319 -0
  24. package/plugins/src/base/skills/project-ideation/SKILL.md +27 -0
  25. package/plugins/src/base/skills/project-ideation/examples/idempotency-verification-harness.md +57 -0
package/package.json CHANGED
@@ -82,7 +82,7 @@
82
82
  "lodash": ">=4.18.1"
83
83
  },
84
84
  "name": "@codyswann/lisa",
85
- "version": "2.107.2",
85
+ "version": "2.109.0",
86
86
  "description": "Claude Code governance framework that applies guardrails, guidance, and automated enforcement to projects",
87
87
  "main": "dist/index.js",
88
88
  "exports": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa",
3
- "version": "2.107.2",
3
+ "version": "2.109.0",
4
4
  "description": "Universal governance — agents, skills, commands, hooks, and rules for all projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa",
3
- "version": "2.107.2",
3
+ "version": "2.109.0",
4
4
  "description": "Universal governance: agents, skills, commands, hooks, and rules for all projects.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -0,0 +1,319 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Deterministic verification harness for project-ideation PRD dedupe.
4
+ *
5
+ * The harness runs a caller-supplied deterministic project-ideation command
6
+ * twice, checks that exactly one open GitHub PRD contains the expected marker,
7
+ * then optionally removes the automation memory file and verifies a third run
8
+ * recreates memory without creating a duplicate PRD. The recreated memory entry
9
+ * must include the advisory run fields project-ideation promises to write.
10
+ */
11
+
12
+ import {
13
+ existsSync,
14
+ mkdirSync,
15
+ readFileSync,
16
+ renameSync,
17
+ rmSync,
18
+ } from "node:fs";
19
+ import { dirname, resolve } from "node:path";
20
+ import { spawnSync } from "node:child_process";
21
+
22
+ const args = parseArgs(process.argv.slice(2));
23
+
24
+ if (args.help) {
25
+ printUsage();
26
+ process.exit(0);
27
+ }
28
+
29
+ const repo = requireValue(args.repo, "--repo");
30
+ const marker = requireValue(args.marker, "--marker");
31
+ const command = requireValue(args.command, "--command");
32
+ const memoryFile = args.memoryFile ? resolve(args.memoryFile) : null;
33
+
34
+ runCommand("first ideation run", command);
35
+ const first = assertSingleOpenMarker(repo, marker, "after first run");
36
+
37
+ runCommand("second ideation run", command);
38
+ const second = assertSingleOpenMarker(repo, marker, "after second run");
39
+
40
+ let missingMemory = null;
41
+ if (memoryFile) {
42
+ missingMemory = runMissingMemoryVariant(repo, marker, command, memoryFile);
43
+ }
44
+
45
+ console.log(
46
+ JSON.stringify(
47
+ {
48
+ repo,
49
+ marker,
50
+ first,
51
+ second,
52
+ missingMemory,
53
+ verdict: "PASS",
54
+ },
55
+ null,
56
+ 2
57
+ )
58
+ );
59
+
60
+ /**
61
+ * @param {string[]} argv
62
+ * @returns {Record<string, string | boolean>}
63
+ */
64
+ function parseArgs(argv) {
65
+ const parsed = {};
66
+
67
+ for (let index = 0; index < argv.length; index += 1) {
68
+ const token = argv[index];
69
+ if (token === "--help" || token === "-h") {
70
+ parsed.help = true;
71
+ continue;
72
+ }
73
+ if (!token.startsWith("--")) {
74
+ fail(`Unexpected positional argument: ${token}`);
75
+ }
76
+
77
+ const eqIndex = token.indexOf("=");
78
+ if (eqIndex !== -1) {
79
+ parsed[token.slice(2, eqIndex)] = token.slice(eqIndex + 1);
80
+ continue;
81
+ }
82
+
83
+ const key = token.slice(2);
84
+ const value = argv[index + 1];
85
+ if (!value || value.startsWith("--")) {
86
+ fail(`Missing value for --${key}`);
87
+ }
88
+ parsed[key] = value;
89
+ index += 1;
90
+ }
91
+
92
+ return parsed;
93
+ }
94
+
95
+ /**
96
+ * @param {Record<string, string | boolean>} input
97
+ * @param {string} key
98
+ * @returns {string}
99
+ */
100
+ function requireValue(input, key) {
101
+ const normalized = key.replace(/^--/, "");
102
+ const value = input[normalized];
103
+ if (typeof value !== "string" || value.trim().length === 0) {
104
+ fail(`${key} is required`);
105
+ }
106
+ return value.trim();
107
+ }
108
+
109
+ /**
110
+ * @param {string} label
111
+ * @param {string} command
112
+ */
113
+ function runCommand(label, command) {
114
+ const result = spawnSync(command, {
115
+ shell: true,
116
+ stdio: "inherit",
117
+ env: process.env,
118
+ });
119
+
120
+ if (result.status !== 0) {
121
+ fail(`${label} failed with exit code ${String(result.status)}`);
122
+ }
123
+ }
124
+
125
+ /**
126
+ * @param {string} repo
127
+ * @param {string} marker
128
+ * @param {string} phase
129
+ * @returns {{ readonly count: number, readonly issue: Record<string, any> }}
130
+ */
131
+ function assertSingleOpenMarker(repo, marker, phase) {
132
+ const issues = queryOpenIssuesByMarker(repo, marker);
133
+ if (issues.length !== 1) {
134
+ fail(
135
+ `Expected exactly one open issue containing marker ${JSON.stringify(marker)} ${phase}, found ${issues.length}`
136
+ );
137
+ }
138
+
139
+ return {
140
+ count: issues.length,
141
+ issue: {
142
+ number: issues[0].number,
143
+ title: issues[0].title,
144
+ url: issues[0].url,
145
+ labels: normalizeLabels(issues[0].labels),
146
+ },
147
+ };
148
+ }
149
+
150
+ /**
151
+ * @param {string} repo
152
+ * @param {string} marker
153
+ * @returns {readonly Record<string, any>[]}
154
+ */
155
+ function queryOpenIssuesByMarker(repo, marker) {
156
+ const search = runJson("gh", [
157
+ "issue",
158
+ "list",
159
+ "--repo",
160
+ repo,
161
+ "--state",
162
+ "open",
163
+ "--search",
164
+ `"${marker}" in:body`,
165
+ "--limit",
166
+ "100",
167
+ "--json",
168
+ "number,title,url,labels",
169
+ ]);
170
+
171
+ if (Array.isArray(search) && search.length > 0) {
172
+ return search;
173
+ }
174
+
175
+ const fallback = runJson("gh", [
176
+ "issue",
177
+ "list",
178
+ "--repo",
179
+ repo,
180
+ "--state",
181
+ "open",
182
+ "--limit",
183
+ "1000",
184
+ "--json",
185
+ "number,title,url,labels,body",
186
+ ]);
187
+
188
+ if (!Array.isArray(fallback)) {
189
+ return [];
190
+ }
191
+
192
+ return fallback.filter(issue => String(issue.body ?? "").includes(marker));
193
+ }
194
+
195
+ /**
196
+ * @param {string} repo
197
+ * @param {string} marker
198
+ * @param {string} command
199
+ * @param {string} memoryFile
200
+ * @returns {{ readonly count: number, readonly issue: Record<string, any>, readonly memoryRecreated: boolean, readonly memoryFieldsRecorded: boolean }}
201
+ */
202
+ function runMissingMemoryVariant(repo, marker, command, memoryFile) {
203
+ const backup = `${memoryFile}.project-ideation-idempotency-backup`;
204
+ rmSync(backup, { force: true });
205
+
206
+ if (existsSync(memoryFile)) {
207
+ mkdirSync(dirname(backup), { recursive: true });
208
+ renameSync(memoryFile, backup);
209
+ }
210
+
211
+ try {
212
+ runCommand("missing-memory ideation run", command);
213
+ const result = assertSingleOpenMarker(
214
+ repo,
215
+ marker,
216
+ "after missing-memory run"
217
+ );
218
+ const memoryRecreated = existsSync(memoryFile);
219
+ const memoryFieldsRecorded =
220
+ memoryRecreated &&
221
+ memoryContainsRunEntry(memoryFile, {
222
+ marker,
223
+ prdUrl: String(result.issue.url),
224
+ });
225
+
226
+ if (!memoryRecreated) {
227
+ fail(`Expected missing-memory run to recreate ${memoryFile}`);
228
+ }
229
+ if (!memoryFieldsRecorded) {
230
+ fail(
231
+ `Expected recreated memory to record marker, PRD URL, outcome, lifecycle_role, and source_agreement`
232
+ );
233
+ }
234
+
235
+ return {
236
+ ...result,
237
+ memoryRecreated,
238
+ memoryFieldsRecorded,
239
+ };
240
+ } finally {
241
+ if (existsSync(backup)) {
242
+ rmSync(memoryFile, { force: true });
243
+ mkdirSync(dirname(memoryFile), { recursive: true });
244
+ renameSync(backup, memoryFile);
245
+ }
246
+ }
247
+ }
248
+
249
+ /**
250
+ * @param {string} memoryFile
251
+ * @param {{ readonly marker: string, readonly prdUrl: string }} expected
252
+ * @returns {boolean}
253
+ */
254
+ function memoryContainsRunEntry(memoryFile, expected) {
255
+ const memory = readFileSync(memoryFile, "utf8");
256
+ const requiredFragments = [
257
+ expected.marker,
258
+ expected.prdUrl,
259
+ "outcome:",
260
+ "lifecycle_role:",
261
+ "source_agreement:",
262
+ ];
263
+
264
+ return requiredFragments.every(fragment => memory.includes(fragment));
265
+ }
266
+
267
+ /**
268
+ * @param {string} command
269
+ * @param {readonly string[]} argv
270
+ * @returns {any}
271
+ */
272
+ function runJson(command, argv) {
273
+ const result = spawnSync(command, argv, {
274
+ encoding: "utf8",
275
+ stdio: ["ignore", "pipe", "pipe"],
276
+ });
277
+
278
+ if (result.status !== 0) {
279
+ fail(`${command} ${argv.join(" ")} failed:\n${result.stderr}`);
280
+ }
281
+
282
+ try {
283
+ return JSON.parse(result.stdout);
284
+ } catch (error) {
285
+ fail(`Could not parse JSON from ${command}: ${error.message}`);
286
+ }
287
+ }
288
+
289
+ /**
290
+ * @param {readonly any[] | undefined} labels
291
+ * @returns {readonly string[]}
292
+ */
293
+ function normalizeLabels(labels) {
294
+ return (Array.isArray(labels) ? labels : [])
295
+ .map(label => (typeof label === "string" ? label : label?.name))
296
+ .filter(Boolean);
297
+ }
298
+
299
+ function printUsage() {
300
+ console.log(`Usage:
301
+ node plugins/lisa/scripts/project-ideation-idempotency-harness.mjs \\
302
+ --repo CodySwannGT/lisa \\
303
+ --marker "[lisa-project-ideation] idea=<stable-key>" \\
304
+ --command "codex exec '/lisa:project-ideation ./fixtures/project-ideation-idempotency prd_ready=true max_prds=1'" \\
305
+ --memory-file "$CODEX_HOME/automations/<automation_id>/memory.md"
306
+
307
+ The command must be deterministic: it should select the same idea and marker on
308
+ each run. When --memory-file is provided, the harness temporarily moves it aside
309
+ for the missing-memory variant and restores the original file afterward.`);
310
+ }
311
+
312
+ /**
313
+ * @param {string} message
314
+ * @returns {never}
315
+ */
316
+ function fail(message) {
317
+ console.error(message);
318
+ process.exit(1);
319
+ }
@@ -25,6 +25,10 @@ cannot verify yourself is noise — demote it honestly.
25
25
  creates **one** PRD (the single top-ranked idea), because `lisa:research` is a heavy full flow.
26
26
  `max_prds=3` creates the top three; `max_prds=all` creates one per build-ready idea. Discovery
27
27
  Spikes and Rejected ideas are never turned into PRDs regardless of `max_prds`.
28
+ - **`fixture=<path>`** (optional, verification-only) — a deterministic host-project fixture used
29
+ for idempotency verification. When present, read the fixture before ranking and honor its declared
30
+ single persona, single idea, existing-fit anchor, and expected dedupe marker. Do not use this
31
+ parameter for normal ideation runs.
28
32
 
29
33
  ## When to use
30
34
 
@@ -157,6 +161,26 @@ For each idea in the creation set, invoke `/lisa:research` with:
157
161
  `lisa:prd-source-write`. `project-ideation` never writes to the source directly — it delegates, so
158
162
  the PRD source stays switchable per project. Capture each returned PRD ref / URL / role / outcome.
159
163
 
164
+ ### Optional Codex automation memory
165
+
166
+ When the run has a Codex automation id or memory path, maintain a concise local advisory ledger after
167
+ the PRD source write returns. Resolve the memory path in this order:
168
+
169
+ 1. explicit `memory_file=<path>` or `automation_memory=<path>` argument, when supplied;
170
+ 2. `$CODEX_AUTOMATION_MEMORY`, when set;
171
+ 3. `$CODEX_HOME/automations/<automation_id>/memory.md`, when `automation_id=<id>` or
172
+ `$CODEX_AUTOMATION_ID` is available.
173
+
174
+ Create the parent directory and `memory.md` if missing. Write one concise run entry keyed by the
175
+ dedupe marker and run timestamp. The entry must include the marker, PRD URL/ref, outcome
176
+ (`created | reused | updated | blocked`), lifecycle role (`draft | ready | blocked` or the returned
177
+ source role), and `source_agreement` (`github-source-wins`, `memory-created`, `memory-updated`, or
178
+ `memory-missing-runtime`). If memory says one thing but the PRD source search finds a matching open
179
+ PRD, GitHub/source truth wins: reuse the source PRD and update memory rather than creating a
180
+ duplicate. Keep memory advisory only; never use it to override lifecycle labels, source marker
181
+ matches, or the PRD source writer's returned role. Do not store secrets, tokens, full PRD bodies, or
182
+ private source excerpts in memory.
183
+
160
184
  ### Dedupe marker (stable, never title-based)
161
185
 
162
186
  Each created PRD carries the marker `[lisa-project-ideation] idea=<stable-key>`. Compute
@@ -232,3 +256,6 @@ Use the markdown examples in `examples/` as shape references for the idea report
232
256
  requirements.
233
257
  - `unavailable-data-rejection.md` — naming missing private/paid/unavailable sources when demoting.
234
258
  - `evidence-card-format.md` — the required evidence fields every Practical Idea card must carry.
259
+ - `idempotency-verification-harness.md` — deterministic fixture and script procedure proving that
260
+ repeated `prd_ready=true` ideation keeps the open GitHub marker count at one, including the
261
+ missing-memory rerun variant.
@@ -0,0 +1,57 @@
1
+ # Project Ideation Idempotency Verification Harness
2
+
3
+ Use this documented harness when validating that `project-ideation prd_ready=true` reuses an
4
+ existing GitHub PRD by stable marker instead of creating duplicates.
5
+
6
+ ## Deterministic fixture
7
+
8
+ Create an isolated fixture project with only one evidence-backed persona and one build-ready idea.
9
+ The fixture should make the selected marker predictable:
10
+
11
+ - Repo identity: `CodySwannGT/lisa`
12
+ - Persona key: `queue-automation-operators`
13
+ - Idea name: `Exploratory PRD run ledger`
14
+ - Existing-fit anchor: `plugins/src/base/skills/project-ideation/SKILL.md`
15
+ - Expected marker: `[lisa-project-ideation] idea=codyswanngt-lisa-exploratory-prd-run-ledger-queue-automation-operators-plugins-lisa-skills-project-ideation-skill-md`
16
+
17
+ The fixture may be a tiny directory with:
18
+
19
+ - `README.md` naming queue automation operators and their goals.
20
+ - `.lisa.config.json` pointing `source` and `tracker` to GitHub.
21
+ - `PROJECT_IDEATION_FIXTURE.md` listing the single persona, the single idea, and the expected
22
+ marker above.
23
+
24
+ Invoke project ideation against that fixture with `prd_ready=true max_prds=1`. The fixture is valid
25
+ only if the idea report selects the single expected idea and the PRD summary reports the expected
26
+ marker.
27
+
28
+ ## Scripted verification
29
+
30
+ Run the shipped harness after choosing the deterministic command for your runtime:
31
+
32
+ ```bash
33
+ node plugins/lisa/scripts/project-ideation-idempotency-harness.mjs \
34
+ --repo CodySwannGT/lisa \
35
+ --marker "[lisa-project-ideation] idea=codyswanngt-lisa-exploratory-prd-run-ledger-queue-automation-operators-plugins-lisa-skills-project-ideation-skill-md" \
36
+ --command "codex exec '/lisa:project-ideation ./fixtures/project-ideation-idempotency prd_ready=true max_prds=1'" \
37
+ --memory-file "$CODEX_HOME/automations/<automation_id>/memory.md"
38
+ ```
39
+
40
+ The harness performs the acceptance check in three phases:
41
+
42
+ 1. Run the deterministic ideation command once, then query open GitHub issues for the marker and
43
+ require exactly one match.
44
+ 2. Run the same command again, then require the open marker count to remain one.
45
+ 3. Move the automation memory file aside, run the command again, require the marker count to remain
46
+ one, and require the memory file to be recreated with marker, PRD URL, outcome, lifecycle role,
47
+ and source agreement fields. The original memory file is restored at the end.
48
+
49
+ ## Expected evidence
50
+
51
+ Capture the harness JSON output as:
52
+
53
+ - `[EVIDENCE: marker-count-one]` from the first and second marker-count checks.
54
+ - `[EVIDENCE: memory-recreated-after-rerun]` from the missing-memory variant.
55
+
56
+ The run passes only when all reported counts are `1`, the issue URL is the same across phases, and
57
+ `memoryRecreated` and `memoryFieldsRecorded` are `true` when `--memory-file` is supplied.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-cdk",
3
- "version": "2.107.2",
3
+ "version": "2.109.0",
4
4
  "description": "AWS CDK-specific plugin",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-cdk",
3
- "version": "2.107.2",
3
+ "version": "2.109.0",
4
4
  "description": "AWS CDK-specific Lisa plugin.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-expo",
3
- "version": "2.107.2",
3
+ "version": "2.109.0",
4
4
  "description": "Expo/React Native-specific skills, agents, rules, and MCP servers",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-expo",
3
- "version": "2.107.2",
3
+ "version": "2.109.0",
4
4
  "description": "Expo and React Native-specific skills, agents, rules, and MCP servers.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-harper-fabric",
3
- "version": "2.107.2",
3
+ "version": "2.109.0",
4
4
  "description": "Harper/Fabric-specific rules for TypeScript component apps",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-harper-fabric",
3
- "version": "2.107.2",
3
+ "version": "2.109.0",
4
4
  "description": "Harper/Fabric-specific Lisa rules for TypeScript component apps.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-nestjs",
3
- "version": "2.107.2",
3
+ "version": "2.109.0",
4
4
  "description": "NestJS-specific skills (GraphQL, TypeORM) and hooks (migration write-protection)",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-nestjs",
3
- "version": "2.107.2",
3
+ "version": "2.109.0",
4
4
  "description": "NestJS-specific skills and migration write-protection hooks.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-openclaw",
3
- "version": "2.107.2",
3
+ "version": "2.109.0",
4
4
  "description": "Connect staff roles to Telegram or Slack via OpenClaw — facilitator/specialist hub-and-spoke routing and repo-coding topics, for Claude Code and Codex",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-openclaw",
3
- "version": "2.107.2",
3
+ "version": "2.109.0",
4
4
  "description": "Connect staff roles to Telegram or Slack via OpenClaw — facilitator/specialist hub-and-spoke routing and repo-coding topics, across Claude and Codex.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-rails",
3
- "version": "2.107.2",
3
+ "version": "2.109.0",
4
4
  "description": "Ruby on Rails-specific hooks — RuboCop linting/formatting and ast-grep scanning on edit",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-rails",
3
- "version": "2.107.2",
3
+ "version": "2.109.0",
4
4
  "description": "Ruby on Rails-specific skills and hooks for RuboCop and ast-grep scanning on edit.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-typescript",
3
- "version": "2.107.2",
3
+ "version": "2.109.0",
4
4
  "description": "TypeScript-specific hooks — Prettier formatting, ESLint linting, ast-grep scanning, and error-suppression blocking on edit",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-typescript",
3
- "version": "2.107.2",
3
+ "version": "2.109.0",
4
4
  "description": "TypeScript-specific hooks for formatting, linting, and ast-grep scanning on edit.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-wiki",
3
- "version": "2.107.2",
3
+ "version": "2.109.0",
4
4
  "description": "LLM Wiki — a distributable, git-native markdown knowledge base for Claude Code and Codex",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-wiki",
3
- "version": "2.107.2",
3
+ "version": "2.109.0",
4
4
  "description": "Distributable LLM Wiki kernel — ingest, query, lint, and maintain a git-native markdown knowledge base across Claude and Codex.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -0,0 +1,319 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Deterministic verification harness for project-ideation PRD dedupe.
4
+ *
5
+ * The harness runs a caller-supplied deterministic project-ideation command
6
+ * twice, checks that exactly one open GitHub PRD contains the expected marker,
7
+ * then optionally removes the automation memory file and verifies a third run
8
+ * recreates memory without creating a duplicate PRD. The recreated memory entry
9
+ * must include the advisory run fields project-ideation promises to write.
10
+ */
11
+
12
+ import {
13
+ existsSync,
14
+ mkdirSync,
15
+ readFileSync,
16
+ renameSync,
17
+ rmSync,
18
+ } from "node:fs";
19
+ import { dirname, resolve } from "node:path";
20
+ import { spawnSync } from "node:child_process";
21
+
22
+ const args = parseArgs(process.argv.slice(2));
23
+
24
+ if (args.help) {
25
+ printUsage();
26
+ process.exit(0);
27
+ }
28
+
29
+ const repo = requireValue(args.repo, "--repo");
30
+ const marker = requireValue(args.marker, "--marker");
31
+ const command = requireValue(args.command, "--command");
32
+ const memoryFile = args.memoryFile ? resolve(args.memoryFile) : null;
33
+
34
+ runCommand("first ideation run", command);
35
+ const first = assertSingleOpenMarker(repo, marker, "after first run");
36
+
37
+ runCommand("second ideation run", command);
38
+ const second = assertSingleOpenMarker(repo, marker, "after second run");
39
+
40
+ let missingMemory = null;
41
+ if (memoryFile) {
42
+ missingMemory = runMissingMemoryVariant(repo, marker, command, memoryFile);
43
+ }
44
+
45
+ console.log(
46
+ JSON.stringify(
47
+ {
48
+ repo,
49
+ marker,
50
+ first,
51
+ second,
52
+ missingMemory,
53
+ verdict: "PASS",
54
+ },
55
+ null,
56
+ 2
57
+ )
58
+ );
59
+
60
+ /**
61
+ * @param {string[]} argv
62
+ * @returns {Record<string, string | boolean>}
63
+ */
64
+ function parseArgs(argv) {
65
+ const parsed = {};
66
+
67
+ for (let index = 0; index < argv.length; index += 1) {
68
+ const token = argv[index];
69
+ if (token === "--help" || token === "-h") {
70
+ parsed.help = true;
71
+ continue;
72
+ }
73
+ if (!token.startsWith("--")) {
74
+ fail(`Unexpected positional argument: ${token}`);
75
+ }
76
+
77
+ const eqIndex = token.indexOf("=");
78
+ if (eqIndex !== -1) {
79
+ parsed[token.slice(2, eqIndex)] = token.slice(eqIndex + 1);
80
+ continue;
81
+ }
82
+
83
+ const key = token.slice(2);
84
+ const value = argv[index + 1];
85
+ if (!value || value.startsWith("--")) {
86
+ fail(`Missing value for --${key}`);
87
+ }
88
+ parsed[key] = value;
89
+ index += 1;
90
+ }
91
+
92
+ return parsed;
93
+ }
94
+
95
+ /**
96
+ * @param {Record<string, string | boolean>} input
97
+ * @param {string} key
98
+ * @returns {string}
99
+ */
100
+ function requireValue(input, key) {
101
+ const normalized = key.replace(/^--/, "");
102
+ const value = input[normalized];
103
+ if (typeof value !== "string" || value.trim().length === 0) {
104
+ fail(`${key} is required`);
105
+ }
106
+ return value.trim();
107
+ }
108
+
109
+ /**
110
+ * @param {string} label
111
+ * @param {string} command
112
+ */
113
+ function runCommand(label, command) {
114
+ const result = spawnSync(command, {
115
+ shell: true,
116
+ stdio: "inherit",
117
+ env: process.env,
118
+ });
119
+
120
+ if (result.status !== 0) {
121
+ fail(`${label} failed with exit code ${String(result.status)}`);
122
+ }
123
+ }
124
+
125
+ /**
126
+ * @param {string} repo
127
+ * @param {string} marker
128
+ * @param {string} phase
129
+ * @returns {{ readonly count: number, readonly issue: Record<string, any> }}
130
+ */
131
+ function assertSingleOpenMarker(repo, marker, phase) {
132
+ const issues = queryOpenIssuesByMarker(repo, marker);
133
+ if (issues.length !== 1) {
134
+ fail(
135
+ `Expected exactly one open issue containing marker ${JSON.stringify(marker)} ${phase}, found ${issues.length}`
136
+ );
137
+ }
138
+
139
+ return {
140
+ count: issues.length,
141
+ issue: {
142
+ number: issues[0].number,
143
+ title: issues[0].title,
144
+ url: issues[0].url,
145
+ labels: normalizeLabels(issues[0].labels),
146
+ },
147
+ };
148
+ }
149
+
150
+ /**
151
+ * @param {string} repo
152
+ * @param {string} marker
153
+ * @returns {readonly Record<string, any>[]}
154
+ */
155
+ function queryOpenIssuesByMarker(repo, marker) {
156
+ const search = runJson("gh", [
157
+ "issue",
158
+ "list",
159
+ "--repo",
160
+ repo,
161
+ "--state",
162
+ "open",
163
+ "--search",
164
+ `"${marker}" in:body`,
165
+ "--limit",
166
+ "100",
167
+ "--json",
168
+ "number,title,url,labels",
169
+ ]);
170
+
171
+ if (Array.isArray(search) && search.length > 0) {
172
+ return search;
173
+ }
174
+
175
+ const fallback = runJson("gh", [
176
+ "issue",
177
+ "list",
178
+ "--repo",
179
+ repo,
180
+ "--state",
181
+ "open",
182
+ "--limit",
183
+ "1000",
184
+ "--json",
185
+ "number,title,url,labels,body",
186
+ ]);
187
+
188
+ if (!Array.isArray(fallback)) {
189
+ return [];
190
+ }
191
+
192
+ return fallback.filter(issue => String(issue.body ?? "").includes(marker));
193
+ }
194
+
195
+ /**
196
+ * @param {string} repo
197
+ * @param {string} marker
198
+ * @param {string} command
199
+ * @param {string} memoryFile
200
+ * @returns {{ readonly count: number, readonly issue: Record<string, any>, readonly memoryRecreated: boolean, readonly memoryFieldsRecorded: boolean }}
201
+ */
202
+ function runMissingMemoryVariant(repo, marker, command, memoryFile) {
203
+ const backup = `${memoryFile}.project-ideation-idempotency-backup`;
204
+ rmSync(backup, { force: true });
205
+
206
+ if (existsSync(memoryFile)) {
207
+ mkdirSync(dirname(backup), { recursive: true });
208
+ renameSync(memoryFile, backup);
209
+ }
210
+
211
+ try {
212
+ runCommand("missing-memory ideation run", command);
213
+ const result = assertSingleOpenMarker(
214
+ repo,
215
+ marker,
216
+ "after missing-memory run"
217
+ );
218
+ const memoryRecreated = existsSync(memoryFile);
219
+ const memoryFieldsRecorded =
220
+ memoryRecreated &&
221
+ memoryContainsRunEntry(memoryFile, {
222
+ marker,
223
+ prdUrl: String(result.issue.url),
224
+ });
225
+
226
+ if (!memoryRecreated) {
227
+ fail(`Expected missing-memory run to recreate ${memoryFile}`);
228
+ }
229
+ if (!memoryFieldsRecorded) {
230
+ fail(
231
+ `Expected recreated memory to record marker, PRD URL, outcome, lifecycle_role, and source_agreement`
232
+ );
233
+ }
234
+
235
+ return {
236
+ ...result,
237
+ memoryRecreated,
238
+ memoryFieldsRecorded,
239
+ };
240
+ } finally {
241
+ if (existsSync(backup)) {
242
+ rmSync(memoryFile, { force: true });
243
+ mkdirSync(dirname(memoryFile), { recursive: true });
244
+ renameSync(backup, memoryFile);
245
+ }
246
+ }
247
+ }
248
+
249
+ /**
250
+ * @param {string} memoryFile
251
+ * @param {{ readonly marker: string, readonly prdUrl: string }} expected
252
+ * @returns {boolean}
253
+ */
254
+ function memoryContainsRunEntry(memoryFile, expected) {
255
+ const memory = readFileSync(memoryFile, "utf8");
256
+ const requiredFragments = [
257
+ expected.marker,
258
+ expected.prdUrl,
259
+ "outcome:",
260
+ "lifecycle_role:",
261
+ "source_agreement:",
262
+ ];
263
+
264
+ return requiredFragments.every(fragment => memory.includes(fragment));
265
+ }
266
+
267
+ /**
268
+ * @param {string} command
269
+ * @param {readonly string[]} argv
270
+ * @returns {any}
271
+ */
272
+ function runJson(command, argv) {
273
+ const result = spawnSync(command, argv, {
274
+ encoding: "utf8",
275
+ stdio: ["ignore", "pipe", "pipe"],
276
+ });
277
+
278
+ if (result.status !== 0) {
279
+ fail(`${command} ${argv.join(" ")} failed:\n${result.stderr}`);
280
+ }
281
+
282
+ try {
283
+ return JSON.parse(result.stdout);
284
+ } catch (error) {
285
+ fail(`Could not parse JSON from ${command}: ${error.message}`);
286
+ }
287
+ }
288
+
289
+ /**
290
+ * @param {readonly any[] | undefined} labels
291
+ * @returns {readonly string[]}
292
+ */
293
+ function normalizeLabels(labels) {
294
+ return (Array.isArray(labels) ? labels : [])
295
+ .map(label => (typeof label === "string" ? label : label?.name))
296
+ .filter(Boolean);
297
+ }
298
+
299
+ function printUsage() {
300
+ console.log(`Usage:
301
+ node plugins/lisa/scripts/project-ideation-idempotency-harness.mjs \\
302
+ --repo CodySwannGT/lisa \\
303
+ --marker "[lisa-project-ideation] idea=<stable-key>" \\
304
+ --command "codex exec '/lisa:project-ideation ./fixtures/project-ideation-idempotency prd_ready=true max_prds=1'" \\
305
+ --memory-file "$CODEX_HOME/automations/<automation_id>/memory.md"
306
+
307
+ The command must be deterministic: it should select the same idea and marker on
308
+ each run. When --memory-file is provided, the harness temporarily moves it aside
309
+ for the missing-memory variant and restores the original file afterward.`);
310
+ }
311
+
312
+ /**
313
+ * @param {string} message
314
+ * @returns {never}
315
+ */
316
+ function fail(message) {
317
+ console.error(message);
318
+ process.exit(1);
319
+ }
@@ -25,6 +25,10 @@ cannot verify yourself is noise — demote it honestly.
25
25
  creates **one** PRD (the single top-ranked idea), because `lisa:research` is a heavy full flow.
26
26
  `max_prds=3` creates the top three; `max_prds=all` creates one per build-ready idea. Discovery
27
27
  Spikes and Rejected ideas are never turned into PRDs regardless of `max_prds`.
28
+ - **`fixture=<path>`** (optional, verification-only) — a deterministic host-project fixture used
29
+ for idempotency verification. When present, read the fixture before ranking and honor its declared
30
+ single persona, single idea, existing-fit anchor, and expected dedupe marker. Do not use this
31
+ parameter for normal ideation runs.
28
32
 
29
33
  ## When to use
30
34
 
@@ -157,6 +161,26 @@ For each idea in the creation set, invoke `/lisa:research` with:
157
161
  `lisa:prd-source-write`. `project-ideation` never writes to the source directly — it delegates, so
158
162
  the PRD source stays switchable per project. Capture each returned PRD ref / URL / role / outcome.
159
163
 
164
+ ### Optional Codex automation memory
165
+
166
+ When the run has a Codex automation id or memory path, maintain a concise local advisory ledger after
167
+ the PRD source write returns. Resolve the memory path in this order:
168
+
169
+ 1. explicit `memory_file=<path>` or `automation_memory=<path>` argument, when supplied;
170
+ 2. `$CODEX_AUTOMATION_MEMORY`, when set;
171
+ 3. `$CODEX_HOME/automations/<automation_id>/memory.md`, when `automation_id=<id>` or
172
+ `$CODEX_AUTOMATION_ID` is available.
173
+
174
+ Create the parent directory and `memory.md` if missing. Write one concise run entry keyed by the
175
+ dedupe marker and run timestamp. The entry must include the marker, PRD URL/ref, outcome
176
+ (`created | reused | updated | blocked`), lifecycle role (`draft | ready | blocked` or the returned
177
+ source role), and `source_agreement` (`github-source-wins`, `memory-created`, `memory-updated`, or
178
+ `memory-missing-runtime`). If memory says one thing but the PRD source search finds a matching open
179
+ PRD, GitHub/source truth wins: reuse the source PRD and update memory rather than creating a
180
+ duplicate. Keep memory advisory only; never use it to override lifecycle labels, source marker
181
+ matches, or the PRD source writer's returned role. Do not store secrets, tokens, full PRD bodies, or
182
+ private source excerpts in memory.
183
+
160
184
  ### Dedupe marker (stable, never title-based)
161
185
 
162
186
  Each created PRD carries the marker `[lisa-project-ideation] idea=<stable-key>`. Compute
@@ -232,3 +256,6 @@ Use the markdown examples in `examples/` as shape references for the idea report
232
256
  requirements.
233
257
  - `unavailable-data-rejection.md` — naming missing private/paid/unavailable sources when demoting.
234
258
  - `evidence-card-format.md` — the required evidence fields every Practical Idea card must carry.
259
+ - `idempotency-verification-harness.md` — deterministic fixture and script procedure proving that
260
+ repeated `prd_ready=true` ideation keeps the open GitHub marker count at one, including the
261
+ missing-memory rerun variant.
@@ -0,0 +1,57 @@
1
+ # Project Ideation Idempotency Verification Harness
2
+
3
+ Use this documented harness when validating that `project-ideation prd_ready=true` reuses an
4
+ existing GitHub PRD by stable marker instead of creating duplicates.
5
+
6
+ ## Deterministic fixture
7
+
8
+ Create an isolated fixture project with only one evidence-backed persona and one build-ready idea.
9
+ The fixture should make the selected marker predictable:
10
+
11
+ - Repo identity: `CodySwannGT/lisa`
12
+ - Persona key: `queue-automation-operators`
13
+ - Idea name: `Exploratory PRD run ledger`
14
+ - Existing-fit anchor: `plugins/src/base/skills/project-ideation/SKILL.md`
15
+ - Expected marker: `[lisa-project-ideation] idea=codyswanngt-lisa-exploratory-prd-run-ledger-queue-automation-operators-plugins-lisa-skills-project-ideation-skill-md`
16
+
17
+ The fixture may be a tiny directory with:
18
+
19
+ - `README.md` naming queue automation operators and their goals.
20
+ - `.lisa.config.json` pointing `source` and `tracker` to GitHub.
21
+ - `PROJECT_IDEATION_FIXTURE.md` listing the single persona, the single idea, and the expected
22
+ marker above.
23
+
24
+ Invoke project ideation against that fixture with `prd_ready=true max_prds=1`. The fixture is valid
25
+ only if the idea report selects the single expected idea and the PRD summary reports the expected
26
+ marker.
27
+
28
+ ## Scripted verification
29
+
30
+ Run the shipped harness after choosing the deterministic command for your runtime:
31
+
32
+ ```bash
33
+ node plugins/lisa/scripts/project-ideation-idempotency-harness.mjs \
34
+ --repo CodySwannGT/lisa \
35
+ --marker "[lisa-project-ideation] idea=codyswanngt-lisa-exploratory-prd-run-ledger-queue-automation-operators-plugins-lisa-skills-project-ideation-skill-md" \
36
+ --command "codex exec '/lisa:project-ideation ./fixtures/project-ideation-idempotency prd_ready=true max_prds=1'" \
37
+ --memory-file "$CODEX_HOME/automations/<automation_id>/memory.md"
38
+ ```
39
+
40
+ The harness performs the acceptance check in three phases:
41
+
42
+ 1. Run the deterministic ideation command once, then query open GitHub issues for the marker and
43
+ require exactly one match.
44
+ 2. Run the same command again, then require the open marker count to remain one.
45
+ 3. Move the automation memory file aside, run the command again, require the marker count to remain
46
+ one, and require the memory file to be recreated with marker, PRD URL, outcome, lifecycle role,
47
+ and source agreement fields. The original memory file is restored at the end.
48
+
49
+ ## Expected evidence
50
+
51
+ Capture the harness JSON output as:
52
+
53
+ - `[EVIDENCE: marker-count-one]` from the first and second marker-count checks.
54
+ - `[EVIDENCE: memory-recreated-after-rerun]` from the missing-memory variant.
55
+
56
+ The run passes only when all reported counts are `1`, the issue URL is the same across phases, and
57
+ `memoryRecreated` and `memoryFieldsRecorded` are `true` when `--memory-file` is supplied.