nightralph 0.0.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/.claude/settings.local.json +5 -0
- package/AGENTS.md +60 -0
- package/CLAUDE.md +1 -0
- package/PROMPT.md +95 -0
- package/README.md +138 -0
- package/WIP.md +5 -0
- package/dist/docs-templates/domain.md +36 -0
- package/dist/docs-templates/issue-tracker-github.md +228 -0
- package/dist/docs-templates/issue-tracker.md +30 -0
- package/dist/docs-templates/triage-labels.md +15 -0
- package/dist/index.js +154 -0
- package/dist/index.js.map +7 -0
- package/dist/meta.json +207 -0
- package/dist/orchestrator.js +273 -0
- package/dist/orchestrator.js.map +7 -0
- package/dist/resolve.js +71 -0
- package/dist/resolve.js.map +7 -0
- package/dist/setup.js +151 -0
- package/dist/setup.js.map +7 -0
- package/dist/skills/domain-modeling/ADR-FORMAT.md +47 -0
- package/dist/skills/domain-modeling/CONTEXT-FORMAT.md +60 -0
- package/dist/skills/domain-modeling/SKILL.md +74 -0
- package/dist/skills/domain-modeling/agents/openai.yaml +3 -0
- package/dist/skills/grill/SKILL.md +7 -0
- package/dist/skills/grill/agents/openai.yaml +5 -0
- package/dist/skills/grilling/SKILL.md +28 -0
- package/dist/skills/grilling/agents/openai.yaml +3 -0
- package/dist/skills/tdd/SKILL.md +38 -0
- package/dist/skills/tdd/agents/openai.yaml +3 -0
- package/dist/skills/tdd/mocking.md +59 -0
- package/dist/skills/tdd/tests.md +77 -0
- package/dist/skills/to-spec/SKILL.md +75 -0
- package/dist/skills/to-spec/agents/openai.yaml +5 -0
- package/dist/skills/to-tickets/SKILL.md +105 -0
- package/dist/skills/to-tickets/agents/openai.yaml +5 -0
- package/dist/skills/upstream.json +5 -0
- package/dist/src/index.d.ts +4 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/orchestrator.d.ts +31 -0
- package/dist/src/orchestrator.d.ts.map +1 -0
- package/dist/src/resolve.d.ts +10 -0
- package/dist/src/resolve.d.ts.map +1 -0
- package/dist/src/setup.d.ts +13 -0
- package/dist/src/setup.d.ts.map +1 -0
- package/dist/test/integration.test.js +848 -0
- package/dist/test/integration.test.js.map +7 -0
- package/dist/test/orchestrator.test.js +749 -0
- package/dist/test/orchestrator.test.js.map +7 -0
- package/dist/test/setup.test.js +574 -0
- package/dist/test/setup.test.js.map +7 -0
- package/docs/README.md +12 -0
- package/docs/agents/domain.md +36 -0
- package/docs/agents/issue-tracker.md +30 -0
- package/docs/agents/triage-labels.md +15 -0
- package/docs/implementation-plans/2026-08-27-issue-tracker-choice/phase-2-future-work.md +144 -0
- package/docs/implementation-plans/2026-08-27-issue-tracker-choice/phase_01.md +292 -0
- package/docs/implementation-plans/2026-08-27-issue-tracker-choice/phase_02.md +212 -0
- package/docs/implementation-plans/2026-08-27-issue-tracker-choice/test-requirements.md +203 -0
- package/docs/skills.md +58 -0
- package/docs/test-plans/2026-08-27-issue-tracker-choice.md +95 -0
- package/eslint.config.js +142 -0
- package/lib.es5.d.ts +19 -0
- package/package.json +36 -0
- package/progress.log +190 -0
- package/ralph_claude.sh +263 -0
- package/scripts/clone-skills.sh +52 -0
- package/specs/prd.json +172 -0
- package/src/docs-templates/domain.md +36 -0
- package/src/docs-templates/issue-tracker-github.md +228 -0
- package/src/docs-templates/issue-tracker.md +30 -0
- package/src/docs-templates/triage-labels.md +15 -0
- package/src/index.ts +184 -0
- package/src/orchestrator.ts +324 -0
- package/src/resolve.ts +73 -0
- package/src/setup.ts +157 -0
- package/src/skills/domain-modeling/ADR-FORMAT.md +47 -0
- package/src/skills/domain-modeling/CONTEXT-FORMAT.md +60 -0
- package/src/skills/domain-modeling/SKILL.md +74 -0
- package/src/skills/domain-modeling/agents/openai.yaml +3 -0
- package/src/skills/grill/SKILL.md +7 -0
- package/src/skills/grill/agents/openai.yaml +5 -0
- package/src/skills/grilling/SKILL.md +28 -0
- package/src/skills/grilling/agents/openai.yaml +3 -0
- package/src/skills/tdd/SKILL.md +38 -0
- package/src/skills/tdd/agents/openai.yaml +3 -0
- package/src/skills/tdd/mocking.md +59 -0
- package/src/skills/tdd/tests.md +77 -0
- package/src/skills/to-spec/SKILL.md +75 -0
- package/src/skills/to-spec/agents/openai.yaml +5 -0
- package/src/skills/to-tickets/SKILL.md +105 -0
- package/src/skills/to-tickets/agents/openai.yaml +5 -0
- package/src/skills/upstream.json +5 -0
- package/tasks/prd-integration-test-coverage.md +243 -0
- package/test/integration.test.ts +636 -0
- package/test/mock-agent-echo-args.js +4 -0
- package/test/mock-agent-fail.js +3 -0
- package/test/mock-agent-pass.js +3 -0
- package/test/orchestrator.test.ts +609 -0
- package/test/setup.test.ts +417 -0
- package/tsconfig.build.json +7 -0
- package/tsconfig.json +31 -0
package/eslint.config.js
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import neostandard from 'newneostandard'
|
|
2
|
+
import tseslint from 'typescript-eslint'
|
|
3
|
+
|
|
4
|
+
// Local rule: forbid spaces around `|` and `&` in TypeScript union and
|
|
5
|
+
// intersection types, so `A|B` is required over `A | B`. Only flags
|
|
6
|
+
// operators whose operands share a line, leaving multiline unions (one
|
|
7
|
+
// member per line) untouched.
|
|
8
|
+
const localPlugin = {
|
|
9
|
+
rules: {
|
|
10
|
+
'union-spacing': {
|
|
11
|
+
meta: {
|
|
12
|
+
type: 'layout',
|
|
13
|
+
fixable: 'whitespace',
|
|
14
|
+
schema: [],
|
|
15
|
+
messages: {
|
|
16
|
+
before: "Unexpected space before '{{op}}' in type.",
|
|
17
|
+
after: "Unexpected space after '{{op}}' in type."
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
create (context) {
|
|
21
|
+
const sc = context.sourceCode
|
|
22
|
+
const check = (node) => {
|
|
23
|
+
for (const member of node.types) {
|
|
24
|
+
const op = sc.getTokenBefore(member)
|
|
25
|
+
if (!op) continue
|
|
26
|
+
if (op.value !== '|' && op.value !== '&') continue
|
|
27
|
+
const prev = sc.getTokenBefore(op)
|
|
28
|
+
const next = sc.getTokenAfter(op)
|
|
29
|
+
// Only enforce when the operator sits inline
|
|
30
|
+
// between two members on a single line; leave
|
|
31
|
+
// multiline unions (leading/trailing pipe) be.
|
|
32
|
+
const inline = prev && next &&
|
|
33
|
+
prev.loc.end.line === op.loc.start.line &&
|
|
34
|
+
op.loc.end.line === next.loc.start.line
|
|
35
|
+
if (!inline) continue
|
|
36
|
+
if (prev.range[1] !== op.range[0]) {
|
|
37
|
+
context.report({
|
|
38
|
+
node: op,
|
|
39
|
+
messageId: 'before',
|
|
40
|
+
data: { op: op.value },
|
|
41
|
+
fix: (fixer) => fixer.removeRange(
|
|
42
|
+
[prev.range[1], op.range[0]]
|
|
43
|
+
)
|
|
44
|
+
})
|
|
45
|
+
}
|
|
46
|
+
if (op.range[1] !== next.range[0]) {
|
|
47
|
+
context.report({
|
|
48
|
+
node: op,
|
|
49
|
+
messageId: 'after',
|
|
50
|
+
data: { op: op.value },
|
|
51
|
+
fix: (fixer) => fixer.removeRange(
|
|
52
|
+
[op.range[1], next.range[0]]
|
|
53
|
+
)
|
|
54
|
+
})
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
TSUnionType: check,
|
|
60
|
+
TSIntersectionType: check
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export default tseslint.config(
|
|
68
|
+
{
|
|
69
|
+
ignores: [
|
|
70
|
+
'lib.es5.d.ts',
|
|
71
|
+
'dist/**',
|
|
72
|
+
'public/**',
|
|
73
|
+
'test/*.js'
|
|
74
|
+
]
|
|
75
|
+
},
|
|
76
|
+
|
|
77
|
+
// JavaScript Standard Style, TypeScript-aware (flat-config successor
|
|
78
|
+
// to `eslint-config-standard`). Stylistic rules live under the
|
|
79
|
+
// `@stylistic/` namespace.
|
|
80
|
+
...neostandard({ ts: true }),
|
|
81
|
+
|
|
82
|
+
// Standard Style overrides, applied to every file.
|
|
83
|
+
{
|
|
84
|
+
rules: {
|
|
85
|
+
'@stylistic/operator-linebreak': 'off',
|
|
86
|
+
'@stylistic/multiline-ternary': 'off',
|
|
87
|
+
'@stylistic/no-multiple-empty-lines': ['error', {
|
|
88
|
+
max: 1,
|
|
89
|
+
maxEOF: 1
|
|
90
|
+
}],
|
|
91
|
+
'@stylistic/indent': ['error', 4, {
|
|
92
|
+
SwitchCase: 1,
|
|
93
|
+
ignoredNodes: ['TemplateLiteral *']
|
|
94
|
+
}],
|
|
95
|
+
'@stylistic/comma-dangle': 'off',
|
|
96
|
+
'@stylistic/no-multi-spaces': ['error', {
|
|
97
|
+
ignoreEOLComments: true
|
|
98
|
+
}]
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
|
|
102
|
+
// `@typescript-eslint/recommended` plus our TypeScript overrides,
|
|
103
|
+
// scoped to TypeScript files where the parser and plugin are active.
|
|
104
|
+
{
|
|
105
|
+
files: ['**/*.ts', '**/*.tsx'],
|
|
106
|
+
extends: [tseslint.configs.recommended],
|
|
107
|
+
plugins: { local: localPlugin },
|
|
108
|
+
rules: {
|
|
109
|
+
'@typescript-eslint/no-explicit-any': 'off',
|
|
110
|
+
'@typescript-eslint/no-unused-vars': ['error', {
|
|
111
|
+
argsIgnorePattern: '^_',
|
|
112
|
+
varsIgnorePattern: '^_',
|
|
113
|
+
caughtErrorsIgnorePattern: '^_'
|
|
114
|
+
}],
|
|
115
|
+
'@typescript-eslint/consistent-type-imports': ['error', {
|
|
116
|
+
prefer: 'type-imports'
|
|
117
|
+
}],
|
|
118
|
+
// No space around the colon in type annotations
|
|
119
|
+
// (`const x:T`, params, returns). Keep `=>` in function
|
|
120
|
+
// types spaced via the arrow override.
|
|
121
|
+
'@stylistic/type-annotation-spacing': ['error', {
|
|
122
|
+
before: false,
|
|
123
|
+
after: false,
|
|
124
|
+
overrides: { arrow: 'ignore' }
|
|
125
|
+
}],
|
|
126
|
+
'@stylistic/arrow-spacing': ['error', {
|
|
127
|
+
before: true,
|
|
128
|
+
after: true
|
|
129
|
+
}],
|
|
130
|
+
// No space around the colon in object and type-literal
|
|
131
|
+
// members (`{ a:1 }`, `classes?:string[]`).
|
|
132
|
+
'@stylistic/key-spacing': ['error', {
|
|
133
|
+
beforeColon: false,
|
|
134
|
+
afterColon: false,
|
|
135
|
+
mode: 'strict'
|
|
136
|
+
}],
|
|
137
|
+
// Let `local/union-spacing` own union/intersection spacing.
|
|
138
|
+
'@stylistic/space-infix-ops': ['error', { ignoreTypes: true }],
|
|
139
|
+
'local/union-spacing': 'error'
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
)
|
package/lib.es5.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** See https://stackoverflow.com/a/51390763/1470607 */
|
|
2
|
+
type Falsy = false | 0 | '' | null | undefined;
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* see https://www.karltarvas.com/typescript-array-filter-boolean.html
|
|
6
|
+
*/
|
|
7
|
+
interface Array<T> {
|
|
8
|
+
/**
|
|
9
|
+
* Returns the elements of an array that meet the condition specified in a
|
|
10
|
+
* callback function.
|
|
11
|
+
* @param predicate A function that accepts up to three arguments. The filter
|
|
12
|
+
* method calls the predicate function one time for each element in the array.
|
|
13
|
+
* @param thisArg An object to which the this keyword can refer in the
|
|
14
|
+
* predicate function. If thisArg is omitted, undefined is used as the
|
|
15
|
+
* this value.
|
|
16
|
+
*/
|
|
17
|
+
filter<S extends T>(predicate: BooleanConstructor, thisArg?: any)
|
|
18
|
+
: Exclude<S, Falsy>[];
|
|
19
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "nightralph",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Small orchestrator that runs a single-agent session per issue.",
|
|
6
|
+
"bin": {
|
|
7
|
+
"nightralph": "dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"engines": {
|
|
10
|
+
"node": ">=18"
|
|
11
|
+
},
|
|
12
|
+
"scripts": {
|
|
13
|
+
"lint": "eslint \"./**/*.{ts,js}\"",
|
|
14
|
+
"start": "npm run build && node dist/index.js",
|
|
15
|
+
"build": "mkdir -p ./dist && rm -rf ./dist/* && npm run build-esm && npm run copy",
|
|
16
|
+
"build-esm": "esbuild src/index.ts src/orchestrator.ts src/setup.ts src/resolve.ts --format=esm --metafile=dist/meta.json --keep-names --tsconfig=tsconfig.build.json --outdir=./dist --sourcemap && tsc --emitDeclarationOnly --project tsconfig.build.json",
|
|
17
|
+
"copy": "mkdir -p ./dist/skills ./dist/docs-templates && cp -r ./src/skills/* ./dist/skills && cp -r ./src/docs-templates/* ./dist/docs-templates",
|
|
18
|
+
"clone-skills": "bash scripts/clone-skills.sh",
|
|
19
|
+
"toc": "markdown-toc --maxdepth 3 -i README.md",
|
|
20
|
+
"test": "esbuild test/setup.test.ts test/orchestrator.test.ts test/integration.test.ts --format=esm --bundle --packages=external --outdir=dist/test --sourcemap && node dist/test/setup.test.js && node dist/test/orchestrator.test.js && node dist/test/integration.test.js"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@substrate-system/tapzero": "^0.10.16",
|
|
24
|
+
"@types/node": "^26.3.0",
|
|
25
|
+
"esbuild": "^0.28.2",
|
|
26
|
+
"eslint": "^10.9.1",
|
|
27
|
+
"markdown-toc": "^1.2.0",
|
|
28
|
+
"newneostandard": "^0.14.3",
|
|
29
|
+
"typescript-eslint": "^8.68.0"
|
|
30
|
+
},
|
|
31
|
+
"author": "nichoth",
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"@inquirer/prompts": "^8.7.0",
|
|
34
|
+
"yargs": "^18.1.0"
|
|
35
|
+
}
|
|
36
|
+
}
|
package/progress.log
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
## 2026-08-27 - US-001: Unit tests for findFeatureDirs
|
|
2
|
+
|
|
3
|
+
Completed. Created test/integration.test.ts with 4 test cases (8 assertions):
|
|
4
|
+
- Returns empty array when .scratch/ does not exist
|
|
5
|
+
- Returns empty array when dirs lack issues/ subdirs
|
|
6
|
+
- Returns correct name, dir, spec for valid feature dirs
|
|
7
|
+
- Ignores non-directory entries in .scratch/
|
|
8
|
+
|
|
9
|
+
Refactoring note: extracted findFeatureDirs and resolveIssuesDir from
|
|
10
|
+
src/index.ts into src/resolve.ts. The top-level yargs(...).parse() call in
|
|
11
|
+
index.ts runs on import, which crashes test processes. Moving the pure
|
|
12
|
+
functions to resolve.ts lets tests import them without triggering CLI
|
|
13
|
+
parsing. index.ts re-exports both functions so the public API is unchanged.
|
|
14
|
+
|
|
15
|
+
Updated package.json:
|
|
16
|
+
- build-esm: added src/resolve.ts to esbuild entry points
|
|
17
|
+
- test: added integration.test.ts to build and run steps
|
|
18
|
+
|
|
19
|
+
All 128 tests pass (setup: 66, orchestrator: 54, integration: 8).
|
|
20
|
+
Lint clean.
|
|
21
|
+
|
|
22
|
+
## 2026-08-27 - US-002: Unit tests for resolveIssuesDir with explicit flags
|
|
23
|
+
|
|
24
|
+
Completed. Added 2 test cases (4 assertions) to test/integration.test.ts:
|
|
25
|
+
- Returns { dir, spec } exactly as given when both flags are provided
|
|
26
|
+
- No filesystem scanning occurs (temp dir has no .scratch/)
|
|
27
|
+
|
|
28
|
+
Both tests confirm that resolveIssuesDir short-circuits when both --dir
|
|
29
|
+
and --spec are supplied, returning them directly without touching the
|
|
30
|
+
filesystem. The temp directory intentionally has no .scratch/ to prove
|
|
31
|
+
no scanning happens.
|
|
32
|
+
|
|
33
|
+
All 132 tests pass (setup: 66, orchestrator: 54, integration: 12).
|
|
34
|
+
Lint clean.
|
|
35
|
+
|
|
36
|
+
## 2026-08-28 - US-003: Subprocess CLI smoke test for execute
|
|
37
|
+
|
|
38
|
+
Completed. Added 1 subprocess test (5 assertions) to
|
|
39
|
+
test/integration.test.ts:
|
|
40
|
+
- Spawns `node dist/index.js` with mock-agent-pass.js as provider
|
|
41
|
+
- Creates temp dir with 2 ticket files (second blocked by first)
|
|
42
|
+
and a spec.md
|
|
43
|
+
- Passes --dir, --spec, --timeout-secs 10 to the built CLI
|
|
44
|
+
- Asserts exit code 0
|
|
45
|
+
- Asserts both ticket files updated to **Status:** done on disk
|
|
46
|
+
- Asserts log files created for both tickets
|
|
47
|
+
|
|
48
|
+
Added helper functions: spawnCli (wraps execFile in a promise),
|
|
49
|
+
makeTicketBody (generates ticket markdown with status and blockers).
|
|
50
|
+
Also added readFileSync, existsSync, execFile imports to the test file.
|
|
51
|
+
|
|
52
|
+
All 137 tests pass (setup: 66, orchestrator: 54, integration: 17).
|
|
53
|
+
Lint clean.
|
|
54
|
+
|
|
55
|
+
## 2026-08-28 - US-004: Create mock-agent-echo-args and test --model flag
|
|
56
|
+
|
|
57
|
+
Completed. Created test/mock-agent-echo-args.js and added 1 test case
|
|
58
|
+
(4 assertions) to test/integration.test.ts:
|
|
59
|
+
- mock-agent-echo-args.js writes JSON.stringify(process.argv) to stdout,
|
|
60
|
+
then reads stdin to completion and exits 0
|
|
61
|
+
- Test spawns CLI with -m test-model using the echo mock agent
|
|
62
|
+
- Asserts exit code 0
|
|
63
|
+
- Asserts log file created
|
|
64
|
+
- Asserts log contains --model flag and test-model value
|
|
65
|
+
|
|
66
|
+
Gotcha: new mock agent files need chmod +x to be spawnable by the
|
|
67
|
+
orchestrator (spawn uses the shebang, not node directly).
|
|
68
|
+
|
|
69
|
+
All 141 tests pass (setup: 66, orchestrator: 54, integration: 21).
|
|
70
|
+
Lint clean.
|
|
71
|
+
|
|
72
|
+
## 2026-08-28 - US-005: Subprocess CLI test for --dry-run
|
|
73
|
+
|
|
74
|
+
Completed. Added 1 subprocess test (6 assertions) to
|
|
75
|
+
test/integration.test.ts:
|
|
76
|
+
- Spawns `node dist/index.js` with --dry-run, --dir, --spec flags
|
|
77
|
+
- Creates temp dir with 2 ticket files (second blocked by first)
|
|
78
|
+
and a spec.md
|
|
79
|
+
- Asserts exit code 0
|
|
80
|
+
- Asserts stdout contains Wave 1 with 01-alpha.md
|
|
81
|
+
- Asserts stdout contains Wave 2 with 02-beta.md
|
|
82
|
+
- Asserts stdout contains AGENT INSTRUCTIONS (prompt preview)
|
|
83
|
+
- Asserts both ticket files are NOT modified (still ready-for-agent)
|
|
84
|
+
|
|
85
|
+
All 147 tests pass (setup: 66, orchestrator: 54, integration: 27).
|
|
86
|
+
Lint clean.
|
|
87
|
+
|
|
88
|
+
## 2026-08-28 - US-006: CLI exits gracefully when no tickets found
|
|
89
|
+
|
|
90
|
+
Completed. Added 1 subprocess test (2 assertions) to
|
|
91
|
+
test/integration.test.ts:
|
|
92
|
+
- Spawns `node dist/index.js` with --dir pointing at an empty
|
|
93
|
+
issues directory and a valid --spec
|
|
94
|
+
- Asserts exit code 0 (graceful, not a crash)
|
|
95
|
+
- Asserts stdout contains "No ticket files found"
|
|
96
|
+
|
|
97
|
+
The implementation already existed in src/index.ts (lines 148-150):
|
|
98
|
+
scanTickets returns an empty array for an empty directory, and
|
|
99
|
+
runExecute prints "No ticket files found. Exiting." then returns
|
|
100
|
+
normally (exit code 0). This test validates that behavior end-to-end
|
|
101
|
+
through the subprocess CLI.
|
|
102
|
+
|
|
103
|
+
All 149 tests pass (setup: 66, orchestrator: 54, integration: 29).
|
|
104
|
+
Lint clean.
|
|
105
|
+
|
|
106
|
+
## 2026-08-28 - US-007: CLI exits with error for missing dir/spec
|
|
107
|
+
|
|
108
|
+
Completed. Added 2 subprocess tests (4 assertions) to
|
|
109
|
+
test/integration.test.ts:
|
|
110
|
+
- Test 1: passes --dir /nonexistent/path with a valid --spec file,
|
|
111
|
+
asserts non-zero exit code and stderr contains "not found"
|
|
112
|
+
- Test 2: passes valid --dir with --spec /nonexistent/spec.md,
|
|
113
|
+
asserts non-zero exit code and stderr contains "not found"
|
|
114
|
+
|
|
115
|
+
The validation logic already existed in src/index.ts (lines 136-143):
|
|
116
|
+
runExecute checks existsSync(dir) and existsSync(spec), prints
|
|
117
|
+
"Directory not found: ..." or "Spec file not found: ..." to stderr,
|
|
118
|
+
and calls process.exit(2). These tests validate that behavior
|
|
119
|
+
end-to-end through the subprocess CLI.
|
|
120
|
+
|
|
121
|
+
All 153 tests pass (setup: 66, orchestrator: 54, integration: 33).
|
|
122
|
+
Lint clean.
|
|
123
|
+
|
|
124
|
+
## 2026-08-28 - US-008: Unknown blocker warning and stripping
|
|
125
|
+
|
|
126
|
+
Completed. Added 1 subprocess test (5 assertions) to
|
|
127
|
+
test/integration.test.ts:
|
|
128
|
+
- Creates a ticket with **Blocked by:** 99 where ticket 99 does not
|
|
129
|
+
exist in the issues directory
|
|
130
|
+
- Spawns the CLI as a subprocess with mock-agent-pass.js
|
|
131
|
+
- Asserts combined stdout+stderr contains "99" (the unknown blocker
|
|
132
|
+
number) and "not found" (the warning message)
|
|
133
|
+
- Asserts exit code 0 (ticket completed despite invalid blocker)
|
|
134
|
+
- Asserts ticket file updated to **Status:** done (blocker stripped)
|
|
135
|
+
- Asserts log file created
|
|
136
|
+
|
|
137
|
+
The warning and stripping logic already existed in src/index.ts
|
|
138
|
+
(lines 155-170): runExecute builds a Set of known ticket numbers,
|
|
139
|
+
iterates tickets to find blockers referencing unknown numbers,
|
|
140
|
+
console.warn's the warning, then filters the blockers array to only
|
|
141
|
+
known numbers. This test validates that behavior end-to-end through
|
|
142
|
+
the subprocess CLI.
|
|
143
|
+
|
|
144
|
+
All 158 tests pass (setup: 66, orchestrator: 54, integration: 38).
|
|
145
|
+
Lint clean.
|
|
146
|
+
|
|
147
|
+
## 2026-08-28 - US-009: Dry-run deadlock detection
|
|
148
|
+
|
|
149
|
+
Completed. Added 1 subprocess test (4 assertions) to
|
|
150
|
+
test/integration.test.ts:
|
|
151
|
+
- Creates two tickets with circular dependencies (1 blocks 2,
|
|
152
|
+
2 blocks 1)
|
|
153
|
+
- Spawns the CLI with --dry-run
|
|
154
|
+
- Asserts exit code 0
|
|
155
|
+
- Asserts stdout contains "Deadlocked tickets"
|
|
156
|
+
- Asserts stdout lists both ticket filenames (01-alpha.md,
|
|
157
|
+
02-beta.md)
|
|
158
|
+
|
|
159
|
+
The deadlock detection logic already existed in dryRun()
|
|
160
|
+
(orchestrator.ts:222-231): after simulating waves, any tickets
|
|
161
|
+
still in ready-for-agent state are reported as deadlocked. This
|
|
162
|
+
test validates that behavior end-to-end through the subprocess
|
|
163
|
+
CLI with a circular dependency scenario.
|
|
164
|
+
|
|
165
|
+
All 162 tests pass (setup: 66, orchestrator: 54, integration: 42).
|
|
166
|
+
Lint clean.
|
|
167
|
+
|
|
168
|
+
## 2026-08-28 - US-010: Setup-then-execute composition test
|
|
169
|
+
|
|
170
|
+
Completed. Added 1 composition test (12 assertions) to
|
|
171
|
+
test/integration.test.ts:
|
|
172
|
+
- Calls setup() with tracker:'local' in a temp project dir,
|
|
173
|
+
using dist/skills and dist/docs-templates as source dirs
|
|
174
|
+
- Creates .scratch/my-feature/issues/ with 2 ticket files
|
|
175
|
+
(second blocked by first) and a spec.md
|
|
176
|
+
- Spawns the CLI as a subprocess with mock-agent-pass.js,
|
|
177
|
+
passing --dir and --spec pointing at the .scratch layout
|
|
178
|
+
- Asserts exit code 0
|
|
179
|
+
- Asserts both tickets updated to **Status:** done
|
|
180
|
+
- Asserts log files created in .scratch/my-feature/logs/
|
|
181
|
+
- Asserts docs/agents/issue-tracker.md exists (from setup)
|
|
182
|
+
- Asserts all 6 .agents/skills/ directories exist (from setup)
|
|
183
|
+
|
|
184
|
+
Added import of setup and SKILL_DIRS from ../src/setup.js to
|
|
185
|
+
the integration test file.
|
|
186
|
+
|
|
187
|
+
All 174 tests pass (setup: 66, orchestrator: 54, integration: 54).
|
|
188
|
+
Lint clean.
|
|
189
|
+
|
|
190
|
+
All 10 user stories now have passes: true. PRD complete.
|
package/ralph_claude.sh
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# ralph_claude.sh -- one-task-per-session agentic loop for Claude Code.
|
|
4
|
+
#
|
|
5
|
+
# Each loop iteration = exactly ONE Claude session = ideally ONE task + commit.
|
|
6
|
+
# The bash loop owns the cadence; Claude owns the work inside a single session.
|
|
7
|
+
#
|
|
8
|
+
# Usage: ./ralph_claude.sh [MAX_ITERATIONS] [MAX_TURNS_PER_SESSION]
|
|
9
|
+
# ./ralph_claude.sh 200 80
|
|
10
|
+
#
|
|
11
|
+
set -uo pipefail # NOT -e: we handle per-iteration failures ourselves.
|
|
12
|
+
|
|
13
|
+
# --- Config -----------------------------------------------------------------
|
|
14
|
+
PROMPT_FILE="PROMPT.md"
|
|
15
|
+
PRD_FILE="specs/prd.json"
|
|
16
|
+
LOG_FILE="progress.log"
|
|
17
|
+
MODEL="claude-opus-4-6"
|
|
18
|
+
MAX_ITERATIONS=${1:-10}
|
|
19
|
+
MAX_TURNS_PER_SESSION=${2:-80} # hard ceiling so a single session can't run away
|
|
20
|
+
STALL_LIMIT=4 # bail after this many no-progress iterations
|
|
21
|
+
ITERATION=0
|
|
22
|
+
STALLED_COUNT=0
|
|
23
|
+
TMP_CONTEXT="" # globals so the EXIT trap can clean them up
|
|
24
|
+
TMP_CAPTURE=""
|
|
25
|
+
|
|
26
|
+
# Adaptive reasoning ladder, indexed by stall count (clamps at the last entry).
|
|
27
|
+
# Valid levels: low | medium | high | xhigh | max -- AVAILABILITY DEPENDS ON THE
|
|
28
|
+
# MODEL. If your $MODEL rejects a level (e.g. "max"), trim the high end here.
|
|
29
|
+
# With STALL_LIMIT=4 the loop runs at stall 0,1,2,3 before bailing, so all four
|
|
30
|
+
# tiers fire. Progress resets STALLED_COUNT=0, which drops effort back to the
|
|
31
|
+
# baseline automatically -- no separate "reset to medium" logic needed.
|
|
32
|
+
EFFORT_TIERS=("medium" "high" "max")
|
|
33
|
+
|
|
34
|
+
# --- UI Colors --------------------------------------------------------------
|
|
35
|
+
BLUE='\033[1;34m'; YELLOW='\033[1;33m'; GREEN='\033[1;32m'; MAGENTA='\033[1;35m'; RED='\033[1;31m'; NC='\033[0m'
|
|
36
|
+
log_info() { echo -e "${BLUE}âšī¸ $1${NC}"; }
|
|
37
|
+
log_step() { echo -e "${YELLOW}â $1${NC}"; }
|
|
38
|
+
log_success() { echo -e "${GREEN}â
$1${NC}"; }
|
|
39
|
+
log_warn() { echo -e "${MAGENTA}â ī¸ $1${NC}"; }
|
|
40
|
+
log_error() { echo -e "${RED}â $1${NC}"; }
|
|
41
|
+
|
|
42
|
+
cleanup() { rm -f "${TMP_CONTEXT:-}" "${TMP_CAPTURE:-}" 2>/dev/null || true; }
|
|
43
|
+
trap cleanup EXIT
|
|
44
|
+
trap 'echo; log_warn "Stopping Ralph Loop..."; exit 130' INT
|
|
45
|
+
|
|
46
|
+
# --- Preflight --------------------------------------------------------------
|
|
47
|
+
for bin in claude jq git; do
|
|
48
|
+
command -v "$bin" >/dev/null 2>&1 || { log_error "'$bin' not found in PATH."; exit 1; }
|
|
49
|
+
done
|
|
50
|
+
for f in "$PROMPT_FILE" "$PRD_FILE"; do
|
|
51
|
+
[ -f "$f" ] || { log_error "Missing required file: $f"; exit 1; }
|
|
52
|
+
done
|
|
53
|
+
git rev-parse --is-inside-work-tree >/dev/null 2>&1 || { log_error "Not inside a git repo."; exit 1; }
|
|
54
|
+
touch "$LOG_FILE"
|
|
55
|
+
|
|
56
|
+
# --- Helpers ----------------------------------------------------------------
|
|
57
|
+
# Is the PRD parseable and shaped as expected? Distinguishes a real "done"
|
|
58
|
+
# state from a corrupted file (which must NOT look like success).
|
|
59
|
+
prd_ok() {
|
|
60
|
+
jq -e 'has("userStories") and (.userStories | type == "array")' "$PRD_FILE" >/dev/null 2>&1
|
|
61
|
+
}
|
|
62
|
+
# Count tasks not yet passing, excluding manual:true (those require a
|
|
63
|
+
# human-in-the-loop device session -- see ralph_device.sh -- and must never
|
|
64
|
+
# be selected or counted by this autonomous loop). Returns -1 on parse failure.
|
|
65
|
+
count_pending() {
|
|
66
|
+
jq -r '[.userStories[] | select((.passes == false or .passes == null) and (.manual != true))] | length' "$PRD_FILE" 2>/dev/null || echo "-1"
|
|
67
|
+
}
|
|
68
|
+
# Count manual:true stories still not passing, for the "only manual work
|
|
69
|
+
# left" report. These are surfaced but never selected or touched here.
|
|
70
|
+
count_pending_manual() {
|
|
71
|
+
jq -r '[.userStories[] | select((.passes == false or .passes == null) and (.manual == true))] | length' "$PRD_FILE" 2>/dev/null || echo "-1"
|
|
72
|
+
}
|
|
73
|
+
# Pretty list of pending tasks (for visibility / context). Manual stories are
|
|
74
|
+
# still listed, tagged, so a session knows they exist and to leave them alone.
|
|
75
|
+
pending_list() {
|
|
76
|
+
jq -r '.userStories[] | select(.passes == false or .passes == null) | "[\(.id)] \(.title)" + (if .manual == true then " (manual -- excluded from this loop)" else "" end)' "$PRD_FILE" 2>/dev/null
|
|
77
|
+
}
|
|
78
|
+
# Pretty list of manual:true stories still not passing.
|
|
79
|
+
pending_manual_list() {
|
|
80
|
+
jq -r '.userStories[] | select((.passes == false or .passes == null) and (.manual == true)) | "[\(.id)] \(.title)"' "$PRD_FILE" 2>/dev/null
|
|
81
|
+
}
|
|
82
|
+
# The single highest-priority pending, non-manual task. Empty string when the
|
|
83
|
+
# PRD is valid and has no such task left (caller checks count_pending_manual()
|
|
84
|
+
# to distinguish "fully done" from "only manual work remains").
|
|
85
|
+
current_task() {
|
|
86
|
+
jq -r '
|
|
87
|
+
[.userStories[] | select((.passes == false or .passes == null) and (.manual != true))]
|
|
88
|
+
| sort_by(.priority // 9999)
|
|
89
|
+
| (.[0] // empty) | "[\(.id)] \(.title)"
|
|
90
|
+
' "$PRD_FILE" 2>/dev/null
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
# Map the stall count onto a reasoning effort level from EFFORT_TIERS, clamping
|
|
94
|
+
# at the last tier. This is the model-side half of escalation: it raises the
|
|
95
|
+
# actual thinking budget via `claude --effort`. The prompt-side half lives in
|
|
96
|
+
# reasoning_hint() below; both ramp on the same counter and stay in sync.
|
|
97
|
+
effort_for_stall() {
|
|
98
|
+
local n="$1" last=$(( ${#EFFORT_TIERS[@]} - 1 ))
|
|
99
|
+
[ "$n" -gt "$last" ] && n="$last"
|
|
100
|
+
echo "${EFFORT_TIERS[$n]}"
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
# Reasoning hint injected into the prompt, escalating with the stall count.
|
|
104
|
+
# With STALL_LIMIT=4 the hint is evaluated at stall 0,1,2,3 -> all tiers fire
|
|
105
|
+
# before the loop bails. (Prompt-based escalation is version-proof; it doesn't
|
|
106
|
+
# rely on any particular `claude` reasoning flag existing.) This reinforces the
|
|
107
|
+
# `--effort` ladder driven by effort_for_stall().
|
|
108
|
+
reasoning_hint() {
|
|
109
|
+
case "$1" in
|
|
110
|
+
0) echo "" ;;
|
|
111
|
+
1) echo "REASONING: Think step by step before acting." ;;
|
|
112
|
+
2) echo "REASONING: This task stalled once already. Re-read progress.log and the spec, question your earlier assumptions, and try a different approach." ;;
|
|
113
|
+
*) echo "REASONING: This task has stalled repeatedly. Think very hard. Re-read the Codebase Patterns and recent learnings in progress.log, abandon the approaches that have already failed, and attempt a genuinely different strategy." ;;
|
|
114
|
+
esac
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
# --- Main loop --------------------------------------------------------------
|
|
118
|
+
while [ "$ITERATION" -lt "$MAX_ITERATIONS" ]; do
|
|
119
|
+
ITERATION=$((ITERATION + 1))
|
|
120
|
+
|
|
121
|
+
# 0. Never treat a broken PRD as completion.
|
|
122
|
+
if ! prd_ok; then
|
|
123
|
+
log_error "$PRD_FILE is missing/malformed or has no 'userStories' array. Stopping for human review."
|
|
124
|
+
exit 3
|
|
125
|
+
fi
|
|
126
|
+
|
|
127
|
+
# 1. Pick the one task for this session.
|
|
128
|
+
TASK="$(current_task)"
|
|
129
|
+
if [ -z "$TASK" ]; then
|
|
130
|
+
PENDING_MANUAL="$(count_pending_manual)"
|
|
131
|
+
if [ "$PENDING_MANUAL" -gt 0 ]; then
|
|
132
|
+
log_success "No headless work left â only manual:true stories remain:"
|
|
133
|
+
pending_manual_list
|
|
134
|
+
log_info "Run each via ./ralph_device.sh <story-id> in an attended session."
|
|
135
|
+
exit 0
|
|
136
|
+
fi
|
|
137
|
+
log_success "All tasks in $PRD_FILE pass. Nothing left to do."
|
|
138
|
+
exit 0
|
|
139
|
+
fi
|
|
140
|
+
PENDING_BEFORE="$(count_pending)"
|
|
141
|
+
HEAD_BEFORE="$(git rev-parse HEAD 2>/dev/null || echo none)"
|
|
142
|
+
|
|
143
|
+
# 1b. Pick this session's reasoning effort from the current stall count.
|
|
144
|
+
EFFORT="$(effort_for_stall "$STALLED_COUNT")"
|
|
145
|
+
|
|
146
|
+
# 2. Stall-driven friction relief: wipe stale local wrangler state.
|
|
147
|
+
if [ "$STALLED_COUNT" -eq 2 ] && [ -d ".wrangler/state/v3" ]; then
|
|
148
|
+
log_warn "Friction detected â clearing .wrangler/state/v3 ..."
|
|
149
|
+
rm -rf .wrangler/state/v3
|
|
150
|
+
fi
|
|
151
|
+
|
|
152
|
+
echo -e "${BLUE}------------------------------------------------------------${NC}"
|
|
153
|
+
log_info "ITERATION $ITERATION/$MAX_ITERATIONS | PENDING: $PENDING_BEFORE | STALL: $STALLED_COUNT/$STALL_LIMIT | EFFORT: $EFFORT"
|
|
154
|
+
log_step "TARGET: $TASK"
|
|
155
|
+
echo -e "${BLUE}------------------------------------------------------------${NC}"
|
|
156
|
+
|
|
157
|
+
# 3. Build the per-session context. Pin THIS task and forbid touching others.
|
|
158
|
+
TMP_CONTEXT="$(mktemp)"
|
|
159
|
+
{
|
|
160
|
+
cat "$PROMPT_FILE"
|
|
161
|
+
echo
|
|
162
|
+
echo "# THIS SESSION"
|
|
163
|
+
echo "Work on EXACTLY ONE task: $TASK"
|
|
164
|
+
echo "Complete it (write failing tests first, implement, lint, run only the"
|
|
165
|
+
echo "relevant tests, update progress.log, set passes:true for this story,"
|
|
166
|
+
echo "then commit). When that single task is done and committed, STOP. Do NOT"
|
|
167
|
+
echo "start any other task this session, and do NOT mark any other story passing."
|
|
168
|
+
echo
|
|
169
|
+
echo "Remaining tasks (context only â do not work on these now):"
|
|
170
|
+
pending_list
|
|
171
|
+
echo
|
|
172
|
+
echo "LAST_LOG_ENTRIES:"
|
|
173
|
+
tail -n 8 "$LOG_FILE" 2>/dev/null
|
|
174
|
+
HINT="$(reasoning_hint "$STALLED_COUNT")"
|
|
175
|
+
if [ -n "$HINT" ]; then echo; echo "$HINT"; fi
|
|
176
|
+
} > "$TMP_CONTEXT"
|
|
177
|
+
|
|
178
|
+
# 4. Execute one session. Stream events live for the human; tee the RAW jsonl
|
|
179
|
+
# to a capture file. tee sits BETWEEN claude and jq, so the capture holds
|
|
180
|
+
# claude's stdout regardless of any downstream jq hiccup.
|
|
181
|
+
# --effort raises the model-side reasoning budget as stalls accumulate.
|
|
182
|
+
TMP_CAPTURE="$(mktemp)"
|
|
183
|
+
set +o pipefail
|
|
184
|
+
cat "$TMP_CONTEXT" | claude -p --verbose \
|
|
185
|
+
--output-format stream-json \
|
|
186
|
+
--model "$MODEL" \
|
|
187
|
+
--effort "$EFFORT" \
|
|
188
|
+
--max-turns "$MAX_TURNS_PER_SESSION" \
|
|
189
|
+
--dangerously-skip-permissions \
|
|
190
|
+
| tee "$TMP_CAPTURE" \
|
|
191
|
+
| jq --unbuffered -r '
|
|
192
|
+
if .type == "system" then
|
|
193
|
+
( .subtype as $s
|
|
194
|
+
| if $s == "init" then "đĻ session start (" + (.model // "?") + ")"
|
|
195
|
+
elif $s == "api_retry" then "âŗ retry " + ((.attempt // 0)|tostring) + "/" + ((.max_retries // 0)|tostring)
|
|
196
|
+
+ " â " + (.error // "?") + " " + ((.error_status // "")|tostring)
|
|
197
|
+
elif $s == "compact_boundary" then "đī¸ context compacted"
|
|
198
|
+
elif $s == "thinking_tokens" then "đ§ thinking: " + ([to_entries[] | select(.value|type=="number") | "\(.key)=\(.value)"] | join(" "))
|
|
199
|
+
else "đĻ system: " + ($s // "?") end )
|
|
200
|
+
elif .type == "assistant" then
|
|
201
|
+
(.message.content[]? |
|
|
202
|
+
if .type == "text" then "đŦ " + .text
|
|
203
|
+
elif .type == "tool_use" then "đ§ " + .name + ": " + ((.input // {}) | tostring | .[0:140])
|
|
204
|
+
else empty end)
|
|
205
|
+
elif .type == "user" then
|
|
206
|
+
(.message.content[]? | if .type == "tool_result" then "âŠī¸ tool result" else empty end)
|
|
207
|
+
elif .type == "result" then
|
|
208
|
+
"â
" + (.subtype // "done") + " ($" + ((.total_cost_usd // 0) | tostring) + ")"
|
|
209
|
+
else empty end
|
|
210
|
+
' 2>/dev/null
|
|
211
|
+
# Capture pipe statuses IMMEDIATELY (before any other command clobbers them).
|
|
212
|
+
PSTATUS=("${PIPESTATUS[@]}")
|
|
213
|
+
set -o pipefail
|
|
214
|
+
CLAUDE_RC="${PSTATUS[1]:-0}" # 0=cat 1=claude 2=tee 3=jq
|
|
215
|
+
if [ "$CLAUDE_RC" -ne 0 ]; then
|
|
216
|
+
log_warn "claude exited non-zero (rc=$CLAUDE_RC) â auth/rate-limit/crash/unsupported --effort? Treating as no progress."
|
|
217
|
+
fi
|
|
218
|
+
|
|
219
|
+
# 5. Detect completion â but ONLY trust it if the PRD agrees all tasks pass.
|
|
220
|
+
# This kills false positives from claude merely *restating* the stop
|
|
221
|
+
# string while planning, and from any stream that echoes the prompt.
|
|
222
|
+
PENDING_AFTER="$(count_pending)"
|
|
223
|
+
if grep -q '<promise>COMPLETE</promise>' "$TMP_CAPTURE"; then
|
|
224
|
+
if [ "$PENDING_AFTER" -eq 0 ]; then
|
|
225
|
+
log_success "MISSION ACCOMPLISHED â COMPLETE reported and 0 tasks pending."
|
|
226
|
+
exit 0
|
|
227
|
+
else
|
|
228
|
+
log_warn "Saw COMPLETE signal but $PENDING_AFTER task(s) still pending â ignoring (likely planning text)."
|
|
229
|
+
fi
|
|
230
|
+
fi
|
|
231
|
+
|
|
232
|
+
# 6. Progress = a NEW COMMIT or FEWER PENDING TASKS. A merely-dirty tree is
|
|
233
|
+
# NOT progress: uncommitted flailing must not reset the stall counter,
|
|
234
|
+
# otherwise reasoning never escalates.
|
|
235
|
+
HEAD_AFTER="$(git rev-parse HEAD 2>/dev/null || echo none)"
|
|
236
|
+
if [ -n "$(git status --porcelain)" ]; then
|
|
237
|
+
log_warn "Working tree left dirty (uncommitted changes). Not counted as progress â Claude should commit completed work."
|
|
238
|
+
fi
|
|
239
|
+
|
|
240
|
+
if [ "$HEAD_AFTER" != "$HEAD_BEFORE" ] || [ "$PENDING_AFTER" -lt "$PENDING_BEFORE" ]; then
|
|
241
|
+
DELTA=$((PENDING_BEFORE - PENDING_AFTER))
|
|
242
|
+
if [ "$DELTA" -gt 1 ]; then
|
|
243
|
+
log_warn "Pending dropped by $DELTA (>1) â session completed more than one task. Tighten the prompt if this recurs."
|
|
244
|
+
fi
|
|
245
|
+
log_success "Progress (commits: $HEAD_BEFORE â $HEAD_AFTER, pending: $PENDING_BEFORE â $PENDING_AFTER). Effort resets to ${EFFORT_TIERS[0]}."
|
|
246
|
+
STALLED_COUNT=0
|
|
247
|
+
else
|
|
248
|
+
STALLED_COUNT=$((STALLED_COUNT + 1))
|
|
249
|
+
log_warn "No progress this session (stall $STALLED_COUNT/$STALL_LIMIT). Next effort: $(effort_for_stall "$STALLED_COUNT")."
|
|
250
|
+
fi
|
|
251
|
+
|
|
252
|
+
# Clean this iteration's tempfiles now (the EXIT trap is the safety net).
|
|
253
|
+
rm -f "$TMP_CONTEXT" "$TMP_CAPTURE"; TMP_CONTEXT=""; TMP_CAPTURE=""
|
|
254
|
+
|
|
255
|
+
# 7. Bail if we're truly stuck â avoid burning all iterations on a no-op.
|
|
256
|
+
if [ "$STALLED_COUNT" -ge "$STALL_LIMIT" ]; then
|
|
257
|
+
log_error "Stalled $STALL_LIMIT sessions in a row with escalation exhausted. Stopping for human review."
|
|
258
|
+
exit 2
|
|
259
|
+
fi
|
|
260
|
+
done
|
|
261
|
+
|
|
262
|
+
log_warn "Reached MAX_ITERATIONS=$MAX_ITERATIONS with tasks still pending ($(count_pending) left)."
|
|
263
|
+
exit 0
|