ralph-lisa-loop 0.3.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/README.md +234 -0
- package/dist/cli.d.ts +8 -0
- package/dist/cli.js +142 -0
- package/dist/commands.d.ts +22 -0
- package/dist/commands.js +1812 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +21 -0
- package/dist/policy.d.ts +28 -0
- package/dist/policy.js +112 -0
- package/dist/state.d.ts +24 -0
- package/dist/state.js +150 -0
- package/dist/test/cli.test.d.ts +1 -0
- package/dist/test/cli.test.js +594 -0
- package/dist/test/policy.test.d.ts +1 -0
- package/dist/test/policy.test.js +130 -0
- package/dist/test/state.test.d.ts +1 -0
- package/dist/test/state.test.js +82 -0
- package/dist/test/watcher.test.d.ts +12 -0
- package/dist/test/watcher.test.js +247 -0
- package/package.json +44 -0
- package/templates/claude-commands/check-turn.md +18 -0
- package/templates/claude-commands/next-step.md +23 -0
- package/templates/claude-commands/read-review.md +11 -0
- package/templates/claude-commands/submit-work.md +39 -0
- package/templates/claude-commands/view-status.md +18 -0
- package/templates/codex-skills/check-turn.md +16 -0
- package/templates/codex-skills/read-work.md +9 -0
- package/templates/codex-skills/submit-review.md +36 -0
- package/templates/codex-skills/view-status.md +16 -0
- package/templates/ralph-prompt.md +59 -0
- package/templates/roles/lisa.md +115 -0
- package/templates/roles/ralph.md +117 -0
- package/templates/skill.json +27 -0
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
const node_test_1 = require("node:test");
|
|
37
|
+
const assert = __importStar(require("node:assert"));
|
|
38
|
+
const policy_js_1 = require("../policy.js");
|
|
39
|
+
(0, node_test_1.describe)("checkRalph", () => {
|
|
40
|
+
(0, node_test_1.it)("warns when CODE missing Test Results and file:line", () => {
|
|
41
|
+
const violations = (0, policy_js_1.checkRalph)("CODE", "[CODE] Done\n\nImplemented it.");
|
|
42
|
+
assert.strictEqual(violations.length, 2);
|
|
43
|
+
assert.ok(violations.some((v) => v.rule === "test-results"));
|
|
44
|
+
assert.ok(violations.some((v) => v.rule === "file-line-ref"));
|
|
45
|
+
});
|
|
46
|
+
(0, node_test_1.it)("passes when CODE includes Test Results and file:line", () => {
|
|
47
|
+
const violations = (0, policy_js_1.checkRalph)("CODE", "[CODE] Done\n\nchanges in commands.ts:42\n\nTest Results\n- Passed");
|
|
48
|
+
assert.strictEqual(violations.length, 0);
|
|
49
|
+
});
|
|
50
|
+
(0, node_test_1.it)("warns when CODE has Test Results but no file:line", () => {
|
|
51
|
+
const violations = (0, policy_js_1.checkRalph)("CODE", "[CODE] Done\n\nTest Results\n- Passed");
|
|
52
|
+
assert.strictEqual(violations.length, 1);
|
|
53
|
+
assert.strictEqual(violations[0].rule, "file-line-ref");
|
|
54
|
+
});
|
|
55
|
+
(0, node_test_1.it)("warns when CODE has file:line but no Test Results", () => {
|
|
56
|
+
const violations = (0, policy_js_1.checkRalph)("CODE", "[CODE] Done\n\nChanged commands.ts:42");
|
|
57
|
+
assert.strictEqual(violations.length, 1);
|
|
58
|
+
assert.strictEqual(violations[0].rule, "test-results");
|
|
59
|
+
});
|
|
60
|
+
(0, node_test_1.it)("warns when FIX missing Test Results", () => {
|
|
61
|
+
const violations = (0, policy_js_1.checkRalph)("FIX", "[FIX] Fixed\n\nChanged code.");
|
|
62
|
+
assert.ok(violations.some((v) => v.rule === "test-results"));
|
|
63
|
+
});
|
|
64
|
+
(0, node_test_1.it)("no warnings for PLAN", () => {
|
|
65
|
+
const violations = (0, policy_js_1.checkRalph)("PLAN", "[PLAN] Plan\n\nDetails");
|
|
66
|
+
assert.strictEqual(violations.length, 0);
|
|
67
|
+
});
|
|
68
|
+
(0, node_test_1.it)("warns when RESEARCH has no substance", () => {
|
|
69
|
+
const violations = (0, policy_js_1.checkRalph)("RESEARCH", "[RESEARCH] Done");
|
|
70
|
+
assert.strictEqual(violations.length, 1);
|
|
71
|
+
assert.strictEqual(violations[0].rule, "research-content");
|
|
72
|
+
});
|
|
73
|
+
(0, node_test_1.it)("passes RESEARCH with 2+ English fields", () => {
|
|
74
|
+
const violations = (0, policy_js_1.checkRalph)("RESEARCH", "[RESEARCH] Done\n\nReference implementation: file.ts\nKey types: MyType\nVerification: tested");
|
|
75
|
+
assert.strictEqual(violations.length, 0);
|
|
76
|
+
});
|
|
77
|
+
(0, node_test_1.it)("passes RESEARCH with exactly 2 fields", () => {
|
|
78
|
+
const violations = (0, policy_js_1.checkRalph)("RESEARCH", "[RESEARCH] Done\n\nReference: file.ts\nVerification: curl tested");
|
|
79
|
+
assert.strictEqual(violations.length, 0);
|
|
80
|
+
});
|
|
81
|
+
(0, node_test_1.it)("passes RESEARCH with data format + verification fields", () => {
|
|
82
|
+
const violations = (0, policy_js_1.checkRalph)("RESEARCH", "[RESEARCH] Done\n\nData structure: { id: string }\nVerified: works");
|
|
83
|
+
assert.strictEqual(violations.length, 0);
|
|
84
|
+
});
|
|
85
|
+
(0, node_test_1.it)("warns RESEARCH with only 1 field", () => {
|
|
86
|
+
const violations = (0, policy_js_1.checkRalph)("RESEARCH", "[RESEARCH] Done\n\nReference: file.ts");
|
|
87
|
+
assert.strictEqual(violations.length, 1);
|
|
88
|
+
});
|
|
89
|
+
(0, node_test_1.it)("passes RESEARCH with substantial content (>3 lines) even without fields", () => {
|
|
90
|
+
const violations = (0, policy_js_1.checkRalph)("RESEARCH", "[RESEARCH] API analysis\n\nLine 1\nLine 2\nLine 3\nLine 4");
|
|
91
|
+
assert.strictEqual(violations.length, 0);
|
|
92
|
+
});
|
|
93
|
+
(0, node_test_1.it)("file:line not required for PLAN or RESEARCH", () => {
|
|
94
|
+
const planV = (0, policy_js_1.checkRalph)("PLAN", "[PLAN] Plan\n\nNo file refs here");
|
|
95
|
+
assert.strictEqual(planV.length, 0);
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
(0, node_test_1.describe)("checkLisa", () => {
|
|
99
|
+
(0, node_test_1.it)("warns when PASS has no reason and no file:line", () => {
|
|
100
|
+
const violations = (0, policy_js_1.checkLisa)("PASS", "[PASS] Looks good");
|
|
101
|
+
assert.strictEqual(violations.length, 2);
|
|
102
|
+
assert.ok(violations.some((v) => v.rule === "reason-required"));
|
|
103
|
+
assert.ok(violations.some((v) => v.rule === "file-line-ref"));
|
|
104
|
+
});
|
|
105
|
+
(0, node_test_1.it)("passes when PASS has reason and file:line", () => {
|
|
106
|
+
const violations = (0, policy_js_1.checkLisa)("PASS", "[PASS] Looks good\n\n- Clean code at commands.ts:42\n- Tests pass");
|
|
107
|
+
assert.strictEqual(violations.length, 0);
|
|
108
|
+
});
|
|
109
|
+
(0, node_test_1.it)("warns when PASS has reason but no file:line", () => {
|
|
110
|
+
const violations = (0, policy_js_1.checkLisa)("PASS", "[PASS] Looks good\n\n- Clean code\n- Tests pass");
|
|
111
|
+
assert.strictEqual(violations.length, 1);
|
|
112
|
+
assert.strictEqual(violations[0].rule, "file-line-ref");
|
|
113
|
+
});
|
|
114
|
+
(0, node_test_1.it)("warns when NEEDS_WORK has no reason", () => {
|
|
115
|
+
const violations = (0, policy_js_1.checkLisa)("NEEDS_WORK", "[NEEDS_WORK] Fix it");
|
|
116
|
+
assert.ok(violations.some((v) => v.rule === "reason-required"));
|
|
117
|
+
});
|
|
118
|
+
(0, node_test_1.it)("passes NEEDS_WORK with reason and file:line", () => {
|
|
119
|
+
const violations = (0, policy_js_1.checkLisa)("NEEDS_WORK", "[NEEDS_WORK] Fix it\n\n- Bug at policy.ts:30");
|
|
120
|
+
assert.strictEqual(violations.length, 0);
|
|
121
|
+
});
|
|
122
|
+
(0, node_test_1.it)("no warnings for DISCUSS", () => {
|
|
123
|
+
const violations = (0, policy_js_1.checkLisa)("DISCUSS", "[DISCUSS] About this");
|
|
124
|
+
assert.strictEqual(violations.length, 0);
|
|
125
|
+
});
|
|
126
|
+
(0, node_test_1.it)("no file:line required for CONSENSUS", () => {
|
|
127
|
+
const violations = (0, policy_js_1.checkLisa)("CONSENSUS", "[CONSENSUS] Agreed");
|
|
128
|
+
assert.strictEqual(violations.length, 0);
|
|
129
|
+
});
|
|
130
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
const node_test_1 = require("node:test");
|
|
37
|
+
const assert = __importStar(require("node:assert"));
|
|
38
|
+
const state_js_1 = require("../state.js");
|
|
39
|
+
(0, node_test_1.describe)("extractTag", () => {
|
|
40
|
+
(0, node_test_1.it)("extracts known tags", () => {
|
|
41
|
+
assert.strictEqual((0, state_js_1.extractTag)("[PLAN] My plan"), "PLAN");
|
|
42
|
+
assert.strictEqual((0, state_js_1.extractTag)("[CODE] Implementation done"), "CODE");
|
|
43
|
+
assert.strictEqual((0, state_js_1.extractTag)("[RESEARCH] Results"), "RESEARCH");
|
|
44
|
+
assert.strictEqual((0, state_js_1.extractTag)("[CHALLENGE] I disagree"), "CHALLENGE");
|
|
45
|
+
assert.strictEqual((0, state_js_1.extractTag)("[PASS] Looks good"), "PASS");
|
|
46
|
+
assert.strictEqual((0, state_js_1.extractTag)("[NEEDS_WORK] Fix this"), "NEEDS_WORK");
|
|
47
|
+
assert.strictEqual((0, state_js_1.extractTag)("[DISCUSS] Let's talk"), "DISCUSS");
|
|
48
|
+
assert.strictEqual((0, state_js_1.extractTag)("[QUESTION] What about?"), "QUESTION");
|
|
49
|
+
assert.strictEqual((0, state_js_1.extractTag)("[CONSENSUS] Agreed"), "CONSENSUS");
|
|
50
|
+
assert.strictEqual((0, state_js_1.extractTag)("[FIX] Fixed the bug"), "FIX");
|
|
51
|
+
});
|
|
52
|
+
(0, node_test_1.it)("returns empty for invalid tags", () => {
|
|
53
|
+
assert.strictEqual((0, state_js_1.extractTag)("No tag here"), "");
|
|
54
|
+
assert.strictEqual((0, state_js_1.extractTag)("[INVALID] Nope"), "");
|
|
55
|
+
assert.strictEqual((0, state_js_1.extractTag)(""), "");
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
(0, node_test_1.describe)("extractSummary", () => {
|
|
59
|
+
(0, node_test_1.it)("extracts summary after tag", () => {
|
|
60
|
+
assert.strictEqual((0, state_js_1.extractSummary)("[PLAN] My plan"), "My plan");
|
|
61
|
+
assert.strictEqual((0, state_js_1.extractSummary)("[CODE] Implementation done"), "Implementation done");
|
|
62
|
+
});
|
|
63
|
+
(0, node_test_1.it)("returns full line if no tag", () => {
|
|
64
|
+
assert.strictEqual((0, state_js_1.extractSummary)("No tag here"), "No tag here");
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
(0, node_test_1.describe)("VALID_TAGS", () => {
|
|
68
|
+
(0, node_test_1.it)("contains all expected tags", () => {
|
|
69
|
+
const tags = state_js_1.VALID_TAGS.split("|");
|
|
70
|
+
assert.ok(tags.includes("PLAN"));
|
|
71
|
+
assert.ok(tags.includes("RESEARCH"));
|
|
72
|
+
assert.ok(tags.includes("CODE"));
|
|
73
|
+
assert.ok(tags.includes("FIX"));
|
|
74
|
+
assert.ok(tags.includes("PASS"));
|
|
75
|
+
assert.ok(tags.includes("NEEDS_WORK"));
|
|
76
|
+
assert.ok(tags.includes("CHALLENGE"));
|
|
77
|
+
assert.ok(tags.includes("DISCUSS"));
|
|
78
|
+
assert.ok(tags.includes("QUESTION"));
|
|
79
|
+
assert.ok(tags.includes("CONSENSUS"));
|
|
80
|
+
assert.strictEqual(tags.length, 10);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Watcher state machine tests.
|
|
3
|
+
*
|
|
4
|
+
* The watcher is a bash script generated at runtime, so we can't unit-test it
|
|
5
|
+
* directly. Instead we test the state machine logic by simulating the key
|
|
6
|
+
* decision points in TypeScript:
|
|
7
|
+
*
|
|
8
|
+
* 1. LAST_TURN only updates on successful trigger (ack semantics)
|
|
9
|
+
* 2. Failure backoff: 10 → degraded (30s), 30 → ALERT
|
|
10
|
+
* 3. Interactive prompt pause/resume with dual-condition gate
|
|
11
|
+
*/
|
|
12
|
+
export {};
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Watcher state machine tests.
|
|
4
|
+
*
|
|
5
|
+
* The watcher is a bash script generated at runtime, so we can't unit-test it
|
|
6
|
+
* directly. Instead we test the state machine logic by simulating the key
|
|
7
|
+
* decision points in TypeScript:
|
|
8
|
+
*
|
|
9
|
+
* 1. LAST_TURN only updates on successful trigger (ack semantics)
|
|
10
|
+
* 2. Failure backoff: 10 → degraded (30s), 30 → ALERT
|
|
11
|
+
* 3. Interactive prompt pause/resume with dual-condition gate
|
|
12
|
+
*/
|
|
13
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
14
|
+
if (k2 === undefined) k2 = k;
|
|
15
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
16
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
17
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
18
|
+
}
|
|
19
|
+
Object.defineProperty(o, k2, desc);
|
|
20
|
+
}) : (function(o, m, k, k2) {
|
|
21
|
+
if (k2 === undefined) k2 = k;
|
|
22
|
+
o[k2] = m[k];
|
|
23
|
+
}));
|
|
24
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
25
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
26
|
+
}) : function(o, v) {
|
|
27
|
+
o["default"] = v;
|
|
28
|
+
});
|
|
29
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
30
|
+
var ownKeys = function(o) {
|
|
31
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
32
|
+
var ar = [];
|
|
33
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
34
|
+
return ar;
|
|
35
|
+
};
|
|
36
|
+
return ownKeys(o);
|
|
37
|
+
};
|
|
38
|
+
return function (mod) {
|
|
39
|
+
if (mod && mod.__esModule) return mod;
|
|
40
|
+
var result = {};
|
|
41
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
42
|
+
__setModuleDefault(result, mod);
|
|
43
|
+
return result;
|
|
44
|
+
};
|
|
45
|
+
})();
|
|
46
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
47
|
+
const node_test_1 = require("node:test");
|
|
48
|
+
const assert = __importStar(require("node:assert"));
|
|
49
|
+
function newState() {
|
|
50
|
+
return {
|
|
51
|
+
seenTurn: "",
|
|
52
|
+
ackedTurn: "",
|
|
53
|
+
failCount: 0,
|
|
54
|
+
panePromptHits: 0,
|
|
55
|
+
panePaused: false,
|
|
56
|
+
panePauseSize: 0,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Simulate check_and_trigger logic (matches bash watcher v2).
|
|
61
|
+
* Two-variable approach: seenTurn (observed) vs ackedTurn (delivered).
|
|
62
|
+
* triggerResult: true = trigger succeeded, false = failed.
|
|
63
|
+
* Returns the action taken.
|
|
64
|
+
*/
|
|
65
|
+
function checkAndTrigger(state, currentTurn, triggerResult) {
|
|
66
|
+
// Detect new turn change
|
|
67
|
+
if (currentTurn && currentTurn !== state.seenTurn) {
|
|
68
|
+
state.seenTurn = currentTurn;
|
|
69
|
+
state.failCount = 0;
|
|
70
|
+
}
|
|
71
|
+
// Need to deliver? (seen but not acked)
|
|
72
|
+
if (state.seenTurn && state.seenTurn !== state.ackedTurn) {
|
|
73
|
+
let mode = "retry";
|
|
74
|
+
if (state.failCount >= 30) {
|
|
75
|
+
mode = "alert";
|
|
76
|
+
}
|
|
77
|
+
else if (state.failCount >= 10) {
|
|
78
|
+
mode = "degraded";
|
|
79
|
+
}
|
|
80
|
+
if (triggerResult) {
|
|
81
|
+
state.ackedTurn = state.seenTurn;
|
|
82
|
+
state.failCount = 0;
|
|
83
|
+
return "ack";
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
state.failCount++;
|
|
87
|
+
return mode;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return "noop";
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Simulate interactive prompt pause/resume logic.
|
|
94
|
+
* Returns whether send_go should proceed.
|
|
95
|
+
*/
|
|
96
|
+
function handleInteractivePrompt(state, promptDetected, outputChanged, currentLogSize) {
|
|
97
|
+
if (state.panePaused) {
|
|
98
|
+
// Resume requires BOTH: output changed AND prompt gone
|
|
99
|
+
if (outputChanged && currentLogSize !== state.panePauseSize && !promptDetected) {
|
|
100
|
+
state.panePaused = false;
|
|
101
|
+
state.panePromptHits = 0;
|
|
102
|
+
return true; // resumed, proceed
|
|
103
|
+
}
|
|
104
|
+
return false; // still paused
|
|
105
|
+
}
|
|
106
|
+
if (promptDetected) {
|
|
107
|
+
state.panePromptHits++;
|
|
108
|
+
if (state.panePromptHits >= 3) {
|
|
109
|
+
state.panePaused = true;
|
|
110
|
+
state.panePauseSize = currentLogSize;
|
|
111
|
+
}
|
|
112
|
+
return false; // don't send
|
|
113
|
+
}
|
|
114
|
+
state.panePromptHits = 0;
|
|
115
|
+
return true; // proceed
|
|
116
|
+
}
|
|
117
|
+
// ─── Tests ───────────────────────────────────────
|
|
118
|
+
(0, node_test_1.describe)("Watcher: ack semantics (seenTurn vs ackedTurn)", () => {
|
|
119
|
+
(0, node_test_1.it)("updates ackedTurn only on successful trigger", () => {
|
|
120
|
+
const s = newState();
|
|
121
|
+
const action = checkAndTrigger(s, "ralph", true);
|
|
122
|
+
assert.strictEqual(action, "ack");
|
|
123
|
+
assert.strictEqual(s.ackedTurn, "ralph");
|
|
124
|
+
assert.strictEqual(s.seenTurn, "ralph");
|
|
125
|
+
});
|
|
126
|
+
(0, node_test_1.it)("does NOT update ackedTurn on failed trigger", () => {
|
|
127
|
+
const s = newState();
|
|
128
|
+
const action = checkAndTrigger(s, "ralph", false);
|
|
129
|
+
assert.strictEqual(action, "retry");
|
|
130
|
+
assert.strictEqual(s.seenTurn, "ralph"); // seen
|
|
131
|
+
assert.strictEqual(s.ackedTurn, ""); // NOT acked
|
|
132
|
+
assert.strictEqual(s.failCount, 1);
|
|
133
|
+
});
|
|
134
|
+
(0, node_test_1.it)("retries on next cycle after failure (same turn)", () => {
|
|
135
|
+
const s = newState();
|
|
136
|
+
checkAndTrigger(s, "ralph", false); // fail
|
|
137
|
+
assert.strictEqual(s.ackedTurn, "");
|
|
138
|
+
assert.strictEqual(s.failCount, 1);
|
|
139
|
+
// Same turn, seen != acked, so it retries
|
|
140
|
+
const action = checkAndTrigger(s, "ralph", true);
|
|
141
|
+
assert.strictEqual(action, "ack");
|
|
142
|
+
assert.strictEqual(s.ackedTurn, "ralph");
|
|
143
|
+
assert.strictEqual(s.failCount, 0);
|
|
144
|
+
});
|
|
145
|
+
(0, node_test_1.it)("resets failCount on new turn change", () => {
|
|
146
|
+
const s = newState();
|
|
147
|
+
s.seenTurn = "ralph";
|
|
148
|
+
s.ackedTurn = "ralph";
|
|
149
|
+
s.failCount = 5;
|
|
150
|
+
const action = checkAndTrigger(s, "lisa", true);
|
|
151
|
+
assert.strictEqual(action, "ack");
|
|
152
|
+
assert.strictEqual(s.failCount, 0);
|
|
153
|
+
assert.strictEqual(s.ackedTurn, "lisa");
|
|
154
|
+
});
|
|
155
|
+
(0, node_test_1.it)("noop when turn unchanged and already acked", () => {
|
|
156
|
+
const s = newState();
|
|
157
|
+
s.seenTurn = "ralph";
|
|
158
|
+
s.ackedTurn = "ralph";
|
|
159
|
+
const action = checkAndTrigger(s, "ralph", true);
|
|
160
|
+
assert.strictEqual(action, "noop");
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
(0, node_test_1.describe)("Watcher: failure backoff", () => {
|
|
164
|
+
(0, node_test_1.it)("enters degraded mode after 10 failures", () => {
|
|
165
|
+
const s = newState();
|
|
166
|
+
// 10 consecutive failures
|
|
167
|
+
for (let i = 0; i < 10; i++) {
|
|
168
|
+
checkAndTrigger(s, "ralph", false);
|
|
169
|
+
}
|
|
170
|
+
assert.strictEqual(s.failCount, 10);
|
|
171
|
+
// Next failure should be degraded
|
|
172
|
+
const action = checkAndTrigger(s, "ralph", false);
|
|
173
|
+
assert.strictEqual(action, "degraded");
|
|
174
|
+
assert.strictEqual(s.failCount, 11);
|
|
175
|
+
});
|
|
176
|
+
(0, node_test_1.it)("enters alert mode after 30 failures", () => {
|
|
177
|
+
const s = newState();
|
|
178
|
+
for (let i = 0; i < 30; i++) {
|
|
179
|
+
checkAndTrigger(s, "ralph", false);
|
|
180
|
+
}
|
|
181
|
+
assert.strictEqual(s.failCount, 30);
|
|
182
|
+
const action = checkAndTrigger(s, "ralph", false);
|
|
183
|
+
assert.strictEqual(action, "alert");
|
|
184
|
+
assert.strictEqual(s.failCount, 31);
|
|
185
|
+
});
|
|
186
|
+
(0, node_test_1.it)("recovers from degraded on success", () => {
|
|
187
|
+
const s = newState();
|
|
188
|
+
for (let i = 0; i < 15; i++) {
|
|
189
|
+
checkAndTrigger(s, "ralph", false);
|
|
190
|
+
}
|
|
191
|
+
assert.ok(s.failCount >= 10);
|
|
192
|
+
const action = checkAndTrigger(s, "ralph", true);
|
|
193
|
+
assert.strictEqual(action, "ack");
|
|
194
|
+
assert.strictEqual(s.failCount, 0);
|
|
195
|
+
assert.strictEqual(s.ackedTurn, "ralph");
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
(0, node_test_1.describe)("Watcher: interactive prompt pause/resume", () => {
|
|
199
|
+
(0, node_test_1.it)("pauses after 3 consecutive prompt detections", () => {
|
|
200
|
+
const s = newState();
|
|
201
|
+
handleInteractivePrompt(s, true, false, 100);
|
|
202
|
+
assert.strictEqual(s.panePromptHits, 1);
|
|
203
|
+
assert.strictEqual(s.panePaused, false);
|
|
204
|
+
handleInteractivePrompt(s, true, false, 100);
|
|
205
|
+
assert.strictEqual(s.panePromptHits, 2);
|
|
206
|
+
handleInteractivePrompt(s, true, false, 100);
|
|
207
|
+
assert.strictEqual(s.panePromptHits, 3);
|
|
208
|
+
assert.strictEqual(s.panePaused, true);
|
|
209
|
+
assert.strictEqual(s.panePauseSize, 100);
|
|
210
|
+
});
|
|
211
|
+
(0, node_test_1.it)("does not resume with only output change (prompt still present)", () => {
|
|
212
|
+
const s = newState();
|
|
213
|
+
s.panePaused = true;
|
|
214
|
+
s.panePauseSize = 100;
|
|
215
|
+
// Output changed but prompt still there
|
|
216
|
+
const proceed = handleInteractivePrompt(s, true, true, 200);
|
|
217
|
+
assert.strictEqual(proceed, false);
|
|
218
|
+
assert.strictEqual(s.panePaused, true);
|
|
219
|
+
});
|
|
220
|
+
(0, node_test_1.it)("does not resume with only prompt gone (no output change)", () => {
|
|
221
|
+
const s = newState();
|
|
222
|
+
s.panePaused = true;
|
|
223
|
+
s.panePauseSize = 100;
|
|
224
|
+
// Prompt gone but output unchanged
|
|
225
|
+
const proceed = handleInteractivePrompt(s, false, false, 100);
|
|
226
|
+
assert.strictEqual(proceed, false);
|
|
227
|
+
assert.strictEqual(s.panePaused, true);
|
|
228
|
+
});
|
|
229
|
+
(0, node_test_1.it)("resumes when BOTH output changed AND prompt gone", () => {
|
|
230
|
+
const s = newState();
|
|
231
|
+
s.panePaused = true;
|
|
232
|
+
s.panePauseSize = 100;
|
|
233
|
+
const proceed = handleInteractivePrompt(s, false, true, 200);
|
|
234
|
+
assert.strictEqual(proceed, true);
|
|
235
|
+
assert.strictEqual(s.panePaused, false);
|
|
236
|
+
assert.strictEqual(s.panePromptHits, 0);
|
|
237
|
+
});
|
|
238
|
+
(0, node_test_1.it)("resets prompt hits when no prompt detected", () => {
|
|
239
|
+
const s = newState();
|
|
240
|
+
handleInteractivePrompt(s, true, false, 100);
|
|
241
|
+
handleInteractivePrompt(s, true, false, 100);
|
|
242
|
+
assert.strictEqual(s.panePromptHits, 2);
|
|
243
|
+
// No prompt this time
|
|
244
|
+
handleInteractivePrompt(s, false, false, 100);
|
|
245
|
+
assert.strictEqual(s.panePromptHits, 0);
|
|
246
|
+
});
|
|
247
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ralph-lisa-loop",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Turn-based dual-agent collaboration: Ralph codes, Lisa reviews, consensus required.",
|
|
5
|
+
"bin": {
|
|
6
|
+
"ralph-lisa": "dist/cli.js"
|
|
7
|
+
},
|
|
8
|
+
"main": "./dist/index.js",
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "tsc",
|
|
11
|
+
"test": "node --test dist/test/*.js",
|
|
12
|
+
"prepack": "npm run build"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist/",
|
|
16
|
+
"templates/"
|
|
17
|
+
],
|
|
18
|
+
"keywords": [
|
|
19
|
+
"dual-agent",
|
|
20
|
+
"collaboration",
|
|
21
|
+
"code-review",
|
|
22
|
+
"claude",
|
|
23
|
+
"codex",
|
|
24
|
+
"ai-agents"
|
|
25
|
+
],
|
|
26
|
+
"author": "YW1975",
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "git+https://github.com/YW1975/Ralph-Lisa-Loop.git",
|
|
31
|
+
"directory": "cli"
|
|
32
|
+
},
|
|
33
|
+
"homepage": "https://github.com/YW1975/Ralph-Lisa-Loop#readme",
|
|
34
|
+
"bugs": {
|
|
35
|
+
"url": "https://github.com/YW1975/Ralph-Lisa-Loop/issues"
|
|
36
|
+
},
|
|
37
|
+
"engines": {
|
|
38
|
+
"node": ">=18"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"typescript": "^5.4.0",
|
|
42
|
+
"@types/node": "^20.0.0"
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Check whose turn it is in Ralph-Lisa collaboration
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Check Turn
|
|
6
|
+
|
|
7
|
+
Check whose turn it is before taking any action.
|
|
8
|
+
|
|
9
|
+
```!
|
|
10
|
+
ralph-lisa whose-turn
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Rules
|
|
14
|
+
|
|
15
|
+
- If output is `ralph`: You can proceed with your work
|
|
16
|
+
- If output is `lisa`: STOP immediately and wait for Lisa's response
|
|
17
|
+
|
|
18
|
+
**NEVER skip this check before working.**
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Move to next implementation step
|
|
3
|
+
argument-hint: "step name"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Next Step
|
|
7
|
+
|
|
8
|
+
Enter a new implementation step after consensus is reached.
|
|
9
|
+
|
|
10
|
+
```!
|
|
11
|
+
ralph-lisa step "$ARGUMENTS"
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Usage
|
|
15
|
+
|
|
16
|
+
Only use this after:
|
|
17
|
+
1. Lisa gave PASS
|
|
18
|
+
2. Both parties confirmed consensus
|
|
19
|
+
|
|
20
|
+
Example:
|
|
21
|
+
```
|
|
22
|
+
/next-step implement login form
|
|
23
|
+
```
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Submit your work to Lisa for review (Ralph only)
|
|
3
|
+
argument-hint: "[TAG] summary and content"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Submit Work to Lisa
|
|
7
|
+
|
|
8
|
+
Submit your work and pass the turn to Lisa.
|
|
9
|
+
|
|
10
|
+
```!
|
|
11
|
+
ralph-lisa submit-ralph "$ARGUMENTS"
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Required Format
|
|
15
|
+
|
|
16
|
+
Your content MUST start with a tag and one-line summary:
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
[TAG] One line summary
|
|
20
|
+
|
|
21
|
+
Detailed content here...
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Valid Tags for Ralph
|
|
25
|
+
|
|
26
|
+
| Tag | When to Use |
|
|
27
|
+
|-----|-------------|
|
|
28
|
+
| `[PLAN]` | Submitting a plan |
|
|
29
|
+
| `[RESEARCH]` | Submitting research results (before coding, when involving reference implementations/protocols/APIs) |
|
|
30
|
+
| `[CODE]` | Submitting code implementation (must include Test Results) |
|
|
31
|
+
| `[FIX]` | Submitting fixes based on feedback (must include Test Results) |
|
|
32
|
+
| `[CHALLENGE]` | Disagreeing with Lisa's suggestion, providing counter-argument |
|
|
33
|
+
| `[DISCUSS]` | General discussion or clarification |
|
|
34
|
+
| `[QUESTION]` | Asking for clarification |
|
|
35
|
+
| `[CONSENSUS]` | Confirming agreement |
|
|
36
|
+
|
|
37
|
+
## After Submission
|
|
38
|
+
|
|
39
|
+
The turn automatically passes to Lisa. You must STOP and wait.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: View current collaboration status
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# View Status
|
|
6
|
+
|
|
7
|
+
See current task, turn, round, and last action.
|
|
8
|
+
|
|
9
|
+
```!
|
|
10
|
+
ralph-lisa status
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Output Shows
|
|
14
|
+
|
|
15
|
+
- Current task
|
|
16
|
+
- Whose turn it is
|
|
17
|
+
- Current round and step
|
|
18
|
+
- Last action summary
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Check Turn
|
|
2
|
+
|
|
3
|
+
Check whose turn it is before taking any action.
|
|
4
|
+
|
|
5
|
+
## Command
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
./mini-skill/io.sh whose-turn
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Rules
|
|
12
|
+
|
|
13
|
+
- If output is `lisa`: You can proceed with your review
|
|
14
|
+
- If output is `ralph`: STOP immediately and wait for Ralph's submission
|
|
15
|
+
|
|
16
|
+
**NEVER skip this check before working.**
|